From 4df4c1ffb67e6ef603e8459c67cb6f5c3af2457b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 23:37:32 +0000 Subject: [PATCH] docs: drop em dashes from authored documentation Clause breaks were using em dashes as a default separator. The same sentences now use commas, periods, or a colon on a short list label. --- README.md | 28 +- agent-patterns/effect-concurrency.md | 14 +- agent-patterns/effect-errors.md | 16 +- agent-patterns/effect-scope.md | 18 +- agent-patterns/effect-stream.md | 14 +- docs/diagnostics.md | 554 +++++++++--------- docs/effect-conventions.md | 116 ++-- docs/entry-conventions.md | 300 +++++----- docs/framework-mode.md | 90 +-- docs/local-ci.md | 36 +- docs/mcp-conformance.md | 16 +- docs/preview-packages.md | 8 +- examples/audiobook-curator/README.md | 20 +- .../src/skills/curate-audiobooks/SKILL.md | 2 +- examples/hooks-and-scripts/README.md | 6 +- examples/host-test/README.md | 14 +- examples/rsc-agent-runtime/README.md | 10 +- examples/worktree-proximity/README.md | 46 +- packages/agent-bundle/README.md | 94 +-- packages/create-agent-bundle/README.md | 8 +- .../templates/cli-tool/README.md | 26 +- .../templates/mcp-server/README.md | 22 +- .../templates/minimal/README.md | 14 +- .../src/skills/getting-started/SKILL.md | 2 +- packages/rsc-markdown-stream/README.md | 26 +- packages/rsc-runtime/README.md | 88 +-- website/docs/en/contributing/index.mdx | 12 +- .../docs/en/examples/audiobook-curator.mdx | 22 +- .../docs/en/examples/hooks-and-scripts.mdx | 10 +- website/docs/en/examples/index.mdx | 6 +- website/docs/en/examples/mcp-app.mdx | 10 +- website/docs/en/examples/skills-starter.mdx | 6 +- website/docs/en/guide/authoring/hooks.mdx | 62 +- website/docs/en/guide/authoring/index.mdx | 40 +- website/docs/en/guide/authoring/mcp.mdx | 166 +++--- .../en/guide/authoring/package-entries.mdx | 68 +-- .../en/guide/authoring/reuse-framework.mdx | 2 +- .../en/guide/authoring/scripts-assets.mdx | 12 +- website/docs/en/guide/authoring/skills.mdx | 42 +- .../docs/en/guide/concepts/architecture.mdx | 40 +- .../docs/en/guide/development/evaluations.mdx | 16 +- website/docs/en/guide/development/index.mdx | 16 +- website/docs/en/guide/development/testing.mdx | 74 +-- .../docs/en/guide/development/workbench.mdx | 64 +- .../en/guide/distribution/installation.mdx | 60 +- .../guide/distribution/preview-packages.mdx | 10 +- .../docs/en/guide/distribution/validation.mdx | 40 +- website/docs/en/guide/start/capabilities.mdx | 2 +- website/docs/en/guide/start/installation.mdx | 10 +- website/docs/en/guide/start/quick-start.mdx | 6 +- website/docs/en/index.mdx | 24 +- website/docs/en/reference/api.mdx | 16 +- .../docs/en/reference/artifact-manifest.mdx | 58 +- website/docs/en/reference/cli.mdx | 58 +- website/docs/en/reference/configuration.mdx | 52 +- website/docs/en/reference/index.mdx | 4 +- website/docs/en/reference/limitations.mdx | 12 +- .../docs/en/reference/runtime-environment.mdx | 28 +- website/docs/en/reference/security.mdx | 14 +- website/docs/zh/contributing/index.mdx | 12 +- .../docs/zh/examples/audiobook-curator.mdx | 14 +- .../docs/zh/examples/hooks-and-scripts.mdx | 14 +- website/docs/zh/examples/index.mdx | 8 +- website/docs/zh/examples/mcp-app.mdx | 8 +- website/docs/zh/examples/skills-starter.mdx | 6 +- website/docs/zh/guide/authoring/hooks.mdx | 70 +-- website/docs/zh/guide/authoring/index.mdx | 36 +- website/docs/zh/guide/authoring/mcp.mdx | 154 ++--- .../zh/guide/authoring/package-entries.mdx | 56 +- .../zh/guide/authoring/reuse-framework.mdx | 2 +- .../zh/guide/authoring/scripts-assets.mdx | 14 +- website/docs/zh/guide/authoring/skills.mdx | 36 +- .../docs/zh/guide/concepts/architecture.mdx | 44 +- .../docs/zh/guide/development/evaluations.mdx | 16 +- website/docs/zh/guide/development/index.mdx | 12 +- website/docs/zh/guide/development/testing.mdx | 56 +- .../docs/zh/guide/development/workbench.mdx | 50 +- .../zh/guide/distribution/installation.mdx | 50 +- .../guide/distribution/preview-packages.mdx | 10 +- .../docs/zh/guide/distribution/validation.mdx | 42 +- website/docs/zh/guide/start/installation.mdx | 8 +- website/docs/zh/guide/start/quick-start.mdx | 6 +- website/docs/zh/index.mdx | 22 +- website/docs/zh/reference/api.mdx | 16 +- .../docs/zh/reference/artifact-manifest.mdx | 60 +- website/docs/zh/reference/cli.mdx | 56 +- website/docs/zh/reference/configuration.mdx | 44 +- website/docs/zh/reference/dev-server-http.mdx | 2 +- website/docs/zh/reference/index.mdx | 4 +- website/docs/zh/reference/limitations.mdx | 6 +- .../docs/zh/reference/runtime-environment.mdx | 28 +- website/docs/zh/reference/security.mdx | 14 +- 92 files changed, 1813 insertions(+), 1813 deletions(-) diff --git a/README.md b/README.md index 11a090651..59b38f7ca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # agent-bundle -agent-bundle compiles an agent plugin — skills, hooks, MCP servers, and scripts, described by one typed config — into installable output for Claude Code, Codex, Cursor, portable Agent Plugins clients, and experimental Amp. You write the plugin once; the compiler emits each selected host's entry, manifests, and wrappers into one composite root. Amp is contract-verified against the pinned PluginAPI and install layout; live activation, tools, and events are runtime-unverified (no Amp account in CI). +agent-bundle compiles an agent plugin, skills, hooks, MCP servers, and scripts, described by one typed config, into installable output for Claude Code, Codex, Cursor, portable Agent Plugins clients, and experimental Amp. You write the plugin once; the compiler emits each selected host's entry, manifests, and wrappers into one composite root. Amp is contract-verified against the pinned PluginAPI and install layout; live activation, tools, and events are runtime-unverified (no Amp account in CI). Documentation: [scriptedalchemy.github.io/agent-bundle](https://scriptedalchemy.github.io/agent-bundle/) (English and 简体中文; guide, reference, generated type API, host capability matrices). @@ -19,7 +19,7 @@ Use a PR number or the SHA of a commit whose package-preview run succeeded (ever ## Quick start -The fastest start is the scaffolder — it prompts for a name, a template +The fastest start is the scaffolder. It prompts for a name, a template (`minimal`, `mcp-server`, or `cli-tool`), and the host targets, then emits a project that already passes its own `check`: @@ -53,25 +53,25 @@ npx agent-bundle dev --root . # local workbench with live rebu `agent-bundle build` writes one composite root (`artifact/` by default; `--output` or `output.distPath` relocates it), and `targets` selects which host projections it carries: Amp's `.amp/plugins//index.js`, the `.claude-plugin/`, `.codex-plugin/`, and `.cursor-plugin/` manifests, and the portable `plugin.json`. Amp installs its nested directory; the other hosts install the composite root, and the generated `INSTALL.md` explains how. Omitting `targets` emits only the `portable` projection. The `amp` target is experimental: contract-verified, runtime-unverified. -The `portable` target is the [Agent Plugins open standard](https://agent-plugins.org/specification) (specification 1.0.0) adapter — the default projection, and the layout Cursor loads natively (Claude Code consumes it only through CLI translation). Every other client that reads this artifact is recorded with its tier, install command, and dated evidence in the `clients` section of that same capability table, rendered as the [hosts reference](https://scriptedalchemy.github.io/agent-bundle/reference/hosts). It emits the closed root `plugin.json` (canonical `$schema`, `name`, `version`, `description`, plus `author`, `homepage`, `repository`, `license`, `keywords`, and reverse-domain `extensions` authored under the `portable` config key), `skills//SKILL.md`, and `mcp.json` with stdio and Streamable HTTP servers whose `args`, `env` values, and `cwd` use the standard's `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` placeholders. Rules, commands, hooks, marketplaces, and client extension directories are honestly unavailable there because the v1 standard packages only skills and MCP servers. Both documents are validated against the vendored, hash-pinned 1.0.0 schemas and the normative text at plan time (`portable.mcp.*.standard`), after every ordinary build and `validate --artifact` (`AB6011`/`AB6012` plus the Agent Plugins byte lane `AB6035`–`AB6037`), under `validate --artifact --host-validation` (same lane with the `AB6038` provenance note), and by `agent-bundle doctor` for installed Cursor local plugins that declare the standard's `$schema` (`AB7320`); see [Diagnostics](docs/diagnostics.md#agent-plugins-portable-validation-ab6035ab6038). Pins live in `packages/agent-bundle/src/adapters/schemas/portable/PROVENANCE.json`; the capability table `packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json` carries a dated row for every standard feature. +The `portable` target is the [Agent Plugins open standard](https://agent-plugins.org/specification) (specification 1.0.0) adapter, the default projection, and the layout Cursor loads natively (Claude Code consumes it only through CLI translation). Every other client that reads this artifact is recorded with its tier, install command, and dated evidence in the `clients` section of that same capability table, rendered as the [hosts reference](https://scriptedalchemy.github.io/agent-bundle/reference/hosts). It emits the closed root `plugin.json` (canonical `$schema`, `name`, `version`, `description`, plus `author`, `homepage`, `repository`, `license`, `keywords`, and reverse-domain `extensions` authored under the `portable` config key), `skills//SKILL.md`, and `mcp.json` with stdio and Streamable HTTP servers whose `args`, `env` values, and `cwd` use the standard's `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` placeholders. Rules, commands, hooks, marketplaces, and client extension directories are honestly unavailable there because the v1 standard packages only skills and MCP servers. Both documents are validated against the vendored, hash-pinned 1.0.0 schemas and the normative text at plan time (`portable.mcp.*.standard`), after every ordinary build and `validate --artifact` (`AB6011`/`AB6012` plus the Agent Plugins byte lane `AB6035`–`AB6037`), under `validate --artifact --host-validation` (same lane with the `AB6038` provenance note), and by `agent-bundle doctor` for installed Cursor local plugins that declare the standard's `$schema` (`AB7320`); see [Diagnostics](docs/diagnostics.md#agent-plugins-portable-validation-ab6035ab6038). Pins live in `packages/agent-bundle/src/adapters/schemas/portable/PROVENANCE.json`; the capability table `packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json` carries a dated row for every standard feature. -Claude Code language servers are declared under `claude.lspServers`; the `claude` projection emits the record as plugin-root `.lsp.json`. Agent Bundle expands path tokens only in `command`, `args`, `env`, and `workspaceFolder`, and it does not include the language-server binary — install that separately so the declared command is available on `PATH`. Codex, Cursor, and the portable format do not currently receive this host-scoped configuration. +Claude Code language servers are declared under `claude.lspServers`; the `claude` projection emits the record as plugin-root `.lsp.json`. Agent Bundle expands path tokens only in `command`, `args`, `env`, and `workspaceFolder`, and it does not include the language-server binary, install that separately so the declared command is available on `PATH`. Codex, Cursor, and the portable format do not currently receive this host-scoped configuration. Claude Code plugin defaults are declared under `claude.settings` and emitted as plugin-root `settings.json`, which Claude Code applies when the plugin is enabled. The pinned contract supports only `agent` and `subagentStatusLine`; Agent Bundle rejects any other key rather than shipping a default Claude Code would silently ignore, and it expands no path tokens here because `settings.json` is absent from the host's placeholder-substitution table. Because the plugin `agents/` component is still deferred, declaring `agent` also raises a warning: the referenced agent has to reach the plugin root some other way, such as a prebuilt payload. -Cursor Plugin manifest metadata is declared under `cursor.*` and emitted verbatim into `.cursor-plugin/plugin.json` by the `cursor` projection: `author` (`name`, optional `email`), `homepage`, `repository`, `license`, `keywords`, plus the schema-admitted `publisher`, `category`, `tags`, and `minClientVersions` (for example `{ cursor: '3.13.0' }`). Every field is validated against the pinned `cursor/plugins` manifest schema before emission — `author.url`, non-HTTP URLs, empty strings, and loose semver are rejected with `cursor.manifest.*` errors, and an invalid block emits no partial metadata. The Cursor Plugin documents never mix Agent Plugin (`plugin.json`, `${PLUGIN_ROOT}`) paths or tokens into the Cursor Plugin format; the portable projection owns that format, even when both share one root. The full documented-surface contract matrix (every Cursor hook event, cloud availability, hook options, marketplace and team-distribution surfaces, canvases, agents) lives as dated `supported` / `unavailable` rows in `packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json`. +Cursor Plugin manifest metadata is declared under `cursor.*` and emitted verbatim into `.cursor-plugin/plugin.json` by the `cursor` projection: `author` (`name`, optional `email`), `homepage`, `repository`, `license`, `keywords`, plus the schema-admitted `publisher`, `category`, `tags`, and `minClientVersions` (for example `{ cursor: '3.13.0' }`). Every field is validated against the pinned `cursor/plugins` manifest schema before emission, `author.url`, non-HTTP URLs, empty strings, and loose semver are rejected with `cursor.manifest.*` errors, and an invalid block emits no partial metadata. The Cursor Plugin documents never mix Agent Plugin (`plugin.json`, `${PLUGIN_ROOT}`) paths or tokens into the Cursor Plugin format; the portable projection owns that format, even when both share one root. The full documented-surface contract matrix (every Cursor hook event, cloud availability, hook options, marketplace and team-distribution surfaces, canvases, agents) lives as dated `supported` / `unavailable` rows in `packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json`. -The same config also owns the npm package build — no second bundler config, bin shims, or hand-rolled stdio lifecycles. `bin` and `lib` entries (or the conventions `src/cli.ts`, `src/index.ts`, and `src/mcp/.ts`) emit executable `dist/bin/.js` bundles and a library output alongside the plugin root; an MCP entry that default-exports a server factory runs under a framework-owned stdio lifecycle; `tools.rsbuild` / `tools.rspack` is the one bundler escape hatch. [Entry conventions](docs/entry-conventions.md) is the full contract, and [Framework mode](docs/framework-mode.md) is the whole authoring model on one screen: structure in config and conventions (`src/skills//SKILL.md` ships with no declaration at all), JSX only where something is rendered. +The same config also owns the npm package build, no second bundler config, bin shims, or hand-rolled stdio lifecycles. `bin` and `lib` entries (or the conventions `src/cli.ts`, `src/index.ts`, and `src/mcp/.ts`) emit executable `dist/bin/.js` bundles and a library output alongside the plugin root; an MCP entry that default-exports a server factory runs under a framework-owned stdio lifecycle; `tools.rsbuild` / `tools.rspack` is the one bundler escape hatch. [Entry conventions](docs/entry-conventions.md) is the full contract, and [Framework mode](docs/framework-mode.md) is the whole authoring model on one screen: structure in config and conventions (`src/skills//SKILL.md` ships with no declaration at all), JSX only where something is rendered. ## Commands -- `build` — validate the project and write the plugin root (plus the `bin`/`lib` package build when declared) -- `validate` — check project source, or a built artifact with `--artifact ` -- `inspect` — show the normalized configuration and each selected host's projection plan; `--bundler` dumps the lowered Rspack config of every compiled output (post-`tools`-hatch merge, as Rslib/Rsbuild hand it to the compiler) -- `dev` — serve the local development workbench and rebuild the `dist/` package build when its inputs change; `--install-host ` installs a development variant whose stable `dev proxy` MCP command hot-swaps epochs behind the host's open connection and re-syncs hooks and Skills on every adopted rebuild (see [Framework mode › Live development into hosts](docs/framework-mode.md#live-development-into-hosts)) -- `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 +- `build`: validate the project and write the plugin root (plus the `bin`/`lib` package build when declared) +- `validate`: check project source, or a built artifact with `--artifact ` +- `inspect`: show the normalized configuration and each selected host's projection plan; `--bundler` dumps the lowered Rspack config of every compiled output (post-`tools`-hatch merge, as Rslib/Rsbuild hand it to the compiler) +- `dev`: serve the local development workbench and rebuild the `dist/` package build when its inputs change; `--install-host ` installs a development variant whose stable `dev proxy` MCP command hot-swaps epochs behind the host's open connection and re-syncs hooks and Skills on every adopted rebuild (see [Framework mode › Live development into hosts](docs/framework-mode.md#live-development-into-hosts)) +- `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 When the `claude` projection is selected, validating the built root also runs the installed Claude Code developer toolchain in addition to the pinned schemas. Use @@ -96,7 +96,7 @@ Run these from the repository root. `pnpm examples:check` validates and builds e ## Development -`pnpm check` runs the local delivery gate (build, unit and integration tests, lint, typecheck); `pnpm check:release` adds the packaging gates. `pnpm check:local-ci` mirrors the full hosted CI gate — the three-Node verify matrix plus the examples, release, and micro-eval jobs — in parallel local worktrees, and is the merge gate for the local-merge workflow described in [docs/local-ci.md](docs/local-ci.md). Versioning goes through Changesets. Native Claude/Codex host smokes are opt-in and intentionally skipped in CI. The Workbench architecture and the optional RSC runtime are documented in [docs/architecture/rsc-runtime-workbench.md](docs/architecture/rsc-runtime-workbench.md). +`pnpm check` runs the local delivery gate (build, unit and integration tests, lint, typecheck); `pnpm check:release` adds the packaging gates. `pnpm check:local-ci` mirrors the full hosted CI gate, the three-Node verify matrix plus the examples, release, and micro-eval jobs, in parallel local worktrees, and is the merge gate for the local-merge workflow described in [docs/local-ci.md](docs/local-ci.md). Versioning goes through Changesets. Native Claude/Codex host smokes are opt-in and intentionally skipped in CI. The Workbench architecture and the optional RSC runtime are documented in [docs/architecture/rsc-runtime-workbench.md](docs/architecture/rsc-runtime-workbench.md). ## Status diff --git a/agent-patterns/effect-concurrency.md b/agent-patterns/effect-concurrency.md index 85b362e40..0cab28715 100644 --- a/agent-patterns/effect-concurrency.md +++ b/agent-patterns/effect-concurrency.md @@ -4,14 +4,14 @@ Source: `repos/effect/packages/effect/src/Fiber.ts`, `Semaphore.ts`, `Latch.ts`, `Queue.ts`, `PubSub.ts`, `Deferred.ts`, and `Effect.ts` (`forkChild`, `forEach`, `all`, `raceFirst`). Refresh when the subtree moves. -Waves 4–6 grow the biggest concurrency surfaces (hook thin-clients under +Waves 4 to 6 grow the biggest concurrency surfaces (hook thin-clients under host deadlines, MCP progress projector, notices ledger, warm-runtime lifecycle). Build those Effect-native from day one behind Promise edges. ## Fibers -- `yield* Effect.forkChild(effect)` — structured child of the current fiber. -- `Fiber.await` / `Fiber.join` / `Fiber.interrupt` — observe or cancel. +- `yield* Effect.forkChild(effect)`: structured child of the current fiber. +- `Fiber.await` / `Fiber.join` / `Fiber.interrupt`: observe or cancel. - Prefer exported Fiber functions over `interruptUnsafe` / `pollUnsafe`. - Forked work that owns a resource must be forked *into a scope* (`forkScoped` / `Layer.scoped`) so interruption closes the resource. @@ -20,7 +20,7 @@ Host `AbortSignal` still exists at the edges (`dispatch()` / `stream()`). Do not thread extra internal signals once the program is an Effect; interrupt the fiber (`Stream.interruptWhen` + `abortToInterrupt`). `Latch.makeUnsafe` is the legal sync bridge when a web `ReadableStream.pull` must open demand -for an Effect stream — do not invent a second AbortSignal for that. +for an Effect stream, do not invent a second AbortSignal for that. ## Bounded work @@ -37,8 +37,8 @@ for an Effect stream — do not invent a second AbortSignal for that. ## Racing and cancellation -- `Effect.raceFirst(a, b)` — first to complete wins; the loser is interrupted. -- `interruptWhenAborted(effect, signal)` — host deadline into a race with +- `Effect.raceFirst(a, b)`: first to complete wins; the loser is interrupted. +- `interruptWhenAborted(effect, signal)`: host deadline into a race with `Effect.interrupt`. - Do not `Promise.race` around Effects. Race inside Effect, `runPromise` once. @@ -50,5 +50,5 @@ for an Effect stream — do not invent a second AbortSignal for that. `Fiber` you join/interrupt. - Using `PubSub` when consumers should compete (that's a `Queue`). - Using `Queue` when every subscriber needs every event (that's `PubSub`). -- Coalescing rebuilds with debounce timers (`setTimeout`) — `Latch` + +- Coalescing rebuilds with debounce timers (`setTimeout`): `Latch` + fiber, not host timers (`globalTimersInEffect`). diff --git a/agent-patterns/effect-errors.md b/agent-patterns/effect-errors.md index a78f73b26..870beb5a2 100644 --- a/agent-patterns/effect-errors.md +++ b/agent-patterns/effect-errors.md @@ -5,7 +5,7 @@ Source: `repos/effect/packages/effect/src/Cause.ts`, `Exit.ts`, `repos/effect/LLMS.md` § Error handling. Refresh when the subtree moves. This repo already has fail-closed typed errors. Effect's error channel maps -onto those classes at the boundary — it does not replace them. +onto those classes at the boundary. It does not replace them. ## Existing contracts (keep) @@ -29,7 +29,7 @@ onto those classes at the boundary — it does not replace them. `return yield* new RuntimeMcpRegistryError('RUNTIME_MCP_REGISTRY_CLOSED', message)`. `Effect.fail(new RuntimeMcpRegistryError(...))` is equally valid; do not churn call sites for style. -- Defect (bug): `Effect.die(defect)` — not for expected fail-closed states. +- Defect (bug): `Effect.die(defect)`: not for expected fail-closed states. - Recover: `Effect.catch`, `Effect.catchTag` when the error is tagged. None of our classes are tagged (`Data.Error`, not `Data.TaggedError` or `Schema.TaggedError`). Catch them with `Effect.catch((error) => ...)` and @@ -64,17 +64,17 @@ export class DevCoordinatorCloseError extends YieldableFrameworkError { The bases keep the `Error` / `CodedError` constructor shapes, so migrating an existing class is the `extends` clause plus the import. They also keep -the plain-`Error` observable shape — `JSON.stringify`, `stableJson`, -`{ ...error }`, `util.inspect`, non-enumerable `cause` — which rc.112 +the plain-`Error` observable shape, `JSON.stringify`, `stableJson`, +`{ ...error }`, `util.inspect`, non-enumerable `cause`, which rc.112 `Data.Error` alone would change (its prototype `toJSON` spreads the constructor fields; its `[nodejs.util.inspect.custom]` prints that instead of the stack). Never extend `Data.Error` directly. Stay on plain `Error` / `CodedError` when the class's declaration file is -reachable from any `package.json` export's `types` — exported or not; a +reachable from any `package.json` export's `types`, exported or not; a consumer's `tsc` follows the whole `.d.ts` graph, so `McpSessionError` (reached from `.` / `./api` through the dev types) stays plain even though -it is never exported — when it is reachable from an Effect-free entry +it is never exported, when it is reachable from an Effect-free entry (`agent-bundle/config`, `meta`, `rstest`, `test/browser`, the CLI `--help` path, the host MCP proxy), or when it ships inside an emitted artifact. `docs/effect-conventions.md` § "Yieldable framework errors" lists the @@ -103,9 +103,9 @@ Callers of `runPromise` see the same types they see today. - Putting `unknown` or global `Error` in the fail channel (`unknownInEffectCatch`, `globalErrorInEffectFailure`). - Swallowing interruption as a typed success. Cancellation is `AbortError`. - `Stream.toReadableStream`'s `Cause.squash` is not that mapping — use the + `Stream.toReadableStream`'s `Cause.squash` is not that mapping, use the boundary helper. - `Effect.runPromise` in a test to "see the error" when `Effect.exit` + - `Cause` is the assertion you want — still only through the boundary. + `Cause` is the assertion you want, still only through the boundary. - New public error codes without updating the authoring docs and the mapping table in `docs/effect-conventions.md`. diff --git a/agent-patterns/effect-scope.md b/agent-patterns/effect-scope.md index c93d89a59..162a29e17 100644 --- a/agent-patterns/effect-scope.md +++ b/agent-patterns/effect-scope.md @@ -17,18 +17,18 @@ const connection = Effect.acquireRelease( ); ``` -- `acquireRelease(acquire, release)` — release receives the exit so you can +- `acquireRelease(acquire, release)`: release receives the exit so you can distinguish success / fail / interrupt. -- `acquireDisposable` — when the resource already implements +- `acquireDisposable`: when the resource already implements `Symbol.dispose` / `Symbol.asyncDispose`. -- `addFinalizer` — extra cleanup on the current scope. -- `Effect.scoped(effect)` — provide a fresh scope and close it when `effect` +- `addFinalizer`: extra cleanup on the current scope. +- `Effect.scoped(effect)`: provide a fresh scope and close it when `effect` finishes (including interruption). -- `Effect.scopedWith((scope) => ...)` — when you must hold the `Scope` value. +- `Effect.scopedWith((scope) => ...)`: when you must hold the `Scope` value. Finalizers run in reverse acquire order. Interruption still runs them. -Do not `ReadableStream.cancel()` a stream React still holds a reader on — +Do not `ReadableStream.cancel()` a stream React still holds a reader on, that throws `ReadableStream is locked` (sync or as a rejected promise) and can defect the finalizer. Interrupt the Effect producer (`AbortSignal` + `Stream.interruptWhen`, or `runFork(Fiber.interrupt)`) instead of canceling @@ -69,7 +69,7 @@ the Effect and let `runPromise` observe the result. - Host → Effect: `runPromise(program, { signal })` or `interruptWhenAborted(program, signal)`. - Effect → host: `yield* scopedAbortSignal` (`Effect.abortSignal`). The - signal aborts when the owning scope closes — with no reason, and the + signal aborts when the owning scope closes, with no reason, and the controller is never exposed. Do not keep it longer than that scope. - A contract that must abort with a caller-supplied reason (MCP session `cancel()`) owns its `AbortController` as the scoped resource instead: @@ -79,7 +79,7 @@ the Effect and let `runPromise` observe the result. Do not allocate a bare `AbortController` inside `Effect.gen`; it must be the `acquireRelease` resource or come from `Effect.abortSignal`. No -`@effect/language-service@0.87.2` rule flags this — review does. +`@effect/language-service@0.87.2` rule flags this, review does. ## What to avoid @@ -90,5 +90,5 @@ Do not allocate a bare `AbortController` inside `Effect.gen`; it must be the - Sharing one connection across requests without a scope (or a documented process-lifetime Layer). - Importing `./state/contract` from the root runtime boundary to type - `AgentStateError` — duck-type by `error.name` so the kernel stays off the + `AgentStateError`, duck-type by `error.name` so the kernel stays off the package-root graph. diff --git a/agent-patterns/effect-stream.md b/agent-patterns/effect-stream.md index 1421dbb8e..47b5cc7d6 100644 --- a/agent-patterns/effect-stream.md +++ b/agent-patterns/effect-stream.md @@ -5,7 +5,7 @@ Source: `repos/effect/packages/effect/src/Stream.ts` (vendored v4, package `repos/effect/LLMS.md` § Working with Streams first. Stage 2 replaces the #145 pull-gated Flight `TransformStream` with Effect -`Stream`. Native pull backpressure is the point — do not re-implement a +`Stream`. Native pull backpressure is the point, do not re-implement a gated reader. ## Constructors @@ -32,11 +32,11 @@ Avoid inventing a custom pull loop. If the source is already a `switchMap` (cancel previous), `filter`, `tap`. - Merge: `merge`, `mergeAll`, `concat`. - Time: `timeout`, `schedule`, `repeat`. -- Resource: `Stream.scoped` — acquire inside the stream, release when it ends +- Resource: `Stream.scoped`: acquire inside the stream, release when it ends or is interrupted. - Consume: `Stream.runCollect`, `Stream.runFold`, `runForEach` / `runForEachArray`. - Edge out: `Stream.toReadableStream` / `toReadableStreamEffect` when a host - still wants a web stream. That helper itself calls `Effect.runFork` — only + still wants a web stream. That helper itself calls `Effect.runFork`, only legal via the package boundary if we wrap it; prefer staying on `Stream` until the Promise edge. @@ -51,7 +51,7 @@ add a second gate (`TransformStream` + manual pause) around an Effect stream. - `Stream.toReadableStream` calls `runFork` and maps failures with `Cause.squash`. Wrap it in the package boundary (`streamToReadableStream`) and use `mapCause` so interrupt-only causes stay `AbortError`. `tapError` - must error the web controller *before* scope finalizers run — a hanging + must error the web controller *before* scope finalizers run, a hanging Flight cancel otherwise hides bound-violation and abort failures. - Never `runPromise(Fiber.interrupt)` from `ReadableStream.cancel`. That cancel is invoked from `acquireRelease` on the parent event fiber; @@ -60,7 +60,7 @@ add a second gate (`TransformStream` + manual pause) around an Effect stream. Wait for event demand *then* `reader.read()` (`Stream.unfold` + Latch). Wait-after-`fromReadableStream` either over-pulls (fails backpressure) or never pulls (deadlock). Do not cancel the Flight byte stream when the - shell root arrives — later boundaries still need those bytes. React may + shell root arrives, later boundaries still need those bytes. React may still hold the reader at scope close; `stream.cancel()` then throws "locked" and must be swallowed. - `host.execute({ progress })` must run in the same turn as `stream()`. @@ -71,7 +71,7 @@ add a second gate (`TransformStream` + manual pause) around an Effect stream. - `Stream.paginate` is the pending-boundary loop (shell → replace/error* → complete). - `progress.report()` after complete must reject `handoff-required` on the - reporter, not only on the stream — share `createAgentRenderEventSequence`. + reporter, not only on the stream, share `createAgentRenderEventSequence`. - After a producer fail, a later `pull()` with HWM 0 must *reject*, not resolve. `controller.error` alone can lose the error if no read is pending. - Flight is not Ndjson. Do not adopt `effect/unstable/encoding` for this @@ -79,7 +79,7 @@ add a second gate (`TransformStream` + manual pause) around an Effect stream. ## What to avoid -- `for await` over a stream you already have as `Stream` — use `mapEffect` / +- `for await` over a stream you already have as `Stream`: use `mapEffect` / `runForEach`. - Encoding/decoding JSON by hand when `Stream.pipeThroughChannel` + `effect/unstable/encoding` (Ndjson / SchemaBinary) would do. Unstable diff --git a/docs/diagnostics.md b/docs/diagnostics.md index d4b05c964..c9c8adbfc 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -15,7 +15,7 @@ even when no error diagnostic was reported. | --- | --- | | `AB30xx` | Skill documents: Markdown parsing (`AB3000`–`AB3002`: unreadable, missing or malformed frontmatter), rendered-skill compilation (`AB3003`: module failed to load, `AB3004`: missing/invalid default component or `frontmatter` export, `AB3005`: content outside the supported Markdown element subset), and the Skill IR (`AB3006`: unknown frontmatter field; `AB3008`–`AB3010`: per-host lowering of tokens and frontmatter); see below. | | `AB40xx` | Plugin metadata and Skill source validation (`AB4000`/`AB4001`: name/version; `AB4002`–`AB4007`: Skill fields; `AB4008`–`AB4011` and `AB4013`: release identity; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project; `AB4014`/`AB4015`: the shared descriptive metadata every host projection reads); see below. | -| `AB41xx` | Normalized model invariants (`AB4100`–`AB4102`: unknown targets — the retired `plugin` name included — duplicate IDs and outputs; `AB4103`, `AB4105`, `AB4106`: the composite-root checks — same path with different bytes across selected projections, a host-scoped component leaking through conventional discovery, an advanced-registry adapter selected beside another target; see below). | +| `AB41xx` | Normalized model invariants (`AB4100`–`AB4102`: unknown targets, the retired `plugin` name included, duplicate IDs and outputs; `AB4103`, `AB4105`, `AB4106`: the composite-root checks, same path with different bytes across selected projections, a host-scoped component leaking through conventional discovery, an advanced-registry adapter selected beside another target; see below). | | `AB42xx` | Hook configuration and native hook sources (`AB4200`–`AB4212`; see below). | | `AB43xx` | MCP server and MCP App configuration (`AB4300`–`AB4339`, see below; `AB4340`: a declaration for a route-generated server redeclares `entry`/`command`/`url`; `AB4341`: the `web` exposure/policy key; see below). | | `AB44xx` | Script configuration (`AB4400`–`AB4408`; see below). | @@ -32,9 +32,9 @@ even when no error diagnostic was reported. | `AB490x`/`AB492x` | Conventional host components (#100 stage 2): rules `src/rules/*.mdc` (`AB4900`–`AB4908`) and commands `src/commands/*.md` (`AB4920`–`AB4928`), including per-host feature-set enforcement (`AB4907`/`AB4908`, `AB4927`/`AB4928`); see below. | | `AB48xx`/`AB494x` | Route graph, state, layout (`AB4830`–`AB4832`), route render budgets (`AB4835`), tool task support (`AB4836`), a route module that value-imports a compiler-carrying framework entry (`AB4837`), an event handler or view declaration (`AB4840`), a CLI surface projection of an MCP tool (`AB4843`–`AB4845`), and provider conventions (see below). | | `AB5000` | General CLI and adapter failures (see below). | -| `AB60xx` | Built-artifact validation, including schema documents and referenced files (`AB6005`: the compiler finds a host-pack surface or package-build entry (`dist/bin/*.js`, the Flight workers, or the `lib` entry) that keeps something other than a Node built-in, `pnpapi`, or an emitted sibling external, or an MCP App view that keeps anything external; the emitted-module walk remains only for what the compiler cannot see — an expression `import()` in a compiled module, and the imports and syntax of JavaScript the framework did not compile or a `tools` hatch may have rewritten; a `dist` finding names `dist/`; `AB6011`/`AB6012`: a target's required pinned-schema document is missing or invalid; `AB6025`: a manifest-declared `logo` path is missing from the artifact or escapes the deploy tree; `AB6034`: emitted Skill Markdown has no instruction body; `AB6035`–`AB6038`: Agent Plugins portable validation, see below). | +| `AB60xx` | Built-artifact validation, including schema documents and referenced files (`AB6005`: the compiler finds a host-pack surface or package-build entry (`dist/bin/*.js`, the Flight workers, or the `lib` entry) that keeps something other than a Node built-in, `pnpapi`, or an emitted sibling external, or an MCP App view that keeps anything external; the emitted-module walk remains only for what the compiler cannot see, an expression `import()` in a compiled module, and the imports and syntax of JavaScript the framework did not compile or a `tools` hatch may have rewritten; a `dist` finding names `dist/`; `AB6011`/`AB6012`: a target's required pinned-schema document is missing or invalid; `AB6025`: a manifest-declared `logo` path is missing from the artifact or escapes the deploy tree; `AB6034`: emitted Skill Markdown has no instruction body; `AB6035`–`AB6038`: Agent Plugins portable validation, see below). | | `AB6200`–`AB6202` | Workbench artifact inspection over published epochs: `AB6200` the validator threw or an internal post-validation invariant failed, `AB6201` an epoch reference could not be released, `AB6202` unsafe runtime metadata. Artifact-validation diagnostics such as `AB6001` retain their original codes (see below). | -| `AB700x` | Host installation and uninstallation: bundle identity, host availability, scope, command failure, and collision checks (`AB7000`–`AB7004`: unsupported host, unreadable bundle identity, missing host, scope or mode refusal, host command failure — the same five codes are also the development project service's preparation failures; `AB7001` in detail: the composite root at `--from` cannot be resolved for the host from its `agent-bundle.manifest.json` — the manifest is missing or not canonical, has no `projections[]` row for the host, the row has no host plugin manifest pointer or the pointed file is missing, a `files[]` row is missing or its bytes, size, digest, or executable state differ from the row after npm normalization, `claude`/`codex` have no marketplace identity, or the `cursor` plugin name is not a safe local plugin name; `install`, `uninstall`, and `doctor` never probe `.claude-plugin/plugin.json` or look under `/`; `AB7005`: version collision, pre-receipt content collision, or foreign install; `AB7006`: the host lists the installed copy with load errors; see below), plus the `uninstall` refusals `AB7007`–`AB7009` (ownership or content mismatch, unconfirmed data purge, missing receipt; see below). | +| `AB700x` | Host installation and uninstallation: bundle identity, host availability, scope, command failure, and collision checks (`AB7000`–`AB7004`: unsupported host, unreadable bundle identity, missing host, scope or mode refusal, host command failure, the same five codes are also the development project service's preparation failures; `AB7001` in detail: the composite root at `--from` cannot be resolved for the host from its `agent-bundle.manifest.json`, the manifest is missing or not canonical, has no `projections[]` row for the host, the row has no host plugin manifest pointer or the pointed file is missing, a `files[]` row is missing or its bytes, size, digest, or executable state differ from the row after npm normalization, `claude`/`codex` have no marketplace identity, or the `cursor` plugin name is not a safe local plugin name; `install`, `uninstall`, and `doctor` never probe `.claude-plugin/plugin.json` or look under `/`; `AB7005`: version collision, pre-receipt content collision, or foreign install; `AB7006`: the host lists the installed copy with load errors; see below), plus the `uninstall` refusals `AB7007`–`AB7009` (ownership or content mismatch, unconfirmed data purge, missing receipt; see below). | | `AB7010`–`AB7015` | npm prepack inventory, artifact freshness, package bin targets, release-version agreement, and installed-dependency hygiene (`AB7014`: a dependency no consumer-runtime evidence requires; `AB7015`: a git, remote-tarball, path, or unrewritten workspace-protocol dependency specifier). | | `AB7200`–`AB7202`, `AB7210`–`AB7211` | Development rebuilds and live host surfaces: rebuild admission and phase failures, development host install sync, and the dev-epoch contract gate (see below). | | `AB7xxx` | Project preparation and development rebuilds (`AB7100`–`AB7102`: a development rebuild's compilation, publication, and cleanup; `AB7101` is also the one-shot `build` / `build()` refusal when source changes during compilation; `AB7103`: the development package build; see below). | @@ -47,7 +47,7 @@ even when no error diagnostic was reported. | `AB8243` | Workbench browser-side strict decoder rejecting a `/api/trace` replay or NDJSON stream frame (unknown `source`, malformed correlation, unsafe text, or a cursor the reply does not account for). It sits between the server-side trace routes (`AB8240`–`AB8242`) and the hook receipt route (`AB8247`–`AB8249`); `AB8244`–`AB8246` are unassigned. | | `AB8231`–`AB8232`, `AB8236`–`AB8238` | Workbench route invocation service (`/api/routes/invocations`): `AB8231` unknown route or invocation id, `AB8232` no published build / invocation manifest unavailable, `AB8236` render child timed out or crashed, `AB8237` malformed invocation request, and `AB8238` unknown fixture id. (`AB8233`–`AB8235` were already assigned to browser-side decoders.) | | `AB8240`–`AB8242` | Workbench unified trace routes (`/api/trace`, `/api/trace/stream`): `AB8240` invalid `after` cursor (400), `AB8241` cursor ahead of the current trace sequence (409), and `AB8242` trace routes unavailable before composition or during shutdown (404/503). | -| `AB8247`–`AB8249` | Workbench hook receipt route (`POST /api/trace/receipts`, posted by a generated hook wrapper of the dev plugin): `AB8247` receipt refused — peer not loopback, `Origin` header present, missing or wrong bearer token (403), or receipts closed (409); `AB8248` malformed receipt — query string, non-object body, unknown key, out-of-range enum, or unbounded field (400, the message names the field); `AB8249` receipt over the 16 KiB limit (413). | +| `AB8247`–`AB8249` | Workbench hook receipt route (`POST /api/trace/receipts`, posted by a generated hook wrapper of the dev plugin): `AB8247` receipt refused, peer not loopback, `Origin` header present, missing or wrong bearer token (403), or receipts closed (409); `AB8248` malformed receipt, query string, non-object body, unknown key, out-of-range enum, or unbounded field (400, the message names the field); `AB8249` receipt over the 16 KiB limit (413). | | `AB8239` | Workbench route invocation service (`/api/routes/invocations`): the published manifest digest or source revision moved while the request waited for a concurrency slot (409). Retry against the current revision so the recorded `manifestDigest`/`sourceRevision` cannot describe a different build than the one that ran. | | `AB8250`–`AB8255` | Workbench production route execution: `AB8250` no manifest-selected published compiler artifact is available, `AB8251` the selected route/surface/host has no eligible executable or preparation binding in the published artifact, `AB8252` the selected compiled CLI projection or event preparation could not be imported or failed, `AB8253` a selected CLI command does not project onto the canonical operation id, `AB8254` a projected `cli:` id was used instead of its canonical `tool:/` id plus CLI surface, and `AB8255` an event route with compiled handler was submitted without a concrete host surface. Rebuild the project or choose an eligible emitted host for `AB8250`/`AB8251`; fix the reported projection or handler failure for `AB8252`; use the command or canonical operation named by `AB8253`/`AB8254`; select a generated host wrapper for `AB8255`. | | `AB8256` | Workbench route invocation cancellation (`POST /api/routes/invocations//cancel`): the invocation is already final (409). Reload the final invocation instead of cancelling it. | @@ -61,7 +61,7 @@ even when no error diagnostic was reported. | `AB8110`–`AB8113` | Workbench standalone MCP Inspector routes (`/api/inspector/status`, `/api/inspector/launch`): `AB8110` invalid path, `AB8111` invalid request shape or query, `AB8112` the Inspector could not be launched (spawn failure, exit before publishing a URL, or the 30 s startup budget elapsed; 502), `AB8113` routes not available (404 when the launcher is not composed, 503 after shutdown). | | `AB8120`–`AB8123` | Workbench route manifest (`/api/routes/manifest`): `AB8120` invalid path, `AB8121` not available (404/409/503), `AB8122` query string on the request, `AB8123` the browser client could not decode the response (see below). | | `AB8024`–`AB8025` | Live host MCP proxy: epoch drift behind a host connection and dev-server unavailability (see below). | -| `AB80xx` | Development server: `AB8000` server construction refusals, `AB8001`–`AB8012` shared transport and foreground routes (origin, session, body, cursor, asset, and Skill route refusals), then one block per route module — MCP sessions (`AB8013`–`AB8019`), MCP App previews (`AB8020`–`AB8023`), hook playground (`AB8030`–`AB8034`), prompt playground (`AB8040`–`AB8057`), artifact epochs (`AB8060`–`AB8068`), evals (`AB8070`–`AB8083`, `AB8085`–`AB8088`), development logs (`AB8090`–`AB8093`); see "Development server" below. | +| `AB80xx` | Development server: `AB8000` server construction refusals, `AB8001`–`AB8012` shared transport and foreground routes (origin, session, body, cursor, asset, and Skill route refusals), then one block per route module, MCP sessions (`AB8013`–`AB8019`), MCP App previews (`AB8020`–`AB8023`), hook playground (`AB8030`–`AB8034`), prompt playground (`AB8040`–`AB8057`), artifact epochs (`AB8060`–`AB8068`), evals (`AB8070`–`AB8083`, `AB8085`–`AB8088`), development logs (`AB8090`–`AB8093`); see "Development server" below. | | `AB9xxx` | Eval selection, harnesses, and persisted runs. | ## Skill documents and Skill IR lowering (`AB3000`–`AB3010`) @@ -71,7 +71,7 @@ entry names) holds a hand-authored `SKILL.md` or a rendered source module (`SKILL.tsx`/`SKILL.ts`; `docs/framework-mode.md`). Parsing runs at discovery and reports `AB3000`–`AB3005` on the document; the Skill IR is then read from the frontmatter (`AB3006`) during source validation, and the IR is lowered once -per selected skill host — `amp`, `claude`, `codex`, `cursor`, `portable` — +per selected skill host, `amp`, `claude`, `codex`, `cursor`, `portable`, with `AB3008`–`AB3010` judged on the normalized model and carrying `target`. A skill whose frontmatter declares no host extension and whose body carries no token passes through byte for byte @@ -79,16 +79,16 @@ and is never lowered. `AB3007` is not assigned. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB3000` | error | `SKILL.md` could not be read: `Unable to read Skill Markdown: ` — a permissions failure, a directory in its place, or a skill directory (an explicit `skills` path, or one that lost its file mid-run) with neither `SKILL.md` nor a rendered source module. | Make the file readable, or remove the directory from `src/skills/` and from `skills`. | +| `AB3000` | error | `SKILL.md` could not be read: `Unable to read Skill Markdown: `, a permissions failure, a directory in its place, or a skill directory (an explicit `skills` path, or one that lost its file mid-run) with neither `SKILL.md` nor a rendered source module. | Make the file readable, or remove the directory from `src/skills/` and from `skills`. | | `AB3001` | error | `SKILL.md` does not open with a `---` YAML frontmatter block. | Start the document with `---`, the YAML fields, and a closing `---`. | | `AB3002` | error | The frontmatter YAML does not parse, or parses to something other than an object (a list or a scalar); the message carries the parser error. | Repair the YAML between the `---` fences. | -| `AB3003` | error | The rendered skill module failed to load: `Rendered Skill module failed to load: ` — a syntax error, an unresolved import, or a throw at module evaluation (the published `agent-bundle/meta` reached without a project identity raises `AB4760` here). | Fix the module so it imports and evaluates; `agent-bundle validate` prints the message. | +| `AB3003` | error | The rendered skill module failed to load: `Rendered Skill module failed to load: `, a syntax error, an unresolved import, or a throw at module evaluation (the published `agent-bundle/meta` reached without a project identity raises `AB4760` here). | Fix the module so it imports and evaluates; `agent-bundle validate` prints the message. | | `AB3004` | error | The rendered skill module does not default-export a component function, does not export a `frontmatter` plain object, or its `frontmatter` cannot be serialized as YAML. | Export both a default component and `export const frontmatter = { name, description, … }`. | -| `AB3005` | error | Rendering the component to Markdown failed: an element outside the supported subset (`h1`–`h6`, `p`, `ul`/`ol`/`li`, `strong`/`b`, `em`/`i`, `code`, `pre`, `blockquote`, `a`, `hr`, `br`, fragments, text), a structural rule of that subset (`` without a nonempty string `href`, `
` holding anything but text or one ``, a list child that is not `
  • `, an empty list), a component that threw or resolved past the depth limit, a value that is neither text nor a supported element, or content that produced no Markdown; the message names the construct. | Write the content within the supported elements, or hand-author `SKILL.md`. | +| `AB3005` | error | Rendering the component to Markdown failed: an element outside the supported subset (`h1` to `h6`, `p`, `ul`/`ol`/`li`, `strong`/`b`, `em`/`i`, `code`, `pre`, `blockquote`, `a`, `hr`, `br`, fragments, text), a structural rule of that subset (`` without a nonempty string `href`, `
    ` holding anything but text or one ``, a list child that is not `
  • `, an empty list), a component that threw or resolved past the depth limit, a value that is neither text nor a supported element, or content that produced no Markdown; the message names the construct. | Write the content within the supported elements, or hand-author `SKILL.md`. | | `AB3006` | error | Skill frontmatter declares a field that is not a portable Agent Skills field (`name`, `description`, `license`, `compatibility`, `allowed-tools`, `metadata`), not a typed host extension (Claude: `agent`, `argument-hint`, `arguments`, `background`, `context`, `disallowed-tools`, `effort`, `hooks`, `model`, `shell`, `user-invocable`, `when_to_use`; Cursor: `color`, `globs`, `icon`; Amp: `builtin-tools`, `mcpServers`; shared: `disable-model-invocation`, `paths`), and not the authoring key `targets`; or `targets` is not an object, carries a key other than `amp`, `claude`, `codex`, `cursor`, or holds an unknown key inside `targets.` (including `targets.amp`, `targets.codex.interface`, `.policy`, `.dependencies`, and `.dependencies.tools[]`). The message names the field path. | Move host-only fields into `targets.` or a documented host key, or remove the unknown field. | | `AB3008` | error | The Skill Markdown body uses a canonical token (`agent-bundle:token:arguments`, `agent-bundle:path:plugin-root`, `agent-bundle:path:plugin-data`, `agent-bundle:path:workspace-root`, `agent-bundle:token:session-identity`, `agent-bundle:token:skill-root`) or a host alias of one (`$ARGUMENTS`, `${CLAUDE_PLUGIN_ROOT}`, `${PLUGIN_ROOT}`, `${workspaceFolder}`, …), and the skill lowers to a host whose pinned Skill Markdown contract documents no interpolation placeholder: Amp, Codex, Cursor, and portable. Claude documents all six. One diagnostic per token per host: `Skill token "" has no Skill Markdown equivalent.` | Remove the token, restrict the skill to a host that documents it, or move the reference to a document that host interpolates. | -| `AB3009` | error | After token lowering, the `` Skill Markdown body still contains a placeholder spelling another host's contract owns — for Amp, Codex, Cursor, and portable documents, one of Claude's `$ARGUMENTS`, `${CLAUDE_PLUGIN_DATA}`, `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_SESSION_ID}`, `${CLAUDE_SKILL_DIR}`; the message names the syntax. | Use canonical agent-bundle tokens so lowering emits only this host's documented placeholders. | -| `AB3010` | error | The lowered `` skill frontmatter fails that host's pinned schema — `schemas/skill-hosts/claude-skill-frontmatter.schema.json` and `cursor-skill-frontmatter.schema.json` for Claude and Cursor, the Agent Skills frontmatter schema for Amp, Codex, and portable, and Amp's pinned skill MCP rules for an Amp skill's `mcpServers`: `Lowered Skill document .` | Remove the unsupported field or restrict the skill to a host that documents it. | +| `AB3009` | error | After token lowering, the `` Skill Markdown body still contains a placeholder spelling another host's contract owns, for Amp, Codex, Cursor, and portable documents, one of Claude's `$ARGUMENTS`, `${CLAUDE_PLUGIN_DATA}`, `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_SESSION_ID}`, `${CLAUDE_SKILL_DIR}`; the message names the syntax. | Use canonical agent-bundle tokens so lowering emits only this host's documented placeholders. | +| `AB3010` | error | The lowered `` skill frontmatter fails that host's pinned schema, `schemas/skill-hosts/claude-skill-frontmatter.schema.json` and `cursor-skill-frontmatter.schema.json` for Claude and Cursor, the Agent Skills frontmatter schema for Amp, Codex, and portable, and Amp's pinned skill MCP rules for an Amp skill's `mcpServers`: `Lowered Skill document .` | Remove the unsupported field or restrict the skill to a host that documents it. | ## Plugin metadata and Skill source validation (`AB4000`–`AB4007`, `AB4012`) @@ -107,9 +107,9 @@ config file (`AB4000`, `AB4012`) or the skill source. | `AB4002` | error | Skill frontmatter `name` fails the pinned schema: missing, not a string, empty, longer than 64 characters, or not lowercase kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`): `Skill frontmatter name .` | Declare a kebab-case `name` of at most 64 characters. | | `AB4003` | error | Skill frontmatter `description` fails the pinned schema: missing, not a string, blank, or longer than 1024 characters. | Declare a nonblank `description` of at most 1024 characters. | | `AB4004` | error | Skill frontmatter `name` differs from the skill directory's name: `Skill name "" must match directory "".` | Rename the directory or the `name` so they agree. | -| `AB4005` | error | The Skill Markdown body links to a relative resource — an inline link or image, a reference-style link through its definition, or a shortcut reference; fenced and inline code are ignored; `#` anchors, absolute paths, and URLs with a scheme are not resources — that is not a file the skill directory ships (project ignore rules apply): `Skill references missing resource "".` | Add the file beside `SKILL.md`, or fix the link. | +| `AB4005` | error | The Skill Markdown body links to a relative resource, an inline link or image, a reference-style link through its definition, or a shortcut reference; fenced and inline code are ignored; `#` anchors, absolute paths, and URLs with a scheme are not resources, that is not a file the skill directory ships (project ignore rules apply): `Skill references missing resource "".` | Add the file beside `SKILL.md`, or fix the link. | | `AB4006` | error | Two discovered skills declare the same frontmatter `name`: `Skill name "" duplicates .` | Rename one skill. | -| `AB4007` | error | Another portable frontmatter field fails the pinned schema: `allowed-tools` or `license` not a string, `compatibility` not a string of 1–500 characters, or `metadata` not an object whose values are all strings: `Skill frontmatter .` | Fix the field's value. | +| `AB4007` | error | Another portable frontmatter field fails the pinned schema: `allowed-tools` or `license` not a string, `compatibility` not a string of 1 to 500 characters, or `metadata` not an object whose values are all strings: `Skill frontmatter .` | Fix the field's value. | | `AB4012` | error | `plugin.logo` is declared but is not a nonempty string, resolves outside the project root (or to the root itself), or does not name an existing file. | Set `plugin.logo` to an existing file inside the project root, or omit the field. | ## Normalized model invariants (`AB4100`–`AB4102`) @@ -123,9 +123,9 @@ backstop. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB4100` | error | A selected target — `targets` in config, or the CLI's `--target` selection — is not a registered adapter: `Unknown target "".` The built-in registry publishes `amp`, `claude`, `codex`, `cursor`, and `portable`; `plugin` is one of the unknown names — it used to name a merged multi-host output and now selects nothing, because every build already emits the composite root (see the composite plugin root section below). Reported with `target`. | Select host projections (`amp`, `claude`, `codex`, `cursor`, `portable`); omit `targets` for the default `portable` projection. | +| `AB4100` | error | A selected target, `targets` in config, or the CLI's `--target` selection, is not a registered adapter: `Unknown target "".` The built-in registry publishes `amp`, `claude`, `codex`, `cursor`, and `portable`; `plugin` is one of the unknown names. It used to name a merged multi-host output and now selects nothing, because every build already emits the composite root (see the composite plugin root section below). Reported with `target`. | Select host projections (`amp`, `claude`, `codex`, `cursor`, `portable`); omit `targets` for the default `portable` projection. | | `AB4101` | error | Two normalized components share one `id` (for example two skills normalizing to `skill:`): `Normalized component ID "" is duplicated.` | Rename or remove one of the two components. | -| `AB4102` | error | Two inputs of one target produce the same artifact path — a skill's `SKILL.md`, sidecar, or resource under `skills//`, an `assets/` file, `commands/.md`, `rules/.mdc`, a payload file, or a Claude `bin/`, `output-styles/`, or `workflows/` file: `Multiple inputs produce ""; first source is .` Reported with `generatedPath`, `sourcePath`, and `target`. | Rename or remove one of the inputs. | +| `AB4102` | error | Two inputs of one target produce the same artifact path, a skill's `SKILL.md`, sidecar, or resource under `skills//`, an `assets/` file, `commands/.md`, `rules/.mdc`, a payload file, or a Claude `bin/`, `output-styles/`, or `workflows/` file: `Multiple inputs produce ""; first source is .` Reported with `generatedPath`, `sourcePath`, and `target`. | Rename or remove one of the inputs. | ## Hook configuration and native hook sources (`AB4200`–`AB4212`) @@ -211,7 +211,7 @@ judged by `AB4340` (see below) plus the shared local-entry field rules | `AB4336` | error | A normalized App selects a target that is not registered. | | `AB4337` | error | A normalized App selects a target its owning server does not ship to, or its server is missing from the model. | | `AB4338` | error | `_meta` is an object but not plain JSON data: a non-finite number, `undefined`, a function, symbol, or bigint, a class instance or other non-plain object, an accessor property, a symbol key, an array with holes or extra properties, or a cycle. | -| `AB4339` | error | A normalized server's `transport` is not `stdio` or `streamable-http` — a legacy `sse`, another string, or a value that could not be read: `MCP server "" uses unsupported transport "".` Judged on the model and again when each built-in host adapter plans its artifact; the source rules above (`AB4308`, `AB4314`, `AB4317`) fix each declaration form's transport first. | +| `AB4339` | error | A normalized server's `transport` is not `stdio` or `streamable-http`, a legacy `sse`, another string, or a value that could not be read: `MCP server "" uses unsupported transport "".` Judged on the model and again when each built-in host adapter plans its artifact; the source rules above (`AB4308`, `AB4314`, `AB4317`) fix each declaration form's transport first. | ## Script configuration (`AB4400`–`AB4408`) @@ -245,7 +245,7 @@ config file. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB4500` | error | A registered extension value contains something that is not strict finite JSON: a non-finite number (`NaN`, `Infinity`), `undefined`, a function, symbol, or bigint, a cyclic reference, an object whose prototype is neither `Object.prototype` nor `null` (a class instance, `Map`, `Date`), a symbol key, an accessor or non-enumerable property, or an array with holes or extra properties: `A registered config extension must contain strict finite JSON data.` | Declare only JSON literals — objects, arrays, strings, finite numbers, booleans, `null` — under the host keys. | +| `AB4500` | error | A registered extension value contains something that is not strict finite JSON: a non-finite number (`NaN`, `Infinity`), `undefined`, a function, symbol, or bigint, a cyclic reference, an object whose prototype is neither `Object.prototype` nor `null` (a class instance, `Map`, `Date`), a symbol key, an accessor or non-enumerable property, or an array with holes or extra properties: `A registered config extension must contain strict finite JSON data.` | Declare only JSON literals, objects, arrays, strings, finite numbers, booleans, `null`, under the host keys. | ## Assets and the generated-runtime floor (`AB4600`–`AB4602`) @@ -299,7 +299,7 @@ failures `AB4770`, and every validator code keeps its own number. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB5000` | error | Three emitters. **`agent-bundle` CLI**: a command threw something other than a `DiagnosticError` — a `TypeError` from option handling (`Use either --input or --input-file, not both.`), an I/O failure, a bug — so `runCli` writes the error's message as one `AB5000` diagnostic (a JSON diagnostics array on stderr, without `sourcePath`) and exits `1`; Commander usage errors exit `2` without a diagnostic. The same line is written when a foreground session (`dev`, `serve-app`) fails to close on SIGINT/SIGTERM. **`agent-bundle serve-app`**: the bound MCP server exited on its own, so the App host closed: `The MCP server behind exited; the MCP App host closed.`, exit code `1`. **`agent-bundle build`**: a target adapter's plan attributed a hook entry to a target other than itself (`Target adapter "" planned hook "" for target "", expected "".`), with `target` — an adapter contract violation, never a config mistake. | Read the message: it is the underlying error. Fix the named cause and rerun; for the build case, fix the adapter. | +| `AB5000` | error | Three emitters. **`agent-bundle` CLI**: a command threw something other than a `DiagnosticError`, a `TypeError` from option handling (`Use either --input or --input-file, not both.`), an I/O failure, a bug, so `runCli` writes the error's message as one `AB5000` diagnostic (a JSON diagnostics array on stderr, without `sourcePath`) and exits `1`; Commander usage errors exit `2` without a diagnostic. The same line is written when a foreground session (`dev`, `serve-app`) fails to close on SIGINT/SIGTERM. **`agent-bundle serve-app`**: the bound MCP server exited on its own, so the App host closed: `The MCP server behind exited; the MCP App host closed.`, exit code `1`. **`agent-bundle build`**: a target adapter's plan attributed a hook entry to a target other than itself (`Target adapter "" planned hook "" for target "", expected "".`), with `target`, an adapter contract violation, never a config mistake. | Read the message: it is the underlying error. Fix the named cause and rerun; for the build case, fix the adapter. | ## Claude Code host validation (`AB6019`–`AB6022`, `AB7311`, `AB7325`) @@ -357,7 +357,7 @@ inventory rows' `errors` already carry the same verdicts. Without `claude` on | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | | `AB6026` | info | Every Cursor host-validation report states that Cursor publishes no plugin-validate devtools verb and names the vendored schema pin used for local validation. | Review the pinned Cursor schema provenance before changing the local validator contract. | -| `AB6027` | error | A required generated Cursor document is missing or a present plugin, marketplace, MCP, or hooks document is unreadable, invalid JSON, or rejected by its pinned schema. The hooks document is the one `.cursor-plugin/plugin.json` `hooks` names — a plugin-root-relative file (`.cursor-plugin/hooks.json` in the composite root, reported under that path) or an inline object (`.cursor-plugin/plugin.json#/hooks`) — falling back to `hooks/hooks.json` folder discovery only when the field is absent; a declared file that is missing or resolves outside the plugin root is an error, and any other `hooks/hooks.json` beside a named document is not read. | Repair the generated Cursor JSON document so it satisfies the vendored pinned schema, then rebuild. | +| `AB6027` | error | A required generated Cursor document is missing or a present plugin, marketplace, MCP, or hooks document is unreadable, invalid JSON, or rejected by its pinned schema. The hooks document is the one `.cursor-plugin/plugin.json` `hooks` names, a plugin-root-relative file (`.cursor-plugin/hooks.json` in the composite root, reported under that path) or an inline object (`.cursor-plugin/plugin.json#/hooks`), falling back to `hooks/hooks.json` folder discovery only when the field is absent; a declared file that is missing or resolves outside the plugin root is an error, and any other `hooks/hooks.json` beside a named document is not read. | Repair the generated Cursor JSON document so it satisfies the vendored pinned schema, then rebuild. | | `AB6028` | error | Generated bytes violate pinned Cursor loader evidence: manifest-candidate precedence selects a fallback manifest, a symlink resolves outside the bundle, or `CURSOR_PLUGIN_ROOT` appears outside loader-substituted fields. | Repair the generated Cursor layout, token locations, or symlinks to match the pinned loader evidence, then rebuild. | | `AB6029` | info / warning | The Cursor Agent version probe is unavailable (`ENOENT`, info) or cannot complete successfully (warning). Local pinned-schema validation still runs. | Install Cursor Agent or repair `cursor-agent --version` when local CLI version evidence is required, then rerun artifact validation. | @@ -440,8 +440,8 @@ Validation happens at three moments, all fail-closed: | `AB7011` | A copied artifact file in the npm root no longer matches its manifest SHA-256. Rebuild and do not modify the generated npm root. | | `AB7012` | A bin in the generated npm-root or published `package.json` points into `src/` or names a file absent from that package's packed file set. Point each bin at a file included by that package; routed CLIs use manifest-declared `bin/.mjs` and authored bins use generated `bin/*.js` files. | | `AB7013` | `package.json`, normalized plugin metadata, a host manifest, or artifact provenance reports a different release version. Make every release identity agree. | -| `AB7014` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` field names packages a consumer never needs installed: `package.json names packages a consumer never needs installed: no packed declaration file references them, no consumer-side install script names or runs them, and no prebuilt payload declares them in runtimeDependencies: "a", "b".` (one diagnostic per field; the three evidence sources follow this table). Peers `peerDependenciesMeta` marks optional are never installed and are not inspected here (their specifier is still checked by `AB7015`), and a name under both `dependencies` and `optionalDependencies` is judged by its optional entry, which npm lets override. A package the compiler bundled is not used; when the build inlined any of the field's names, the diagnostic adds `The build inlined "a" into dist/bin/x.js and dist/index.js; every consumer installs them for nothing.`, otherwise `Nothing packed reaches them at runtime; every consumer installs them for nothing.` Recovery is `Move build-only packages to devDependencies; compiled bundles inline their imports (AB6005). Keep a runtime dependency only for what a packed declaration file references, a consumer install script names or runs, or a prebuilt payload declares in runtimeDependencies (definePrebuilt).` For `peerDependencies` the diagnostic is a warning: a required peer may be a deliberate compatibility contract with the host that loads the package, though npm 7+ still installs it for every consumer — keep it, mark it optional in `peerDependenciesMeta`, or move a build-only package to `devDependencies`. | -| `AB7015` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` entry that a consumer's npm cannot resolve through a registry. Each entry — name and specifier together, the value exactly as written (a leading space makes `" npm:bar@1"` an invalid dist-tag, not an alias) — is read with `npm-package-arg`, the parser npm, Arborist, and pacote share, so the verdict is npm's own rather than an imitation of its grammar: **registry** (a version, range, or dist-tag, or an `npm:` alias of one — the only kind a published package can rely on), **fetched** (parseable, but a `git`/`github:`/`gitlab:`/`bitbucket:`/`gist:` source or `owner/repo` shorthand, an `http(s):` tarball, or a `file:`/relative/bare path or tarball filename — npm 12 refuses git and remote fetches by default (`allow-git=none`, `allow-remote=none`) and a path never exists on the consumer's disk), or **unparseable** (npm rejects the manifest before fetching anything: `EINVALIDPACKAGENAME` for a name such as `bad name`, `.hidden`, or `node_modules`; `EUNSUPPORTEDPROTOCOL` for `link:`, `portal:`, `jsr:`, a `git+` transport npm lacks, or a typo; `EINVALIDTAGNAME` for a selector that is neither a range nor a URL-safe dist-tag, such as `"not a valid spec"`; an `npm:` alias without a name or with a non-registry target, since aliases only work for registry dependencies; or an invalid URL such as `http:%zz`). A fetched specifier is reported on installed entries only; an unparseable one is reported on every entry, even an optional peer npm would never install, because the manifest read itself fails. A peer that `dependencies` or `optionalDependencies` also names is judged by that concrete entry alone: npm resolves the concrete declaration and never reads the duplicate peer's selector. For an `optionalDependencies` entry that is fetched, the diagnostic is a warning, not an error (`agent-bundle prepack` prints it and exits 0): npm continues an install without such a dependency, but every consumer still tries and fails to fetch it. It stays an error when the entry is unparseable, or when a consumer-side install script needs the skipped package — runs one of its `bin` commands in command position (`setup-tool --init`, `npx setup-tool`, `cross-env CI=1 setup-tool`, `./node_modules/.bin/setup-tool`; a mention elsewhere, `echo setup-tool`, proves nothing), runs one of its files (`node node_modules/setup-tool/install.js`), or preloads it (`node -r setup-tool/register install.js`; `-r`/`--require`, `--import`, `--loader`/`--experimental-loader`, with a space or `=` before the module — read as Node does, `node [options] script [arguments]`: options end at the first positional or a `--`, valued options such as `--conditions x` or `--env-file x` taking their word with them, so `node install.js --require x` passes `--require x` to `install.js` and preloads nothing; a `NODE_OPTIONS` assignment on the same command — `NODE_OPTIONS=--require=setup-tool/register node install.js`, `cross-env NODE_OPTIONS="-r setup-tool/register" node .` — supplies options Node applies before the command line's, while one `export`ed by an earlier command is not read; a relative preload names no package): npm continues past the failed fetch, then the script fails on the missing command or module. The scripts read are `preinstall`, `install`, and `postinstall` and every script they reach through `npm run ` (also `pnpm`/`yarn`/`bun run` and npm's `run-script`/`rum`/`urn` aliases; the script is the first positional after `run`, options before or after `run` skipped with their values — `npm --prefix . run setup`, `npm run -w pkg setup`, `pnpm --filter pkg run setup`, `npm run -- setup` — and every later word an argument of that script, so `npm run setup -- dormant` and `npm run setup dormant` run `setup` alone; shell quotes and backslash escapes resolved (`npm run "setup"`) and `&&`/`||`/`;`/`|`/`&`/newline split off; the script is visited with its `pre`/`post` hooks) or through npm's direct script commands (`npm test`, `t`, `tst`, `start`, `stop`, `restart`, each running the script of that name with its hooks; `npm test foo` runs `test` alone; `npm restart` without a `restart` script runs `stop` then `start`, each with its hooks, inside `prerestart`/`postrestart`). Each command of a script is read on its own: after a shell operator (`&&`, `;`, and the rest) or a newline — the second line of a script, and each lifecycle script after the first, starts a new command — and Node's options belong to `node` alone (`rm -r dist` preloads nothing). The inline program of a `node -e` command and the JavaScript files a script runs are not read: what they load is not evidence here. Depend on a published registry version, or bundle the package and declare it under `devDependencies`. Entries the tarball itself carries are never reported, since a consumer does not fetch them: `bundleDependencies` (by name or `true`; never a peer, which npm cannot bundle; only when the pack inventory contains `node_modules//package.json`, since npm silently packs nothing for a bundled name absent from `node_modules`), and a `file:` or bare path inside the package (`file:vendor/foo`, `file:vendor/foo.tgz`) whose packed source npm can install from — a directory whose packed `package.json` parses to an object, or a packed tarball (gzipped or plain tar, ustar headers with valid checksums and payloads inside the archive) whose `/package.json` entry parses to an object — since npm installs it from the consumer's own copy. A path that escapes the package (`file:../sibling`), whose source is not packed, or whose packed source is not installable (a `.tgz` that is not an archive, or is malformed or truncated, fails the consumer's install with `TAR_BAD_ARCHIVE`; a manifest that does not parse, on disk or inside the archive, fails it with `EJSONPARSE`) is reported. The canonical root is validated for `npm publish ./dist`, so `workspace:` and `catalog:` are always reported: npm publishes them verbatim and consumers fail with `EUNSUPPORTEDPROTOCOL`. | +| `AB7014` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` field names packages a consumer never needs installed: `package.json names packages a consumer never needs installed: no packed declaration file references them, no consumer-side install script names or runs them, and no prebuilt payload declares them in runtimeDependencies: "a", "b".` (one diagnostic per field; the three evidence sources follow this table). Peers `peerDependenciesMeta` marks optional are never installed and are not inspected here (their specifier is still checked by `AB7015`), and a name under both `dependencies` and `optionalDependencies` is judged by its optional entry, which npm lets override. A package the compiler bundled is not used; when the build inlined any of the field's names, the diagnostic adds `The build inlined "a" into dist/bin/x.js and dist/index.js; every consumer installs them for nothing.`, otherwise `Nothing packed reaches them at runtime; every consumer installs them for nothing.` Recovery is `Move build-only packages to devDependencies; compiled bundles inline their imports (AB6005). Keep a runtime dependency only for what a packed declaration file references, a consumer install script names or runs, or a prebuilt payload declares in runtimeDependencies (definePrebuilt).` For `peerDependencies` the diagnostic is a warning: a required peer may be a deliberate compatibility contract with the host that loads the package, though npm 7+ still installs it for every consumer, keep it, mark it optional in `peerDependenciesMeta`, or move a build-only package to `devDependencies`. | +| `AB7015` | A `package.json` `dependencies`, `optionalDependencies`, or `peerDependencies` entry that a consumer's npm cannot resolve through a registry. Each entry, name and specifier together, the value exactly as written (a leading space makes `" npm:bar@1"` an invalid dist-tag, not an alias), is read with `npm-package-arg`, the parser npm, Arborist, and pacote share, so the verdict is npm's own rather than an imitation of its grammar: **registry** (a version, range, or dist-tag, or an `npm:` alias of one, the only kind a published package can rely on), **fetched** (parseable, but a `git`/`github:`/`gitlab:`/`bitbucket:`/`gist:` source or `owner/repo` shorthand, an `http(s):` tarball, or a `file:`/relative/bare path or tarball filename, npm 12 refuses git and remote fetches by default (`allow-git=none`, `allow-remote=none`) and a path never exists on the consumer's disk), or **unparseable** (npm rejects the manifest before fetching anything: `EINVALIDPACKAGENAME` for a name such as `bad name`, `.hidden`, or `node_modules`; `EUNSUPPORTEDPROTOCOL` for `link:`, `portal:`, `jsr:`, a `git+` transport npm lacks, or a typo; `EINVALIDTAGNAME` for a selector that is neither a range nor a URL-safe dist-tag, such as `"not a valid spec"`; an `npm:` alias without a name or with a non-registry target, since aliases only work for registry dependencies; or an invalid URL such as `http:%zz`). A fetched specifier is reported on installed entries only; an unparseable one is reported on every entry, even an optional peer npm would never install, because the manifest read itself fails. A peer that `dependencies` or `optionalDependencies` also names is judged by that concrete entry alone: npm resolves the concrete declaration and never reads the duplicate peer's selector. For an `optionalDependencies` entry that is fetched, the diagnostic is a warning, not an error (`agent-bundle prepack` prints it and exits 0): npm continues an install without such a dependency, but every consumer still tries and fails to fetch it. It stays an error when the entry is unparseable, or when a consumer-side install script needs the skipped package, runs one of its `bin` commands in command position (`setup-tool --init`, `npx setup-tool`, `cross-env CI=1 setup-tool`, `./node_modules/.bin/setup-tool`; a mention elsewhere, `echo setup-tool`, proves nothing), runs one of its files (`node node_modules/setup-tool/install.js`), or preloads it (`node -r setup-tool/register install.js`; `-r`/`--require`, `--import`, `--loader`/`--experimental-loader`, with a space or `=` before the module, read as Node does, `node [options] script [arguments]`: options end at the first positional or a `--`, valued options such as `--conditions x` or `--env-file x` taking their word with them, so `node install.js --require x` passes `--require x` to `install.js` and preloads nothing; a `NODE_OPTIONS` assignment on the same command, `NODE_OPTIONS=--require=setup-tool/register node install.js`, `cross-env NODE_OPTIONS="-r setup-tool/register" node .`, supplies options Node applies before the command line's, while one `export`ed by an earlier command is not read; a relative preload names no package): npm continues past the failed fetch, then the script fails on the missing command or module. The scripts read are `preinstall`, `install`, and `postinstall` and every script they reach through `npm run ` (also `pnpm`/`yarn`/`bun run` and npm's `run-script`/`rum`/`urn` aliases; the script is the first positional after `run`, options before or after `run` skipped with their values, `npm --prefix . run setup`, `npm run -w pkg setup`, `pnpm --filter pkg run setup`, `npm run -- setup`, and every later word an argument of that script, so `npm run setup -- dormant` and `npm run setup dormant` run `setup` alone; shell quotes and backslash escapes resolved (`npm run "setup"`) and `&&`/`||`/`;`/`|`/`&`/newline split off; the script is visited with its `pre`/`post` hooks) or through npm's direct script commands (`npm test`, `t`, `tst`, `start`, `stop`, `restart`, each running the script of that name with its hooks; `npm test foo` runs `test` alone; `npm restart` without a `restart` script runs `stop` then `start`, each with its hooks, inside `prerestart`/`postrestart`). Each command of a script is read on its own: after a shell operator (`&&`, `;`, and the rest) or a newline, the second line of a script, and each lifecycle script after the first, starts a new command, and Node's options belong to `node` alone (`rm -r dist` preloads nothing). The inline program of a `node -e` command and the JavaScript files a script runs are not read: what they load is not evidence here. Depend on a published registry version, or bundle the package and declare it under `devDependencies`. Entries the tarball itself carries are never reported, since a consumer does not fetch them: `bundleDependencies` (by name or `true`; never a peer, which npm cannot bundle; only when the pack inventory contains `node_modules//package.json`, since npm silently packs nothing for a bundled name absent from `node_modules`), and a `file:` or bare path inside the package (`file:vendor/foo`, `file:vendor/foo.tgz`) whose packed source npm can install from, a directory whose packed `package.json` parses to an object, or a packed tarball (gzipped or plain tar, ustar headers with valid checksums and payloads inside the archive) whose `/package.json` entry parses to an object, since npm installs it from the consumer's own copy. A path that escapes the package (`file:../sibling`), whose source is not packed, or whose packed source is not installable (a `.tgz` that is not an archive, or is malformed or truncated, fails the consumer's install with `TAR_BAD_ARCHIVE`; a manifest that does not parse, on disk or inside the archive, fails it with `EJSONPARSE`) is reported. The canonical root is validated for `npm publish ./dist`, so `workspace:` and `catalog:` are always reported: npm publishes them verbatim and consumers fail with `EUNSUPPORTEDPROTOCOL`. | `AB7014` counts exactly three sources of evidence that a consumer needs a declared package: @@ -490,9 +490,9 @@ compiler's message, plus a `sourcePath`: has or is using name 'CliCommandDefinition' from external module "…" but cannot be named. ``` -When no diagnostic can be recovered — the project has no resolvable +When no diagnostic can be recovered, the project has no resolvable `typescript`, or the replay passes because the failure was elsewhere in -declaration generation — the failure still reports as a single `AB4716` +declaration generation, the failure still reports as a single `AB4716` carrying the bundler's own message. Declaration failures never fall through to the `AB5000` catch-all, whose dev-lock meaning previously misdirected triage. @@ -506,7 +506,7 @@ directory. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB4716` | error (build) | Declaration emit for a `lib` entry with `dts` enabled failed. One diagnostic per recovered TypeScript diagnostic — `Declaration generation for lib entry "" failed: (,): TS: ` — or a single one carrying the bundler's own message when none could be recovered (no resolvable `typescript`, or a replay that passes). `sourcePath` is the file TypeScript located, when it did. | Fix the reported TypeScript declaration errors and rebuild; replay them with `tsc --declaration --emitDeclarationOnly` over the lib entry source directory, since `tsc --noEmit` never shows them. | +| `AB4716` | error (build) | Declaration emit for a `lib` entry with `dts` enabled failed. One diagnostic per recovered TypeScript diagnostic, `Declaration generation for lib entry "" failed: (,): TS: `, or a single one carrying the bundler's own message when none could be recovered (no resolvable `typescript`, or a replay that passes). `sourcePath` is the file TypeScript located, when it did. | Fix the reported TypeScript declaration errors and rebuild; replay them with `tsc --declaration --emitDeclarationOnly` over the lib entry source directory, since `tsc --noEmit` never shows them. | ## MCP App view compilation (`AB4770`–`AB4772`) @@ -516,8 +516,8 @@ reads the Rspack stats of every App environment instead and reports **one `AB4770` error per Rspack error**, each carrying the failing module as a project-relative path (forward slashes; absolute when the module lives outside the project root), the `line:column` the bundler reported, and the bundler's -message — ANSI colours, the miette frame glyphs, and code-frame lines -stripped, the remaining lines joined into one — plus a `sourcePath` naming the +message, ANSI colours, the miette frame glyphs, and code-frame lines +stripped, the remaining lines joined into one, plus a `sourcePath` naming the failing module: ```text @@ -536,8 +536,8 @@ source. A compile that fails without a single stats error still reports one cut at 20, and the last diagnostic ends with `… and N more errors (run the compile with logLevel error via tools.rsbuild for the full list)`. App compile failures never fall through to the `AB5000` catch-all, and `agent-bundle dev` -shows the same `AB4770` rows in the Workbench Overview's Diagnostics table — -the Source column is the failing file — instead of +shows the same `AB4770` rows in the Workbench Overview's Diagnostics table, +the Source column is the failing file, instead of `AB7100 "Unable to compile the build: Rspack build failed."`. Rspack warnings that are not on the framework's ignore list report as @@ -545,7 +545,7 @@ Rspack warnings that are not on the framework's ignore list report as compiling` in place of `failed to compile`. They never fail the build and are returned beside the compiled Apps (`build.diagnostics` in `agent-bundle build --json`). The ignore list is the documented constant in -`packages/agent-bundle/src/build/mcp-app-diagnostics.ts` — one comment per +`packages/agent-bundle/src/build/mcp-app-diagnostics.ts`, one comment per entry citing the warning text it drops and why it is noise; it may be empty. Every App is measured after it is emitted: the UTF-8 bytes of the @@ -553,9 +553,9 @@ self-contained HTML and their gzip size, what a compressing transport would carry. `AB4772` is the size advisory, one **warning** per App. A view that reaches its host through `agent-bundle/app` carries the framework's client and nothing else from the protocol stack; one that imports -`@modelcontextprotocol/ext-apps` instead starts at about 437 kB (104 kB gzip) -— `zod` v3 and v4, `@modelcontextprotocol/sdk`, `zod-to-json-schema`, and -`ext-apps` itself — so the advisory bound of 1 MiB (1,048,576 bytes) sits at +`@modelcontextprotocol/ext-apps` instead starts at about 437 kB (104 kB gzip), +`zod` v3 and v4, `@modelcontextprotocol/sdk`, `zod-to-json-schema`, and +`ext-apps` itself, so the advisory bound of 1 MiB (1,048,576 bytes) sits at roughly 2.4× that floor and at half the 2 MiB (2,097,152 bytes) bound above which the Workbench and `serve-app` hosts refuse the resource and the Rstest browser harness refuses to mount it. The advisory fires when a production @@ -568,7 +568,7 @@ sizes 1024-based to one decimal, a trailing `.0` dropped (`427.1 KiB`, them. `agent-bundle dev` compiles views unminified so the Workbench preview is -readable — about 2.7× the production bytes. A view whose readable document +readable, about 2.7× the production bytes. A view whose readable document would exceed the 2 MiB host bound is recompiled with the production profile so the preview still renders it, and one `AB4772` reports the substitution instead: `MCP App "" readable development output compiled to , @@ -584,9 +584,9 @@ readable output. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB4770` | error (build) | One Rspack error while compiling an App view — a syntax error, an unresolved import, a `tsconfig.json` whose `extends` target is missing, or any other module failure. `MCP App "" failed to compile: ::: `, without the location prefix when Rspack attributes the error to no module; `sourcePath` is the failing module, else the App's entry. | Fix the reported error in the named file and rebuild; run `agent-bundle build` for the full message. | +| `AB4770` | error (build) | One Rspack error while compiling an App view, a syntax error, an unresolved import, a `tsconfig.json` whose `extends` target is missing, or any other module failure. `MCP App "" failed to compile: ::: `, without the location prefix when Rspack attributes the error to no module; `sourcePath` is the failing module, else the App's entry. | Fix the reported error in the named file and rebuild; run `agent-bundle build` for the full message. | | `AB4771` | warning | One Rspack warning while compiling an App view that the framework's ignore list does not cover; `MCP App "" produced a warning while compiling: ::: `. | Address the warning in the named file; a warning that is bundler noise inside the framework's own dependency graph belongs on the documented ignore list. | -| `AB4772` | warning | The emitted App HTML is 1 MiB or larger in a production build, or larger than 2 MiB in any build; `MCP App "" compiled to ( gzip), above the … bound; largest modules: …`. In `agent-bundle dev`, a view whose readable output would exceed 2 MiB was recompiled with the production profile for the preview and that production build fits: `MCP App "" readable development output compiled to , above the 2 MiB bound …; the preview renders the production build (…) instead; largest modules: …` — the only size advisory that view receives; a production build that is itself over 2 MiB gets the ordinary over-bound message instead. | Trim the largest modules the message names — usually a dependency imported whole; a view over 2 MiB does not render in the Workbench or `serve-app` and must shrink before it ships. The development substitution costs only the readable source in the preview. | +| `AB4772` | warning | The emitted App HTML is 1 MiB or larger in a production build, or larger than 2 MiB in any build; `MCP App "" compiled to ( gzip), above the … bound; largest modules: …`. In `agent-bundle dev`, a view whose readable output would exceed 2 MiB was recompiled with the production profile for the preview and that production build fits: `MCP App "" readable development output compiled to , above the 2 MiB bound …; the preview renders the production build (…) instead; largest modules: …`, the only size advisory that view receives; a production build that is itself over 2 MiB gets the ordinary over-bound message instead. | Trim the largest modules the message names, usually a dependency imported whole; a view over 2 MiB does not render in the Workbench or `serve-app` and must shrink before it ships. The development substitution costs only the readable source in the preview. | ## Release identity (`AB4001`, `AB4008`–`AB4011`, `AB4013`) @@ -599,8 +599,8 @@ package name. `plugin.version` is **deprecated and optional**. New projects declare the release version only in `package.json`; removal of the compatibility field follows the normal breaking-change policy rather than a fixed window. When it -is omitted, the version every surface reports — manifests, host projections, -dev status, and the `agent-bundle/meta` constant compiled into plugin code — +is omitted, the version every surface reports, manifests, host projections, +dev status, and the `agent-bundle/meta` constant compiled into plugin code, is the `package.json` version. When it is declared, the declared value still wins so a legacy config never changes meaning mid-migration, and a disagreement reports the `AB4008` **warning**. Declaring it as anything but a @@ -619,10 +619,10 @@ development-only fallback can never produce a release artifact, so | `AB4008` | warning | A declared `plugin.version` differs from the `package.json` version. Align the two, or drop `plugin.version`. | | `AB4009` | warning | `package.json` `name` is not a valid npm package name; the `packageName` axis is withheld. | | `AB4010` | warning | `package.json` `version` is not a valid semantic version; the `packageVersion` axis is withheld. | -| `AB4011` | warning | `package.json` is unusable — unparsable, not a JSON object, or symlinked outside the project root. | +| `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. | -| `AB4014` | error | A `plugin.metadata` field is not the shape the shared descriptive layer accepts, or the block declares a field beyond `author`, `homepage`, `keywords`, `license`, and `repository`. The config declared it, so it is an error rather than a withheld value — a blank string or empty array included, where `null` is how a field is opted out. | -| `AB4015` | warning | A `package.json` descriptive field cannot be shared with any host manifest — a `homepage`, `repository`, or `author.url` the pinned host schemas' `uri` format refuses, an `author.email` their `email` format refuses, or a `repository` in a form this compiler will not convert (`owner/repo` and `github:` shorthands, `git@`/`git://`/`git+ssh`/`git+http` URLs; only `http(s)` and the `git+https://…` URL npm writes, with or without a trailing `.git`, are read). An `author` with any malformed part is withheld whole. The field is withheld rather than guessed at; declare `plugin.metadata.` to share an explicit value. A field the config already overrides is not reported. | +| `AB4014` | error | A `plugin.metadata` field is not the shape the shared descriptive layer accepts, or the block declares a field beyond `author`, `homepage`, `keywords`, `license`, and `repository`. The config declared it, so it is an error rather than a withheld value, a blank string or empty array included, where `null` is how a field is opted out. | +| `AB4015` | warning | A `package.json` descriptive field cannot be shared with any host manifest, a `homepage`, `repository`, or `author.url` the pinned host schemas' `uri` format refuses, an `author.email` their `email` format refuses, or a `repository` in a form this compiler will not convert (`owner/repo` and `github:` shorthands, `git@`/`git://`/`git+ssh`/`git+http` URLs; only `http(s)` and the `git+https://…` URL npm writes, with or without a trailing `.git`, are read). An `author` with any malformed part is withheld whole. The field is withheld rather than guessed at; declare `plugin.metadata.` to share an explicit value. A field the config already overrides is not reported. | ## Migration nudges and convention claims (`AB4730`–`AB4738`) @@ -646,7 +646,7 @@ 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 +### `AB4730` self-connecting stdio MCP entry A local MCP server entry module (explicit `entry:` or the conventional `src/mcp/.ts`) has no default export, so the build bundles it @@ -656,9 +656,9 @@ 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. +the self-connecting entry, its behavior is preserved exactly. -### `AB4731` — `src/cli.ts` shadowed by explicit `bin` config +### `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. @@ -667,7 +667,7 @@ references it, so the conventional package bin is silently shadowed. 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 +### `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. @@ -675,35 +675,35 @@ 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/.ts` shadowed by explicit server config +### `AB4733` `src/mcp/.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. +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. -### `AB4734` — conventional skill shadowed by explicit `skills` config +### `AB4734` conventional skill shadowed by explicit `skills` config A `src/skills//SKILL.md` (or rendered `SKILL.tsx`/`SKILL.ts`) directory -exists, but the explicit `skills` configuration does not cover it — the +exists, but the explicit `skills` configuration does not cover it, the conventional skill is silently shadowed. When config is silent, every `src/skills//` directory ships by convention and this nudge never fires. Adopt: remove the explicit `skills` configuration so the convention applies, or add the directory to `skills`. Silence: remove the directory. -### `AB4735` — rendered skill source shadowed by hand-authored `SKILL.md` +### `AB4735` rendered skill source shadowed by hand-authored `SKILL.md` A skill directory contains both a hand-authored `SKILL.md` and a rendered -skill source (`SKILL.tsx`/`SKILL.ts`). The authored file wins — an authored -document beats a generated one — so the component module never compiles. +skill source (`SKILL.tsx`/`SKILL.ts`). The authored file wins, an authored +document beats a generated one, so the component module never compiles. Adopt: remove `SKILL.md` so the rendered skill compiles at build. Silence: remove the component module. -### `AB4736` — legacy top-level authored document location +### `AB4736` legacy top-level authored document location A document still matches a removed top-level convention: `skills//SKILL.md` (or rendered `SKILL.tsx`/`SKILL.ts`), @@ -716,15 +716,15 @@ 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 +### `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/.tsx` or `.jsx`) that does not declare both a default component and a named `main`. Inspection checks export presence; the runtime checks callability. Components may be sync or async, and the bundler resolves re-exports. A plain `src/scripts/.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 +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 a 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 @@ -741,7 +741,7 @@ 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 +### `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/.ts`) that exports a `default` but no named `main`. Both @@ -771,7 +771,7 @@ simply not been built yet is a validation **warning** that only | Code | Severity | Trigger | | --- | --- | --- | | `AB4740` | error | The `payload` block, one entry, or its `targets` list is malformed, or a payload selects an unknown target. A `runtimeDependencies` list that is not an array of nonempty strings is also `AB4740`. | -| `AB4741` | error | A payload destination is not a safe directory name, shadows a compiler-owned artifact namespace (`assets`, `hooks`, `mcp`, `mcp-apps`, `scripts`, `skills`, root documents), or names the runtime-owned `state` root in any letter case — the manifest never indexes a file under `state/`, and installers neither copy nor own that root. | +| `AB4741` | error | A payload destination is not a safe directory name, shadows a compiler-owned artifact namespace (`assets`, `hooks`, `mcp`, `mcp-apps`, `scripts`, `skills`, root documents), or names the runtime-owned `state` root in any letter case, the manifest never indexes a file under `state/`, and installers neither copy nor own that root. | | `AB4742` | error | A payload source escapes the project root, is not a directory, or contains another payload's source. | | `AB4743` | warning | A declared payload directory does not exist yet or contains no files. Run the project's own build first. | | `AB4744` | error | A `{ prebuilt: ... }` entry (MCP server or hook handler) does not resolve inside a declared payload, or its payload does not select every target the component needs. | @@ -781,7 +781,7 @@ simply not been built yet is a validation **warning** that only | `AB4748` | error (build) | `agent-bundle build` refuses a prebuilt entry file absent from its payload. | | `AB4749` | error (build) | A payload directory overlaps the artifact `--output` root. | | `AB4750` | info | A payload is older than the newest project source file and may be stale; rerun the project's own build if so. | -| `AB4751` | error | A `runtimeDependencies` entry is not a bare package name as npm reads one (a subpath such as `sharp/lib`, a selector such as `sharp@1`, a path, or a URL scheme), or `package.json` does not declare it as a dependency a consumer installs — `dependencies`, `optionalDependencies`, or a peer not marked optional in `peerDependenciesMeta` (the set `AB7014` judges). The declaration check is skipped when `package.json` is missing (silent) or unparsable or outside the root (`AB4011`). | +| `AB4751` | error | A `runtimeDependencies` entry is not a bare package name as npm reads one (a subpath such as `sharp/lib`, a selector such as `sharp@1`, a path, or a URL scheme), or `package.json` does not declare it as a dependency a consumer installs, `dependencies`, `optionalDependencies`, or a peer not marked optional in `peerDependenciesMeta` (the set `AB7014` judges). The declaration check is skipped when `package.json` is missing (silent) or unparsable or outside the root (`AB4011`). | ## Build-time identity outside the compiler (`AB4760`) @@ -789,8 +789,8 @@ simply not been built yet is a validation **warning** that only the compiler replaces in every compiled surface with the project's exact `{ name, packageName, packageVersion, version }`. The published `dist/meta.js` module behind that specifier therefore never carries an -identity of its own: every binding — `name`, `version`, `packageName`, -`packageVersion`, `meta`, and the default export — throws this diagnostic at +identity of its own: every binding, `name`, `version`, `packageName`, +`packageVersion`, `meta`, and the default export, throws this diagnostic at module evaluation, so a module that reaches it fails on import rather than observing a fabricated identity. The thrown value is an `Error` named `AgentBundleMetaUnavailableError` whose `code`, `recovery`, and structured @@ -808,7 +808,7 @@ aliasing the specifier to `.agent-bundle/test/meta.mjs`, generated from the same compiler pass. When that pass produced no plugin model (the configuration could not be loaded or normalized) there is no identity to stamp, so the aliased module throws the same `AB4760` naming the compiler diagnostics and -the recovery "fix them, then rerun Rstest" — the manifest's placeholder +the recovery "fix them, then rerun Rstest", the manifest's placeholder identity is never served as a real one. Rendered skills (`src/skills//SKILL.tsx`) evaluate during discovery, @@ -822,12 +822,12 @@ skill's `AB3003`. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB4760` | error | A module evaluated the published `agent-bundle/meta` outside a surface Agent Bundle compiles — typically a unit test pool not built from the Rstest preset, or a hand-run script importing plugin source. | Run the test under `agentBundleRstest()` or `agentBundleBrowserRstest()` from `agent-bundle/rstest` (pass `include` to cover a plain unit pool), or compile the surface with `agent-bundle build`. In a custom test runner, alias `agent-bundle/meta` (`resolve.alias`, exact match) to a module with the named exports `{ name, packageName, packageVersion, version, meta }` — `meta` the frozen object of the other four, exported as both the named binding and the default export — computed from the project's `agent-bundle.config.ts` plugin name and `package.json` version; the `.agent-bundle/test/meta.mjs` module `agentBundleRstest()` writes is that module. | +| `AB4760` | error | A module evaluated the published `agent-bundle/meta` outside a surface Agent Bundle compiles, typically a unit test pool not built from the Rstest preset, or a hand-run script importing plugin source. | Run the test under `agentBundleRstest()` or `agentBundleBrowserRstest()` from `agent-bundle/rstest` (pass `include` to cover a plain unit pool), or compile the surface with `agent-bundle build`. In a custom test runner, alias `agent-bundle/meta` (`resolve.alias`, exact match) to a module with the named exports `{ name, packageName, packageVersion, version, meta }`, `meta` the frozen object of the other four, exported as both the named binding and the default export, computed from the project's `agent-bundle.config.ts` plugin name and `package.json` version; the `.agent-bundle/test/meta.mjs` module `agentBundleRstest()` writes is that module. | ## The composite plugin root (`AB4100`, `AB4103`, `AB4105`, `AB4106`) `build` emits **one** composite root at the artifact directory (#555). The -`targets` list selects which host *projections* the root carries — +`targets` list selects which host *projections* the root carries, `amp`, `claude`, `codex`, `cursor`, `portable`. `claude`, `codex`, `cursor`, and `portable` read the artifact directory itself as their plugin root: their manifests sit in `.claude-plugin/`, `.codex-plugin/`, `.cursor-plugin/`, or the @@ -845,8 +845,8 @@ exactly where `build` would refuse. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | | `AB4103` | error | Two selected projections plan the same component path with different bytes, so one root cannot hold both without changing native precedence. The diagnostic names the component location, path, and hosts. Common cases are a Skill whose host extension lowers to different `skills//SKILL.md` bytes, or a command whose Claude and Cursor dialects lower differently. Projections are compared in host-name order and paths in path order, so the same selection reports the same collision however `targets` is written. | Make the component identical for every selected host, or build the conflicting hosts into separate artifacts (one `targets` entry per build). | -| `AB4105` | error | A component scoped to a subset of the selected hosts (a command or rule with frontmatter `targets`) would be discovered by another selected host that scans the same conventional directory (`commands/` for Claude Code and Cursor, `rules/` for Cursor). Inside one root the file cannot be hidden from that host, so the build refuses rather than leaking it. Skills are never host-scoped — every skill ships to every selected host, and a per-host frontmatter extension that changes its bytes is an `AB4103` collision instead. | Extend the component's `targets` to every selected host that discovers its directory, or build those hosts into separate artifacts. | -| `AB4106` | error | The selection mixes an adapter registered on an advanced `TargetRegistry` — any target whose adapter is not one of the shipped `amp`, `claude`, `codex`, `cursor`, `portable` adapters, judged by adapter identity, so a custom adapter registered under one of those names counts as advanced — with one or more other targets. The built-in hosts agree on where the files they cannot share live, which conventional directories each discovers, and one install surface; a third-party adapter has made none of those agreements, so it cannot share a root. Judged on the normalized model, so `validate`, `inspect`, and `build` all report it, on the non-built-in target with its config provenance. A selection of one target never triggers it, whatever the target; unknown names are `AB4100`'s and do not count. | Build that target alone — `targets: ['']` — into its own `--output`, and the remaining targets into another. | +| `AB4105` | error | A component scoped to a subset of the selected hosts (a command or rule with frontmatter `targets`) would be discovered by another selected host that scans the same conventional directory (`commands/` for Claude Code and Cursor, `rules/` for Cursor). Inside one root the file cannot be hidden from that host, so the build refuses rather than leaking it. Skills are never host-scoped, every skill ships to every selected host, and a per-host frontmatter extension that changes its bytes is an `AB4103` collision instead. | Extend the component's `targets` to every selected host that discovers its directory, or build those hosts into separate artifacts. | +| `AB4106` | error | The selection mixes an adapter registered on an advanced `TargetRegistry`, any target whose adapter is not one of the shipped `amp`, `claude`, `codex`, `cursor`, `portable` adapters, judged by adapter identity, so a custom adapter registered under one of those names counts as advanced, with one or more other targets. The built-in hosts agree on where the files they cannot share live, which conventional directories each discovers, and one install surface; a third-party adapter has made none of those agreements, so it cannot share a root. Judged on the normalized model, so `validate`, `inspect`, and `build` all report it, on the non-built-in target with its config provenance. A selection of one target never triggers it, whatever the target; unknown names are `AB4100`'s and do not count. | Build that target alone, `targets: ['']`, into its own `--output`, and the remaining targets into another. | ## Artifact-hosted routed CLI and npm lifecycle paths (`AB4765`–`AB4768`) @@ -856,7 +856,7 @@ publishes a supported `cli` capability, as `bin/.mjs` (plus target hosts it; the two codes cover a target that does not and a host file that claims the same path. The npm root copies the manifest-declared bin unchanged and points `package.json` at it rather than recompiling the graph. -See “The routed CLI shell” in +See "The routed CLI shell" in `docs/entry-conventions.md` for the layout and the sibling-path convention. | Code | Severity | Trigger | @@ -870,7 +870,7 @@ See “The routed CLI shell” in A `mcp.servers.` block for a server the route graph compiles in `generated` mode augments that server (`env`, `args`, `targets`, `apps`, -`transport: 'stdio'`) — see the precedence table in +`transport: 'stdio'`), see the precedence table in [Entry conventions](entry-conventions.md#config-beside-a-route-generated-mcp-server). The local-entry field rules apply to it unchanged (`AB4305`, `AB4308`–`AB4312`, `AB432x`), and it never triggers `AB4304` or `AB4322`: the route modules are @@ -889,7 +889,7 @@ nine message shapes. | Code | Severity | Trigger | | --- | --- | --- | -| `AB4341` | error (shape 9: warning) | The `web` key is invalid. Message shapes: (1) `web.apps[] names /, which no mcp.servers..apps entry declares.` Recovery: `Declare the App under mcp.servers..apps or remove it from web.apps.` — an App `web.apps` names that no `mcp.servers..apps` declares. (2) `web.apps[] allows , which is not an App-initiated consent capability.` Recovery: `Use one of: call-tool, download-file, open-external-link, request-display-mode; browser hardware and clipboard permissions always ask in the host page.` — unknown `allow` capability. (3) `web.open must be "browser" or "never".` — `open` outside that enum. (4) `web.apps[].tool is not a tool this project's route graph declares for .` Recovery: `Name a tool whose _meta.ui.resourceUri is , or omit tool when exactly one such tool exists.` — only for a generated route server whose tools are known statically; a hand-written server factory skips this check and the runtime verifies. (5) `web.apps[] names / twice.` Recovery: `List each App once.` (6) `CLI command "web" is reserved by the web surface (web.apps is configured).` (or `CLI alias "web" of is reserved ...`). Recovery: `Rename the command or remove web.apps.` (`Rename the alias ...`) — a generated-mode `src/cli/**` command whose first path segment is `web`, or a top-level command with a `web` alias, while `web` is configured; the generated shell dispatches `web` before the authored tree, so either would be unreachable. (7) `web.apps is configured, but src/cli.ts owns the "" executable, so the framework-generated web command has nowhere to live.` (or `... the bin config owns ...`). Recovery: `Move that executable's commands under src/cli/** so the framework generates the bin, or remove web.apps.` — `web` never replaces an authored executable; the hand-written bin keeps the plugin name and the web surface is not emitted. (8) `web.apps is configured, but no framework-generated executable carries the web command (bin is false, or the plugin name is not a safe executable name).` Recovery: `Remove bin: false (or choose a safe plugin name), or remove web.apps.` (9) **warning** `The web surface is not hosted by target "": . Its artifact carries no working web command.` where the judgment is `the target publishes no web capability row` or `its web capability is : `. Recovery: `Publish a supported web capability on the adapter, or drop the target.` — the `web` capability row gates the web-only bin's emission the way `cli` gates a routed CLI (`AB4765`); every built-in host publishes it, so this fires for custom adapters only. | +| `AB4341` | error (shape 9: warning) | The `web` key is invalid. Message shapes: (1) `web.apps[] names /, which no mcp.servers..apps entry declares.` Recovery: `Declare the App under mcp.servers..apps or remove it from web.apps.`, an App `web.apps` names that no `mcp.servers..apps` declares. (2) `web.apps[] allows , which is not an App-initiated consent capability.` Recovery: `Use one of: call-tool, download-file, open-external-link, request-display-mode; browser hardware and clipboard permissions always ask in the host page.`, unknown `allow` capability. (3) `web.open must be "browser" or "never".`, `open` outside that enum. (4) `web.apps[].tool is not a tool this project's route graph declares for .` Recovery: `Name a tool whose _meta.ui.resourceUri is , or omit tool when exactly one such tool exists.`, only for a generated route server whose tools are known statically; a hand-written server factory skips this check and the runtime verifies. (5) `web.apps[] names / twice.` Recovery: `List each App once.` (6) `CLI command "web" is reserved by the web surface (web.apps is configured).` (or `CLI alias "web" of is reserved ...`). Recovery: `Rename the command or remove web.apps.` (`Rename the alias ...`), a generated-mode `src/cli/**` command whose first path segment is `web`, or a top-level command with a `web` alias, while `web` is configured; the generated shell dispatches `web` before the authored tree, so either would be unreachable. (7) `web.apps is configured, but src/cli.ts owns the "" executable, so the framework-generated web command has nowhere to live.` (or `... the bin config owns ...`). Recovery: `Move that executable's commands under src/cli/** so the framework generates the bin, or remove web.apps.`, `web` never replaces an authored executable; the hand-written bin keeps the plugin name and the web surface is not emitted. (8) `web.apps is configured, but no framework-generated executable carries the web command (bin is false, or the plugin name is not a safe executable name).` Recovery: `Remove bin: false (or choose a safe plugin name), or remove web.apps.` (9) **warning** `The web surface is not hosted by target "": . Its artifact carries no working web command.` where the judgment is `the target publishes no web capability row` or `its web capability is : `. Recovery: `Publish a supported web capability on the adapter, or drop the target.`, the `web` capability row gates the web-only bin's emission the way `cli` gates a routed CLI (`AB4765`); every built-in host publishes it, so this fires for custom adapters only. | ## Conventional host components: rules and commands (`AB4900`–`AB4908`, `AB4920`–`AB4928`) @@ -902,7 +902,7 @@ documents compile to the Command IR (closed frontmatter: `description`, supports; a document without `targets` is emitted where supported and accounted as `skipped` with the host's judgment elsewhere (see `agent-bundle inspect`), while a document that explicitly names a host without -the surface is a build error — unsupported components fail before artifact +the surface is a build error, unsupported components fail before artifact publication rather than shipping as a broken half. Identity paths are canonicalized so the model digest is root-independent. @@ -944,13 +944,13 @@ above, never per feature. Skills keep their own closed per-host schemas `tools.rsbuild` and `tools.rspack` are validated with the rest of the config source, so a malformed or colliding hatch is an **error** before any bundler -runs — in `validate`, `build`, `inspect`, and `dev` alike. `AB4720`–`AB4723` +runs, in `validate`, `build`, `inspect`, and `dev` alike. `AB4720`–`AB4723` check the shape: `tools` must be an object whose only keys are `rsbuild` (an Rsbuild environment-config object) and `rspack` (an Rspack config object, a mutator function, or an array of both). `AB4724` checks `tools.rsbuild.plugins` against the Rsbuild plugins the -framework registers itself — currently `@rsbuild/plugin-react` +framework registers itself, currently `@rsbuild/plugin-react` (`rsbuild:react`), which every synthesized Rslib entry and every MCP App view carries, whatever the view's entry extension. The hatch merges *beside* the framework profile (`mergeRslibConfig` / `mergeRsbuildConfig` concatenate `plugins` arrays), and @@ -970,7 +970,7 @@ framework-owned plugin twice by accident. | `AB4722` | error | `tools.rsbuild` is not an Rsbuild environment-config object. | Declare an object fragment. | | `AB4723` | error | `tools.rspack` is not an Rspack config object, a mutator function, or an array of both. | Use one of the three Rslib `tools.rspack` forms. | | `AB4724` | error | `tools.rsbuild.plugins` supplies a plugin whose `name` matches a framework-owned registration (`rsbuild:react` from `@rsbuild/plugin-react`). The message names the plugin and its package. | Remove the plugin from `tools.rsbuild.plugins`; agent-bundle registers it in every config it synthesizes. | -| `AB4725` | error | `tools` externalizes a non-built-in (`tools.rsbuild.output.autoExternal` not `false`, or a string/object `externals` entry that names a package — neither a Node built-in, `pnpapi`, nor a relative path — in `tools.rsbuild.output` or an object-form `tools.rspack`). | Remove the externalization; RegExp, function, and relative externals are judged by the compilation's evidence instead (AB6005), where the emitted siblings are known. | +| `AB4725` | error | `tools` externalizes a non-built-in (`tools.rsbuild.output.autoExternal` not `false`, or a string/object `externals` entry that names a package, neither a Node built-in, `pnpapi`, nor a relative path, in `tools.rsbuild.output` or an object-form `tools.rspack`). | Remove the externalization; RegExp, function, and relative externals are judged by the compilation's evidence instead (AB6005), where the emitted siblings are known. | | `AB4726` | error | `tools.rsbuild` contains a deprecated or removed Rsbuild v2 configuration key: `source.alias`, `source.aliasStrategy`, `performance.bundleAnalyze`, `performance.removeMomentLocale`, `performance.profile`, `performance.chunkSplit`, `output.sourceMap.extract.js`, `provider`, `tools.webpack`, `tools.webpackChain`, `dev.setupMiddlewares`, or the proxy keys `context`, `onOpen`, `onClose`, `onError`, `onProxyReq`, and `onProxyRes`. | Apply the replacement named by the diagnostic. The check is limited to these Rsbuild paths and does not reject authored `html.templateParameters` variables or similarly named or otherwise valid `tools.rspack` keys. | ## Route graph, state, layout, and provider conventions (`AB4800`–`AB4845`, `AB4940`–`AB4942`) @@ -985,11 +985,11 @@ it, and never derives a `tool:/.cli` identity from it. Discovery is not a packaging choice, so every collision is a hard **error** and the compiler never silently picks a side. Modules that explicit `scripts`, `hooks`, `bin`, `lib`, or `mcp` configuration references are -claimed by that declaration and never become routes — config always wins. +claimed by that declaration and never become routes, config always wins. `agent-bundle inspect --routes` dumps the compiled graph. -Each route's `config` export is extracted statically — the module is parsed -with the TypeScript compiler, never executed — from a single top-level +Each route's `config` export is extracted statically, the module is parsed +with the TypeScript compiler, never executed, from a single top-level `export const config = ` declaration. The accepted expression grammar is: object literals whose property names are identifiers, string literals, or numeric literals (no computed names, spreads, shorthand @@ -1006,16 +1006,16 @@ in every tool that opens it: - **`appResourceUri('')`** imported from `agent-bundle/routes`. The compiler resolves the reference to the target App route's static `config.resourceUri` while compiling the graph. The App must belong to the - referencing route's own generated server — a generated server registers + referencing route's own generated server, a generated server registers exactly its own Apps, so another server's URI could never be read through it. References are `''`, `'/'`, `'app:/'`, or a module path relative to the referencing file - (`'../apps/dashboard'`, with or without its `.ts`/`.tsx` extension — a + (`'../apps/dashboard'`, with or without its `.ts`/`.tsx` extension, a `.js`/`.jsx` spelling maps onto the TypeScript source, and any other suffix is part of the App name). The argument may be a string literal or a const identifier of the first form. An unknown - reference — another server's App, an App whose own `resourceUri` is not a - static string, or any reference from a non-MCP route — is `AB4826`, and the + reference, another server's App, an App whose own `resourceUri` is not a + static string, or any reference from a non-MCP route, is `AB4826`, and the route compiles with the empty config beside it. Routes of a server that is not generated (`custom`/`command`/`remote`, or an `AB4800` conflict) never ship their config, so their references are left as authored rather than @@ -1025,9 +1025,9 @@ in every tool that opens it: servers read the compiled config, never the module's evaluated `config`, so use the const form when the URI is also needed inside the component. -Anything else — any other identifier, a call, a package import, a relative +Anything else, any other identifier, a call, a package import, a relative import that leaves the project or whose chain does not end in a -string-literal `export const` — is dynamic: the route compiles with an empty +string-literal `export const`, is dynamic: the route compiles with an empty config beside a named `AB4806` error whose recovery names both reference forms. A module without a `config` export compiles silently with an empty config. @@ -1048,9 +1048,9 @@ the same graph. Development writes a sibling temporary file and renames it over the prior complete declaration atomically; invalid source retains the prior last-good file, while a successful route-free, provider-free preparation removes it. Beside `AgentBundleRoutes`, a graph with conventional providers -declares `AgentBundleProviders` (`ProviderKey`, `ProviderValue`) — each +declares `AgentBundleProviders` (`ProviderKey`, `ProviderValue`), each camel-cased key mapped to its factory's awaited return type, in execution -order — and augments `@agent-bundle/runtime`'s `AgentProviderValues` so +order, and augments `@agent-bundle/runtime`'s `AgentProviderValues` so `await (await agent()).provider(key)` observes that type in projects whose TypeScript program includes the file. Provider-free graphs emit no augmentation, so the declaration never references a module the project has no @@ -1074,7 +1074,7 @@ through the Agent renderer with the full CLI output contract (`--json`, reserves exactly `--json` and `--ndjson`, every other argument passes through as `argv`, and the exit code derives from the final document status (0 on `success`, 1 otherwise). Explicit `scripts` config entries keep -ordinary Node semantics regardless of extension — config always wins, and +ordinary Node semantics regardless of extension, config always wins, and only the conventional route contract opts into rendering. Script routes neither pipeline can ship are hard errors (`AB4808`/`AB4809`), never silent omissions. @@ -1096,14 +1096,14 @@ modes: interactive TTY updates progress in place before the final document; piped output emits exactly one final Markdown document (no partial fallbacks); `--json` emits the canonical validated final value; `--ndjson` emits the sequence-numbered render-event stream (an Agent Bundle CLI/script -dialect — never MCP JSON-RPC, never written to an MCP server's stdout). +dialect, never MCP JSON-RPC, never written to an MCP server's stdout). Diagnostics go to stderr; machine output owns stdout. Exit codes: 0 on success (or the validated result's integer `exitCode` under `config.exitCode: 'result'`), 1 on execution/render failure, 2 on usage or input-validation failure, 130/143 after SIGINT/SIGTERM. `--help`, `--json`, `--ndjson`, and `--version` are owned by the generated shell. An -`inputSchema` rejection is reported one issue per line in CLI terms — -`Invalid value for : expected ; received .` — then +`inputSchema` rejection is reported one issue per line in CLI terms, +`Invalid value for : expected ; received .`, then the usage line; `--json` writes one `{"error":{"code":"CLI_INPUT_INVALID", ...}}` line to stderr and `--ndjson` one `type: "error"` event (#465). @@ -1153,7 +1153,7 @@ Imported runtime schemas do not create shared static contracts. A generated tool may also carry an opt-in CLI surface projection: a colocated `.cli.{ts,tsx}` beside the tool route. The module is never -a route — `RouteContract.routes` does not list it — and the compiled +a route, `RouteContract.routes` does not list it, and the compiled command's `routeId` stays the tool id. `inspect --routes` prints `cli.commands[].projection` (`module`, `mapInput`, `defaults?`, `relaxed?`) and the mapped `options[]` (`key`, `option`, `aliases`). A projection that cannot @@ -1177,9 +1177,9 @@ projections; no static provider subset declaration is required. | `AB4801` | error | The conventional `src/cli.ts` entry and `src/cli/` command route modules both exist without an explicit `routes.cli` mode. | | `AB4802` | error | Two route modules derive the same route id (for example `.ts` and `.tsx` siblings with one stem). | | `AB4803` | error | A route path derives an unsafe identity segment (each segment must match `^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$`). | -| `AB4804` | error | A `routes` mode override is not `generated`/`custom`/`command`/`remote` for a server, or `generated`/`conventional` for the CLI; or `routes.cli: 'conventional'` is set while the project has generated commands to carry (`routes.mcpCommands`, or a `.cli.{ts,tsx}` projection module — the message names the modules). | +| `AB4804` | error | A `routes` mode override is not `generated`/`custom`/`command`/`remote` for a server, or `generated`/`conventional` for the CLI; or `routes.cli: 'conventional'` is set while the project has generated commands to carry (`routes.mcpCommands`, or a `.cli.{ts,tsx}` projection module, the message names the modules). | | `AB4805` | error | A route module exports `config` through a rejected declaration shape (`let`/`var`, destructuring, `export { config }`, a function or class, a missing initializer), or the extracted value is not an object. | -| `AB4806` | error | A route module's `config` initializer is dynamic — the message names the offending construct and position (for a reference the static resolver could not follow, the boundary it stopped at: a non-relative specifier, a module outside the project, a missing `export const`, a non-`const` binding, a non-literal initializer), and the recovery names the two accepted reference forms (a top-level `const` string literal declared locally or reached through `export const` alias hops across any number of relative modules inside the project, and `appResourceUri('')` from `agent-bundle/routes`). | +| `AB4806` | error | A route module's `config` initializer is dynamic, the message names the offending construct and position (for a reference the static resolver could not follow, the boundary it stopped at: a non-relative specifier, a module outside the project, a missing `export const`, a non-`const` binding, a non-literal initializer), and the recovery names the two accepted reference forms (a top-level `const` string literal declared locally or reached through `export const` alias hops across any number of relative modules inside the project, and `appResourceUri('')` from `agent-bundle/routes`). | | `AB4807` | retired | The stage-1 rendered-script gate. Rendered script routes ship through the Agent renderer pipeline since #102 stage 3; the code is never reused. | | `AB4808` | error | A conventional `src/scripts/` route nests below the scripts root; conventional scripts ship as direct children only. Move it up, prefix a path segment with `_`, or declare it under `scripts` in config with a flat name. | | `AB4809` | error | A conventional `src/scripts/` route and a configured `scripts` entry share one script identity through different files. Point the config entry at the module to claim it, or rename one of the two. | @@ -1202,21 +1202,21 @@ projections; no static provider subset declaration is required. | `AB4826` | error | A route's static `config` calls `appResourceUri('')` with a reference that matches no App route of the route's own generated server with a static `config.resourceUri`: an unknown name, another server's App (a generated server registers only its own Apps), or a reference from a non-MCP route. The message names the cause and lists the server's known App route ids; reference the App as `''`, `'/'`, `'app:/'`, or a relative module path. | | `AB4827` | error | An MCP App route's `config.template` is ambiguous or missing: both the route-relative and the project-root-relative interpretation name different existing files, or neither exists. The message names both candidate paths; templates resolve relative to the route module, so rewrite the path as `'./.html'` beside the route. | | `AB4828` | error | A generated MCP route advertises `_meta.ui.resourceUri` of an App on its server (through `appResourceUri()` or a literal) that is not built for every target the server ships to, because the App's `config.targets` (or a config-declared App's `targets`) is narrower. Widen the App's targets or restrict `mcp.servers..targets`. | -| `AB4829` | error | Two distinct MCP App routes of one generated server declare the same static `config.resourceUri`. The message names both route files and the server; a generated server registers one App per resource URI and never picks a side. The same URI on App routes of *different* servers is not a collision — each server registers only its own Apps. Give each App route of the server a distinct `config.resourceUri`, or remove the duplicate module. | +| `AB4829` | error | Two distinct MCP App routes of one generated server declare the same static `config.resourceUri`. The message names both route files and the server; a generated server registers one App per resource URI and never picks a side. The same URI on App routes of *different* servers is not a collision, each server registers only its own Apps. Give each App route of the server a distinct `config.resourceUri`, or remove the duplicate module. | | `AB4830` | error | A conventional layout module (`src/layout.*`, `src/mcp//layout.*`) does not satisfy the layout contract: it has no default export, it exports the route-only `config`/`inputSchema`/`resultSchema`, or it exports `execute`/`render`. Default-export one component receiving `{ children, route, signal }` that renders `Agent.Result` around `children`. | | `AB4831` | error | Two layout modules declare one layout scope (for example `src/layout.ts` beside `src/layout.tsx`). Keep exactly one module per scope. | -| `AB4832` | error | A server layout (`src/mcp//layout.*`) names an MCP server that declares no tool, resource, or prompt route modules — the server directory is missing or holds only `apps/` routes, which never take a layout. Add routes under that server directory, move the layout, or rename it `_layout.*` to opt out. A server pinned to `custom`, `command`, or `remote` via `routes.servers.` is skipped entirely: its layout is neither validated (`AB4830`) nor retained, because no generated worker composes it. | -| `AB4833` | error | `notices.retention` is malformed: `notices` or `retention` is not an object, carries an unknown key, `terminalTtl` is not a positive integer of milliseconds or a duration such as `"7d"`, `"12h"`, `"30m"`, or `"90s"`, `maxTerminal` / `maxJournalBytes` is not a positive integer — or the policy is declared by a project without a conventional `src/state.ts`, which has no co-mounted notice ledger to retain. Omit a field to keep the runtime default (`7d`, `500`, `16777216`). | -| `AB4834` | retired | Generated declaration inclusion is checked by the consumer’s TypeScript build. Include `.agent-bundle/routes.d.ts` in every consuming tsconfig; normal framework validation no longer constructs TypeScript programs. | -| `AB4835` | error | A route's static `config.render` (the render budget of one call, #454) is malformed: `render` is not an object, carries a key other than `maxElapsedMs`, `maxElapsedMs` is not a positive integer of milliseconds, or it exceeds the framework ceiling of `86400000` (24 hours) — or a plain `.ts` CLI command declares one, although it executes without a render session. Reported once per route: on an MCP tool, resource, or prompt route with its server (the tool's projected CLI command inherits the value), or on a `src/cli/**` command route; a route with a rejected budget compiles no command. Omit `render` to keep the runtime default (`60000`). Declare `config.render = { maxElapsedMs: }` on a rendered route, or remove it. The budget bounds the framework's render session only: Codex's `tool_timeout_sec` (60 s by default) and any per-server host timeout must be raised by the operator separately, while Claude Code's default per-call wall clock is about 28 hours and its idle timer is kept alive by the `notifications/progress` the projector forwards. | -| `AB4836` | error | A route's static `config.execution` (MCP task support, #369) is malformed: `execution` is not an object, carries a key other than `taskSupport`, or `taskSupport` is not one of `forbidden`, `optional`, `required` — or a resource or prompt route declares it, although the `2025-11-25` Tasks utility augments `tools/call` only. Reported once per route with its server. Omit `execution` to keep the wire default (`forbidden`: every call is an ordinary request), or declare `config.execution = { taskSupport: 'optional' }` so a task-aware client may receive a `CreateTaskResult` and poll `tasks/get` / `tasks/result` while the render continues, or `'required'` to refuse ordinary calls with JSON-RPC `-32601`. The generated server advertises the value in `tools/list` and declares the `tasks` capability only when at least one tool opted in. | +| `AB4832` | error | A server layout (`src/mcp//layout.*`) names an MCP server that declares no tool, resource, or prompt route modules, the server directory is missing or holds only `apps/` routes, which never take a layout. Add routes under that server directory, move the layout, or rename it `_layout.*` to opt out. A server pinned to `custom`, `command`, or `remote` via `routes.servers.` is skipped entirely: its layout is neither validated (`AB4830`) nor retained, because no generated worker composes it. | +| `AB4833` | error | `notices.retention` is malformed: `notices` or `retention` is not an object, carries an unknown key, `terminalTtl` is not a positive integer of milliseconds or a duration such as `"7d"`, `"12h"`, `"30m"`, or `"90s"`, `maxTerminal` / `maxJournalBytes` is not a positive integer, or the policy is declared by a project without a conventional `src/state.ts`, which has no co-mounted notice ledger to retain. Omit a field to keep the runtime default (`7d`, `500`, `16777216`). | +| `AB4834` | retired | Generated declaration inclusion is checked by the consumer's TypeScript build. Include `.agent-bundle/routes.d.ts` in every consuming tsconfig; normal framework validation no longer constructs TypeScript programs. | +| `AB4835` | error | A route's static `config.render` (the render budget of one call, #454) is malformed: `render` is not an object, carries a key other than `maxElapsedMs`, `maxElapsedMs` is not a positive integer of milliseconds, or it exceeds the framework ceiling of `86400000` (24 hours), or a plain `.ts` CLI command declares one, although it executes without a render session. Reported once per route: on an MCP tool, resource, or prompt route with its server (the tool's projected CLI command inherits the value), or on a `src/cli/**` command route; a route with a rejected budget compiles no command. Omit `render` to keep the runtime default (`60000`). Declare `config.render = { maxElapsedMs: }` on a rendered route, or remove it. The budget bounds the framework's render session only: Codex's `tool_timeout_sec` (60 s by default) and any per-server host timeout must be raised by the operator separately, while Claude Code's default per-call wall clock is about 28 hours and its idle timer is kept alive by the `notifications/progress` the projector forwards. | +| `AB4836` | error | A route's static `config.execution` (MCP task support, #369) is malformed: `execution` is not an object, carries a key other than `taskSupport`, or `taskSupport` is not one of `forbidden`, `optional`, `required`, or a resource or prompt route declares it, although the `2025-11-25` Tasks utility augments `tools/call` only. Reported once per route with its server. Omit `execution` to keep the wire default (`forbidden`: every call is an ordinary request), or declare `config.execution = { taskSupport: 'optional' }` so a task-aware client may receive a `CreateTaskResult` and poll `tasks/get` / `tasks/result` while the render continues, or `'required'` to refuse ordinary calls with JSON-RPC `-32601`. The generated server advertises the value in `tools/list` and declares the `tasks` capability only when at least one tool opted in. | | `AB4837` | error | A compiled executable imports a compiler-carrying framework entry (`agent-bundle`, `/api`, `/config`, `/eval`, `/rstest`, `/test`, or `/test/browser`). The build checks requests after transformation and resolved package export identities, including aliases, and names the importing module. Type-only imports erased by the configured transform are legal. Source-only `inspect` and `validate` do not prove this dependency boundary. Keep compiler calls in a host process, or use `import type` for framework types. | -| `AB4838` | — | Retired. Schema imports are resolved by the bundler, not an inspection-time interpreter. | -| `AB4839` | — | Retired. Inspection does not follow schema alias chains. | +| `AB4838` | none | Retired. Schema imports are resolved by the bundler, not an inspection-time interpreter. | +| `AB4839` | none | Retired. Inspection does not follow schema alias chains. | | `AB4840` | error | An event exports removed `before` or `preflight` bindings, its helper disagrees with the conventional path, or its `.view.tsx` sibling is not a valid rendered event module. Use a `.ts` handler and `ctx.render('./name.view.js', data)` for an explicit rendered view. | -| `AB4843` | error | A `.cli.{ts,tsx}` module under `src/mcp//tools/` has no sibling tool route `.{ts,tsx}` (orphan), a `.cli.{ts,tsx}` module sits under `resources/`, `prompts/`, or `apps/`, or a second projection module (`.cli.ts` beside `.cli.tsx`) names the same tool — the first in path order wins and the second is reported. The suffix is reserved under `src/mcp/**` only. The message is `CLI projection for tool:/: .` (`has no sibling tool route …`, ` already projects this tool …`); a misplaced module names no tool, so its message is `CLI projection : sits under resources/, prompts/, or apps/ …`. `sourcePath` is the projection module's absolute path. Recovery: rename the file to match the sibling tool, or prefix `_` to park it, then inspect again. It is an error because a projection that cannot compile has no correct partial output. | +| `AB4843` | error | A `.cli.{ts,tsx}` module under `src/mcp//tools/` has no sibling tool route `.{ts,tsx}` (orphan), a `.cli.{ts,tsx}` module sits under `resources/`, `prompts/`, or `apps/`, or a second projection module (`.cli.ts` beside `.cli.tsx`) names the same tool, the first in path order wins and the second is reported. The suffix is reserved under `src/mcp/**` only. The message is `CLI projection for tool:/: .` (`has no sibling tool route …`, ` already projects this tool …`); a misplaced module names no tool, so its message is `CLI projection : sits under resources/, prompts/, or apps/ …`. `sourcePath` is the projection module's absolute path. Recovery: rename the file to match the sibling tool, or prefix `_` to park it, then inspect again. It is an error because a projection that cannot compile has no correct partial output. | | `AB4844` | error | A CLI projection config does not satisfy its closed metadata contract, combines JSON input mode with flag mapping, or relaxes a required key without declaring `mapInput`. The generated runtime checks that a loaded `mapInput` is callable, awaits it, and validates the result through the original input schema. | -| `AB4845` | error | A CLI projection's grammar does not bind to the tool's contract: `flags`/`positionals` name a key absent from the tool's `RouteContract.input`; a `name`/alias is not kebab-case, is reserved (`help`, `json`, `ndjson`, `version`, and `yes` when confirm), or collides with another option's spelling or alias; `flags..name` or `flags..aliases` is declared on a key `positionals` consumes as a bare argument (`description`, `default`, and `required: false` still apply there); the tool's contract has a key `yes` while the command confirms — the shell keys parsed values by canonical key and strips `yes` as the confirmation, so no `name` override reaches the tool (`set confirm: false or rename the key`); or a `command` segment is not a safe identity segment. The message is `CLI projection for tool:/: .` and `sourcePath` is the projection module's absolute path. Recovery names the offending key or spelling and the accepted form, then says to inspect again. It is an error because a projection that cannot compile has no correct partial output. | +| `AB4845` | error | A CLI projection's grammar does not bind to the tool's contract: `flags`/`positionals` name a key absent from the tool's `RouteContract.input`; a `name`/alias is not kebab-case, is reserved (`help`, `json`, `ndjson`, `version`, and `yes` when confirm), or collides with another option's spelling or alias; `flags..name` or `flags..aliases` is declared on a key `positionals` consumes as a bare argument (`description`, `default`, and `required: false` still apply there); the tool's contract has a key `yes` while the command confirms, the shell keys parsed values by canonical key and strips `yes` as the confirmation, so no `name` override reaches the tool (`set confirm: false or rename the key`); or a `command` segment is not a safe identity segment. The message is `CLI projection for tool:/: .` and `sourcePath` is the projection module's absolute path. Recovery names the offending key or spelling and the accepted form, then says to inspect again. It is an error because a projection that cannot compile has no correct partial output. | | `AB4940` | error | A conventional provider module has no default export. Default-export a factory receiving `{ invocation, plugin, signal }`. | | `AB4941` | error | Two provider filenames derive the same camel-cased provider key. Rename one file so every provider key is unique. | | `AB4942` | retired | Process identity is available through `context.process`; `processLifetime` is no longer a reserved provider key. | @@ -1235,14 +1235,14 @@ SQLite lock or shared-memory files. An installed pack's shells read `/.env` and `.env.local` at launch (#469) to fill variables the host did not set. Doctor reports whether -those files are present and how many variables each declares — never a name -or a value — so an operator can see that a credential-configured pack is, or +those files are present and how many variables each declares, never a name +or a value, so an operator can see that a credential-configured pack is, or is not, configured. Absent files are the normal case and produce no diagnostic. | Code | Severity | Trigger | | --- | --- | --- | -| `AB7331` | info / warning | Info: an installed copy (or the `--from` bundle) carries `.env` or `.env.local` at its plugin root; the message names the file and its variable count. Warning: the file exists but cannot be read, so the pack's shells skip it at launch — repair its permissions and rerun Doctor. | +| `AB7331` | info / warning | Info: an installed copy (or the `--from` bundle) carries `.env` or `.env.local` at its plugin root; the message names the file and its variable count. Warning: the file exists but cannot be read, so the pack's shells skip it at launch, repair its permissions and rerun Doctor. | ## Read-only Doctor legacy state (`AB7332`) @@ -1277,8 +1277,8 @@ the receipt-recorded derived root purgeable. ## Read-only Doctor bundle resolution (`AB7306`) -`doctor --from ` resolves the bundle exactly as `install` does — through -`agent-bundle.manifest.json` (`install/identity.ts` `readBundleIdentity`) — and +`doctor --from ` resolves the bundle exactly as `install` does, through +`agent-bundle.manifest.json` (`install/identity.ts` `readBundleIdentity`), and reports the host's `bundle` finding as `failed` when that resolution throws. The identity reader's own refusals keep their code (`AB7001`, below) and the message `install` would print, with Doctor's recovery attached; `AB7306` is @@ -1286,7 +1286,7 @@ reserved for everything else. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB7306` | error | Resolving the bundle failed for a reason that is not a manifest diagnostic — a filesystem error other than "missing" while checking a pointer (`ENOTDIR` because a pointer's parent is a regular file, `EACCES`), or any other thrown error; the message carries the underlying error text. | Repair the root or its permissions (or rebuild it), then rerun Doctor. | +| `AB7306` | error | Resolving the bundle failed for a reason that is not a manifest diagnostic, a filesystem error other than "missing" while checking a pointer (`ENOTDIR` because a pointer's parent is a regular file, `EACCES`), or any other thrown error; the message carries the underlying error text. | Repair the root or its permissions (or rebuild it), then rerun Doctor. | ## Read-only Doctor static validation (`AB7319`–`AB7320`) @@ -1297,7 +1297,7 @@ host CLI, repair a bundle, or perform a live protocol exchange. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | | `AB7319` | error | A host tree resolved from `doctor --from` violates its pinned document schemas or process-free loader rules. The message retains the originating build-validator code and detail. | Rebuild that host bundle from valid source bytes, then rerun Doctor. | -| `AB7320` | error / info | Error when a `.cursor-plugin/plugin.json` install violates Cursor's pinned document schemas or token-location rules (the hooks document checked is the one the manifest `hooks` field names — `.cursor-plugin/hooks.json` — so a Claude-format `hooks/hooks.json` beside it in a composite root is not a finding), when a root `plugin.json` install that declares an Agent Plugins `$schema` violates the pinned Agent Plugins 1.0.0 contract (`AB6035`–`AB6037`, retained in the message), or when any local plugin contains a symlink that escapes `~/.cursor/plugins/local`; the inventory entry is reported as `corrupt`. Info naming the contract applied to an Agent Plugins install, or stating that a `.claude-plugin/plugin.json` (or schema-less root `plugin.json`) install has no Cursor-side pinned static document contract; loader-recognized entries remain `installed`. | Reinstall an invalid Cursor plugin, rebuild an invalid portable bundle, or repair an escaping symlink. For other manifest flavors, use that ecosystem's validator when static document proof is required. | +| `AB7320` | error / info | Error when a `.cursor-plugin/plugin.json` install violates Cursor's pinned document schemas or token-location rules (the hooks document checked is the one the manifest `hooks` field names, `.cursor-plugin/hooks.json`, so a Claude-format `hooks/hooks.json` beside it in a composite root is not a finding), when a root `plugin.json` install that declares an Agent Plugins `$schema` violates the pinned Agent Plugins 1.0.0 contract (`AB6035`–`AB6037`, retained in the message), or when any local plugin contains a symlink that escapes `~/.cursor/plugins/local`; the inventory entry is reported as `corrupt`. Info naming the contract applied to an Agent Plugins install, or stating that a `.claude-plugin/plugin.json` (or schema-less root `plugin.json`) install has no Cursor-side pinned static document contract; loader-recognized entries remain `installed`. | Reinstall an invalid Cursor plugin, rebuild an invalid portable bundle, or repair an escaping symlink. For other manifest flavors, use that ecosystem's validator when static document proof is required. | ## Install replacement and Doctor install comparison (`AB7005`, `AB7307`–`AB7309`, `AB7321`) @@ -1335,7 +1335,7 @@ committed tree, so theirs live in an Agent Bundle-owned store, `/agent-bundle/receipts/...json` for Claude and Codex and `.marketplace.json` for Cursor staging (`~/.claude` or `$CLAUDE_CONFIG_DIR`, `~/.codex` or `$CODEX_HOME`, -`~/.cursor`), with `files: []` — they own no files, only the registrations and +`~/.cursor`), with `files: []`. They own no files, only the registrations and the content hash. The host identifies a registration as `@`, so the same plugin installed from two marketplaces is two installs with two receipts. A Claude `project` / `local` scope registration belongs to @@ -1343,15 +1343,15 @@ the working directory the host verbs ran in (the bundle root), so those receipts are keyed `...<12-hex digest of projectRoot>.json` and record `projectRoot`: two projects installing the same plugin at the same scope are two receipts. The `-marketplace` registration is recorded only when -the install actually created it — `plugin marketplace list --json` did not list +the install actually created it, `plugin marketplace list --json` did not list the marketplace beforehand (or the receipted install it replaces recorded it); a marketplace that already existed, or one whose state could not be read, is not claimed, and `uninstall` then retains it and says why. Between `plugin marketplace add` and the receipt write those registrations exist only in memory, so if the plugin install or the receipt write fails the install -reverses what did complete — the plugin (`plugin uninstall … --keep-data` / +reverses what did complete, the plugin (`plugin uninstall … --keep-data` / `plugin remove`) when it was installed, then the marketplace when this run -created it — before rethrowing; a failed reversal is reported with the exact +created it, before rethrowing; a failed reversal is reported with the exact host commands to run before retrying. Nothing is left registered without a receipt to record it. A format 1 receipt (written by #420) is read with those @@ -1366,7 +1366,7 @@ so the artifact hash, the installed tree, and the receipt always describe the same entries. Ownership of an existing destination is decided as **receipt** (a receipt naming this plugin), **legacy** (no receipt, but the emitted `INSTALL.md` + `install.mjs` and a manifest with -this plugin's name — a copy installed before receipts existed), or **foreign** +this plugin's name, a copy installed before receipts existed), or **foreign** (anything else). Claude and Codex copies are located through the host's own `plugin list --json` inventory (Doctor runs it once per host and also lists every installed plugin from it; `AB7303` is emitted only when that listing is unusable); @@ -1380,13 +1380,13 @@ settings-preserving update API, and native add would set plugin-level enabled to | Installed copy | `install` | `install --replace` (alias `--force`) | Doctor | | --- | --- | --- | --- | | Identical content (receipt / host-managed) | `already-installed` no-op | `already-installed` no-op | `current` | -| Identical content (legacy) | `already-installed` no-op | `adopted` — receipt written, no plugin file changes | `current` | -| Receipt / host-managed, same version, different content | replaced automatically (`replaced`) | replaced | `stale` — `AB7308` warning | -| Receipt / host-managed, different version | `AB7005` version collision | replaced | `version-mismatch` — `AB7309` warning | -| Legacy, different content | `AB7005` content collision | adopted: the artifact's files are rewritten, every other file is left in place and stays unowned, receipt written (`replaced`) | `stale` — `AB7308` warning, recovery names `--replace` | -| Foreign directory | `AB7005` foreign install | `AB7005` foreign install | `foreign` — `AB7321` warning | -| Claude copy listed with `errors` (host refused to load it) | identical content: `AB7006`; otherwise replaced, then `AB7006` if the fresh row still carries `errors` | replaced, then `AB7006` if the fresh row still carries `errors` | `load-failed` — `AB7325` error (see below) | -| Nothing installed | installed | installed | `not-installed` — `AB7307` info | +| Identical content (legacy) | `already-installed` no-op | `adopted`, receipt written, no plugin file changes | `current` | +| Receipt / host-managed, same version, different content | replaced automatically (`replaced`) | replaced | `stale`, `AB7308` warning | +| Receipt / host-managed, different version | `AB7005` version collision | replaced | `version-mismatch`, `AB7309` warning | +| Legacy, different content | `AB7005` content collision | adopted: the artifact's files are rewritten, every other file is left in place and stays unowned, receipt written (`replaced`) | `stale`, `AB7308` warning, recovery names `--replace` | +| Foreign directory | `AB7005` foreign install | `AB7005` foreign install | `foreign`, `AB7321` warning | +| Claude copy listed with `errors` (host refused to load it) | identical content: `AB7006`; otherwise replaced, then `AB7006` if the fresh row still carries `errors` | replaced, then `AB7006` if the fresh row still carries `errors` | `load-failed`, `AB7325` error (see below) | +| Nothing installed | installed | installed | `not-installed`, `AB7307` info | Every `AB7005`, `AB7308`, `AB7309`, and `AB7321` message carries the comparison `installed @ content vs artifact @ content @@ -1394,9 +1394,9 @@ Every `AB7005`, `AB7308`, `AB7309`, and `AB7321` message carries the comparison Cursor replacement is in place and touches owned files only: stale owned files are removed and the emptied directories the installer itself created (`directories` in the receipt) are pruned, staged files are renamed over their -predecessors, and the receipt lands last. Entries the installer does not own — +predecessors, and the receipt lands last. Entries the installer does not own, notably workspace-durable `state/` stores, and any directory that already -existed before the installer wrote beneath it — are never removed or rewritten; +existed before the installer wrote beneath it, are never removed or rewritten; when a rebuilt artifact introduces a path that an existing unowned entry already occupies, replacement aborts before any change (`AB7004`, "Refusing to overwrite unowned files") and names the colliding paths. Receipt file and directory lists @@ -1408,8 +1408,8 @@ FIFO) is refused outright (`AB7004`) before it is read. The same rules apply to the artifact itself: a file whose path could not round-trip through a receipt (a backslash in a POSIX name, reserved characters, a trailing dot or space) is refused (`AB7004`) before anything is staged. Every other failure of a local -Cursor install — a `~/.cursor` that exists but cannot be inspected, an -inventory, staging, or receipt write that fails — is reported the same way, as +Cursor install, a `~/.cursor` that exists but cannot be inspected, an +inventory, staging, or receipt write that fails, is reported the same way, as `AB7004` with `target: cursor` and the underlying message; a missing `~/.cursor` is `AB7002`. The staging directory is removed before the failure is reported, so a refused or failed replacement never leaves a @@ -1431,8 +1431,8 @@ also retained unowned entries beside (or instead of) `state/`, both the inventory finding and the `--from` bundle finding read the directory and the `AB7307` message names those retained entries and points at removing them by hand, since `uninstall` never will. Preserved state is only what `uninstall` -would still keep — a `state/` that holds something, and this home's real, -non-empty `PLUGIN_DATA` directory — so a remnant whose data has since been +would still keep, a `state/` that holds something, and this home's real, +non-empty `PLUGIN_DATA` directory, so a remnant whose data has since been removed or emptied is reported as exhausted, with the default `uninstall` that consumes it as the recovery. @@ -1445,14 +1445,14 @@ receipt-owned reverse of `install` (#101; the maintainer's 2026-09-01 G4 deferral of mutation was reversed on 2026-09-03 with the request to fix every open issue). Every mutation is opt-in and bounded by the receipt: -- **Cursor local** — removes exactly the receipt's `files`, prunes its +- **Cursor local**: removes exactly the receipt's `files`, prunes its `directories` and the plugin root once empty, then the `hostDirectories` the install created (`~/.cursor/plugins/local`, `~/.cursor/plugins` in a fresh - home). Unowned entries are listed as retained and never removed — files by + home). Unowned entries are listed as retained and never removed, files by path, and unowned directories that hold nothing retained as `name/` (the prune only ever touches owned directories, so they survive too). When the plugin root survives (retained state or unowned entries), a **remnant - receipt** — `files: []`, `registrations: []`, the carried `hostDirectories` — + receipt**, `files: []`, `registrations: []`, the carried `hostDirectories`, is written there so a later purge can still prune the created directories and Doctor can explain the directory. When the receipt records a Cursor placeholder expansion (`cursorExpansion`, written by the emitted `install.mjs` @@ -1468,18 +1468,18 @@ open issue). Every mutation is opt-in and bounded by the receipt: purge of the leaf would follow it outside the Cursor home. Doctor's `AB7307` names the directory as preserved state only when it is that same real, non-empty directory. -- **Cursor marketplace** — verifies the staged repository's `HEAD` against the +- **Cursor marketplace**: verifies the staged repository's `HEAD` against the commit the store receipt recorded and its working tree against that commit (`git --no-optional-locks status --porcelain --untracked-files=all --ignored=matching`, the same probe Doctor uses: any uncommitted, untracked, - or ignored entry — or a tree that cannot be verified because git is missing - or `status` fails — is refused with `AB7007` until `--force`, since the + or ignored entry, or a tree that cannot be verified because git is missing + or `status` fails, is refused with `AB7007` until `--force`, since the removal is recursive and those entries are not receipt-owned), then removes the repository wholesale and the receipt; a copy Cursor imported into `~/.cursor/plugins/cache` (recognised by the receipted commit and version, not the version the bundle may have been rebuilt to) is Cursor-owned and is reported `manual` with the Customize step in `nextSteps`. -- **Claude / Codex** — reads ` plugin list --json` (an unusable listing +- **Claude / Codex**: reads ` plugin list --json` (an unusable listing fails closed, `AB7004`), compares the cached copy with the receipt, runs `claude plugin uninstall --scope --keep-data` / `codex plugin remove `, then `plugin marketplace remove ` @@ -1487,13 +1487,13 @@ open issue). Every mutation is opt-in and bounded by the receipt: to every scope, the marketplace is `retained` when the receipt does not record Agent Bundle registering it (it pre-existed the install, or there is no receipt), when another installed plugin still names it, when another - store receipt (another project's scoped install) installs from it — whether + store receipt (another project's scoped install) installs from it, whether that receipt records the marketplace registration or only its plugin, since - a plugin installed after the marketplace existed still needs it — when the + a plugin installed after the marketplace existed still needs it, when the same plugin is installed at another Claude scope or in another project (live - row, Claude's cross-project `plugins/installed_plugins.json` registry — + row, Claude's cross-project `plugins/installed_plugins.json` registry, which also records hand-made `project`/`local` installs elsewhere that have - no receipt and are invisible to `plugin list --json` run here — or stored + no receipt and are invisible to `plugin list --json` run here, or stored receipt), or when `plugin marketplace list --json`, the dependency re-read of `plugin list --json`, that registry, or any receipt in the store cannot be read (a failed read is not proof that nothing depends on it: an @@ -1519,32 +1519,32 @@ for its ~14-day grace period; a purge additionally removes `state/` and `plugins/data//`), `removed-by-host` (codex-cli 0.147.0 deletes the cached tree on `plugin remove`), and `unavailable` (Codex has no keep-data option; a staged Cursor marketplace holds no runtime state). `--plan` computes the same -report — exact absolute paths, registrations, data decision — without opening a +report, exact absolute paths, registrations, data decision, without opening a writer; planned directories are exactly the ones the run would prune (purged `state/` first, then every owned directory that would be left empty, and for store receipts the `/agent-bundle/receipts` and -`/agent-bundle` directories — plus Cursor's -`agent-bundle/marketplaces` — once the last entry leaves them), never a +`/agent-bundle` directories, plus Cursor's +`agent-bundle/marketplaces`, once the last entry leaves them), never a directory kept alive by retained state or unowned entries: `removed` in a `--plan` result equals `removed` in the completed one. A second run after a successful uninstall is a `not-installed` no-op. When `--keep-data` left `state/` (or a written `PLUGIN_DATA` directory) behind under a Cursor local root, the remnant receipt written there stays in place (`receipt.status: 'remnant'`) and a rerun without `--purge-data` is the same `not-installed` -no-op for as long as that preserved data — or an unowned entry the uninstall -retained — is still there; `--purge-data --confirm-purge` removes the +no-op for as long as that preserved data, or an unowned entry the uninstall +retained, is still there; `--purge-data --confirm-purge` removes the preserved state and prunes the root. Once the preserved data has been removed or emptied by hand (an empty `state/` or `PLUGIN_DATA` directory holds no data, so it is pruned like an installer-created directory rather than kept), -the remnant guards nothing, and the next run — with or without `--purge-data` -— consumes it: the receipt, the empty plugin root, and the host and +the remnant guards nothing, and the next run, with or without `--purge-data`, +consumes it: the receipt, the empty plugin root, and the host and `plugin-data` directories it recorded. Doctor reports such a remnant as exhausted (`AB7307`) instead of claiming preserved state that is gone. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB7007` | error | `uninstall` refused a mismatch or a foreign target: the owned files hash differently from the receipt, the cached host copy differs from the receipt in version or content, the staged repository's `HEAD` is not the recorded commit or its working tree is dirty / unverifiable, the receipt names another plugin, the directory is not this plugin's install at all, or a destination / `state/` entry is a symlink or special file. | `--force` overrides content and `HEAD` mismatches (the receipt-owned set is still the only thing removed); a receipt or manifest naming another plugin, and symlinked entries, are refused regardless — inspect and remove them manually. | -| `AB7008` | error | `--purge-data` without `--confirm-purge`, `--purge-data` together with `--keep-data`, or (Claude) `--purge-data` while the same plugin is installed at another scope or in another project (a live `plugin list --json` row, an entry in Claude's `plugins/installed_plugins.json` registry, or a stored receipt for the same plugin) — the cached copy and `plugins/data//` are scope-less and still in use — or while `claude plugin list --json` or that registry cannot be read to prove there is no other scope. | Pass `--purge-data --confirm-purge` to delete durable state, or neither flag to keep it; for a shared Claude scope, uninstall without `--purge-data` and purge after the last scope is removed. | +| `AB7007` | error | `uninstall` refused a mismatch or a foreign target: the owned files hash differently from the receipt, the cached host copy differs from the receipt in version or content, the staged repository's `HEAD` is not the recorded commit or its working tree is dirty / unverifiable, the receipt names another plugin, the directory is not this plugin's install at all, or a destination / `state/` entry is a symlink or special file. | `--force` overrides content and `HEAD` mismatches (the receipt-owned set is still the only thing removed); a receipt or manifest naming another plugin, and symlinked entries, are refused regardless, inspect and remove them manually. | +| `AB7008` | error | `--purge-data` without `--confirm-purge`, `--purge-data` together with `--keep-data`, or (Claude) `--purge-data` while the same plugin is installed at another scope or in another project (a live `plugin list --json` row, an entry in Claude's `plugins/installed_plugins.json` registry, or a stored receipt for the same plugin), the cached copy and `plugins/data//` are scope-less and still in use, or while `claude plugin list --json` or that registry cannot be read to prove there is no other scope. | Pass `--purge-data --confirm-purge` to delete durable state, or neither flag to keep it; for a shared Claude scope, uninstall without `--purge-data` and purge after the last scope is removed. | | `AB7009` | error | `uninstall` found the install but no receipt proving Agent Bundle owns it: a Cursor local copy in the pre-receipt legacy layout, a staged marketplace repository without its store receipt, or a host-registered Claude/Codex copy without its store receipt. | Re-run with `--force` (a legacy Cursor copy is removed by its inventory, `state/` kept; a host-CLI install is removed through the host verbs), or reinstall with `--replace` first to record a receipt. | The Cursor and portable host-install proofs (`tests/host-install-proof.test.ts`, @@ -1558,9 +1558,9 @@ bookkeeping; Codex: an empty `config.toml` and empty cache directories). ## Read-only Doctor lifecycle receipts and activation states (`AB7328`–`AB7330`) With `--from`, Doctor reports each host bundle's lifecycle as four typed -observations — **placed** (bytes at the host's install location), **registered** +observations, **placed** (bytes at the host's install location), **registered** (the host's registry names the plugin), **enabled** (the host reports it -enabled/trusted), **active** (loaded by a live host process) — each either +enabled/trusted), **active** (loaded by a live host process), each either `observed` with the host evidence that made it true or false, or `unavailable` with the reason no pinned read-only surface exposes it, and a `stage` (the furthest observed-true stage; `absent` when placement is observed false, @@ -1580,16 +1580,16 @@ inventory finding and on the bundle finding). | Code | Severity | Meaning | Recovery | | --- | --- | --- | --- | -| `AB7328` | warning | A store receipt is orphaned — the host no longer holds the registration it records (Claude/Codex listing lacks the plugin — a Claude `project`/`local` receipt is checked by `plugin list --json` run from its recorded `projectRoot`, and is `unknown`, never orphaned, when that root cannot be listed; the staged Cursor marketplace repository is gone) — or the receipt store / a receipt file could not be read or is not a valid receipt. | `agent-bundle uninstall --from [--mode marketplace]` consumes an orphaned receipt; reinstall to rewrite an invalid one; repair permissions. | +| `AB7328` | warning | A store receipt is orphaned, the host no longer holds the registration it records (Claude/Codex listing lacks the plugin, a Claude `project`/`local` receipt is checked by `plugin list --json` run from its recorded `projectRoot`, and is `unknown`, never orphaned, when that root cannot be listed; the staged Cursor marketplace repository is gone), or the receipt store / a receipt file could not be read or is not a valid receipt. | `agent-bundle uninstall --from [--mode marketplace]` consumes an orphaned receipt; reinstall to rewrite an invalid one; repair permissions. | | `AB7329` | info | A receipt predates lifecycle receipts (`agent-bundle-install-receipt/1`) and was read with synthesized `mode`, `scope`, `registrations`, and `hostDirectories`. Doctor never rewrites it. | Rerun `agent-bundle install` (or `install.mjs`) once; an identical copy rewrites the receipt as format 2 without changing plugin files. `uninstall` accepts the migrated receipt as is. | -| `AB7330` | info | The bundle's lifecycle stage on this host and its four observations; the message lists every `unavailable` stage with its reason. When Claude lists the plugin at several scopes the observations aggregate every row — a stage holds only when it holds for every listed copy, and the evidence names the scopes that are disabled, unplaced, or carry no enabled flag — so the report never depends on Claude's row order. | Stage-specific: register (`agent-bundle install`), enable (`claude plugin enable`, Codex `/plugins`, Cursor Customize), or complete the Cursor import; unavailable stages need no action and are never guessed. | +| `AB7330` | info | The bundle's lifecycle stage on this host and its four observations; the message lists every `unavailable` stage with its reason. When Claude lists the plugin at several scopes the observations aggregate every row, a stage holds only when it holds for every listed copy, and the evidence names the scopes that are disabled, unplaced, or carry no enabled flag, so the report never depends on Claude's row order. | Stage-specific: register (`agent-bundle install`), enable (`claude plugin enable`, Codex `/plugins`, Cursor Customize), or complete the Cursor import; unavailable stages need no action and are never guessed. | ## Live development into hosts (`AB7200`–`AB7202`, `AB7210`–`AB7211`, `AB8024`–`AB8025`) `agent-bundle dev` keeps a host's one stdio MCP process connected while it swaps the generated plugin behind it (`dev proxy`), re-syncs opted-in -development installs (`--install-host`) on every adopted epoch, and — when a -project declares `dev.contracts` — gates host-facing adoption on the +development installs (`--install-host`) on every adopted epoch, and, when a +project declares `dev.contracts`, gates host-facing adoption on the development contract matrix. Every failure on that path is a structured diagnostic; none of them silently changes what a host serves. A failing gate is not a build failure: the epoch publishes to the Workbench playground, and @@ -1614,8 +1614,8 @@ it, and publishes the result as an immutable epoch. One-shot `agent-bundle build` and programmatic `build()` use the same source-input snapshot check before accepting a compiled artifact, so compiler metadata from preparation cannot be published against a different tree Rspack compiled. Structured -diagnostics thrown along the dev path — the `AB4770` compile errors of an -MCP App view, the artifact validation codes — pass through to the failed +diagnostics thrown along the dev path, the `AB4770` compile errors of an +MCP App view, the artifact validation codes, pass through to the failed attempt unchanged, so the Workbench Overview and the `build.failed` Logs entry show the real finding. `AB7100` is only what remains: the fallback for a throw in that pass that carried no structured diagnostics, and the code of @@ -1624,9 +1624,9 @@ is the project's config file. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB7100` | error / warning | `Unable to compile the build: ` — the compile, validate, or publish pass of a rebuild threw something that was not a `DiagnosticError` carrying diagnostics. Also `Unable to clean up build attempt after the build: ` or `Unable to clean up staging epoch after the build: ` when removing the attempt directory or closing an unpublished staging epoch fails: a **warning** on a succeeded attempt (the epoch is live), an error on a failed one. | Read the wrapped error; a structured cause reports under its own code instead. A cleanup failure names a path under `.agent-bundle/attempts` or `.agent-bundle/epochs` to repair or remove. | -| `AB7101` | error | `Project source changed while the artifact was compiling; publication was rejected.` — the source snapshot taken after compilation and validation, immediately before publish, differs from the inputs the build read. The staged tree is discarded without replacing live artifact or package output, so a one-shot `build` / `build()` or a development epoch cannot publish mixed inputs. | Retry the build; the source that raced compilation is already on disk. The previous published artifact or package output remains. During `agent-bundle dev`, the change is already queued as the follow-up rebuild and the last-good epoch stays active until it succeeds. | -| `AB7102` | warning | `Artifact epoch was committed, but follow-up work was incomplete: ` — the epoch is published and active, but the work after the commit failed: retention cleanup of older epochs (`Epoch publication committed, but retention cleanup failed.`) or confirming the active-epoch metadata reached disk (`… active metadata durability could not be confirmed.`). | The epoch itself is valid and serving. Check the epoch store under `.agent-bundle/epochs` for the retained or unsynced files the wrapped error names; the next publication runs the same follow-up work again. | +| `AB7100` | error / warning | `Unable to compile the build: `, the compile, validate, or publish pass of a rebuild threw something that was not a `DiagnosticError` carrying diagnostics. Also `Unable to clean up build attempt after the build: ` or `Unable to clean up staging epoch after the build: ` when removing the attempt directory or closing an unpublished staging epoch fails: a **warning** on a succeeded attempt (the epoch is live), an error on a failed one. | Read the wrapped error; a structured cause reports under its own code instead. A cleanup failure names a path under `.agent-bundle/attempts` or `.agent-bundle/epochs` to repair or remove. | +| `AB7101` | error | `Project source changed while the artifact was compiling; publication was rejected.`, the source snapshot taken after compilation and validation, immediately before publish, differs from the inputs the build read. The staged tree is discarded without replacing live artifact or package output, so a one-shot `build` / `build()` or a development epoch cannot publish mixed inputs. | Retry the build; the source that raced compilation is already on disk. The previous published artifact or package output remains. During `agent-bundle dev`, the change is already queued as the follow-up rebuild and the last-good epoch stays active until it succeeds. | +| `AB7102` | warning | `Artifact epoch was committed, but follow-up work was incomplete: `, the epoch is published and active, but the work after the commit failed: retention cleanup of older epochs (`Epoch publication committed, but retention cleanup failed.`) or confirming the active-epoch metadata reached disk (`… active metadata durability could not be confirmed.`). | The epoch itself is valid and serving. Check the epoch store under `.agent-bundle/epochs` for the retained or unsynced files the wrapped error names; the next publication runs the same follow-up work again. | ## Development package build (`AB7103`) @@ -1639,7 +1639,7 @@ invalidation. See `docs/entry-conventions.md` for the dev-watch contract. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB7103` | warning | `Package build (bin/lib) failed during development rebuild: ` — the framework-owned package build threw during a rebuild; the artifact epoch that committed stays live. `Unable to remove stale package build outputs: ` — the project no longer declares a package build and the outputs this session last published could not be removed. `sourcePath` is the project's config file. | Read the wrapped error; the package build runs again on the next invalidation. | +| `AB7103` | warning | `Package build (bin/lib) failed during development rebuild: `, the framework-owned package build threw during a rebuild; the artifact epoch that committed stays live. `Unable to remove stale package build outputs: `, the project no longer declares a package build and the outputs this session last published could not be removed. `sourcePath` is the project's config file. | Read the wrapped error; the package build runs again on the next invalidation. | ## Read-only Doctor Cursor hook registration and marketplace staging (`AB7322`–`AB7324`) @@ -1650,9 +1650,9 @@ that registration statically and never writes `~/.cursor/hooks.json`. | Code | Severity | Meaning | Recovery | | --- | --- | --- | --- | -| `AB7322` | info / error | Info: an installed `.cursor-plugin/plugin.json` plugin registers plugin-scoped hooks (from the document its manifest `hooks` field names, or from `hooks/hooks.json` folder discovery when the field is absent; events and command count listed) and the script each command executes — `${CURSOR_PLUGIN_ROOT}/…` or any relative path, including an interpreter's entry operand — exists under the plugin root (`hooks.state = registered`). Error: the declared hooks file is missing (`missing`), is not a regular file or not a `{ version, hooks: { : [{ command }] } }` document, or an executed script is absent (`stale`). Documents and scripts are probed with `stat` before any read, so a FIFO cannot stall Doctor. | Reinstall the plugin from a bundle whose emitted hooks document and scripts are intact. | -| `AB7323` | warning | `~/.cursor/hooks.json` registers a command whose executed file (after leading `NAME=value` assignments, `env`, and interpreter options) points into an installed plugin directory — compared on path-component boundaries, case-folded on Windows — so Cursor would deliver that hook twice; or the file is not a valid hooks document. | Remove the plugin-pointing entries or repair the file; manifest registration alone is sufficient. | -| `AB7324` | info / warning / error | A staged marketplace repository under `~/.cursor/agent-bundle/marketplaces/` (from `install cursor --mode marketplace`) is imported by Cursor (matching plugin under `~/.cursor/plugins/cache`; info, `registered`), still awaiting the Customize "Add Plugins from Local Repository" step (warning, `unregistered`), or incomplete (error, `corrupt`: manifests missing or failing the pinned schemas, no resolvable Git HEAD, HEAD naming a commit object that does not exist, or a working tree that differs from committed HEAD — verified read-only through `git cat-file -e` / `git --no-optional-locks status` when `git` is available). | Complete the Customize import, use `--mode local`, or remove the staged directory and rerun the installer. | +| `AB7322` | info / error | Info: an installed `.cursor-plugin/plugin.json` plugin registers plugin-scoped hooks (from the document its manifest `hooks` field names, or from `hooks/hooks.json` folder discovery when the field is absent; events and command count listed) and the script each command executes, `${CURSOR_PLUGIN_ROOT}/…` or any relative path, including an interpreter's entry operand, exists under the plugin root (`hooks.state = registered`). Error: the declared hooks file is missing (`missing`), is not a regular file or not a `{ version, hooks: { : [{ command }] } }` document, or an executed script is absent (`stale`). Documents and scripts are probed with `stat` before any read, so a FIFO cannot stall Doctor. | Reinstall the plugin from a bundle whose emitted hooks document and scripts are intact. | +| `AB7323` | warning | `~/.cursor/hooks.json` registers a command whose executed file (after leading `NAME=value` assignments, `env`, and interpreter options) points into an installed plugin directory, compared on path-component boundaries, case-folded on Windows, so Cursor would deliver that hook twice; or the file is not a valid hooks document. | Remove the plugin-pointing entries or repair the file; manifest registration alone is sufficient. | +| `AB7324` | info / warning / error | A staged marketplace repository under `~/.cursor/agent-bundle/marketplaces/` (from `install cursor --mode marketplace`) is imported by Cursor (matching plugin under `~/.cursor/plugins/cache`; info, `registered`), still awaiting the Customize "Add Plugins from Local Repository" step (warning, `unregistered`), or incomplete (error, `corrupt`: manifests missing or failing the pinned schemas, no resolvable Git HEAD, HEAD naming a commit object that does not exist, or a working tree that differs from committed HEAD, verified read-only through `git cat-file -e` / `git --no-optional-locks status` when `git` is available). | Complete the Customize import, use `--mode local`, or remove the staged directory and rerun the installer. | The installer side reuses the `AB700x` codes: `AB7002` when `git` is missing in marketplace mode, `AB7003` when a mode is passed for a non-Cursor host, when @@ -1672,7 +1672,7 @@ content collisions (including a working tree that differs from committed HEAD). plugin that Claude Code then refuses at load time; the refusal surfaces only as the `errors` array on that plugin's row in `claude plugin list --json` (Claude Code 2.1.259 shape: `id`, `version`, `scope`, `enabled`, `installPath`, -`installedAt`, `lastUpdated`, optional `mcpServers`, and `errors` — a nonempty +`installedAt`, `lastUpdated`, optional `mcpServers`, and `errors`, a nonempty array of strings present only on a refused plugin; healthy rows omit the key, and the refused row still reports `enabled: true`). A refused copy is installed but contributes no hooks, MCP servers, or skills to a session, so @@ -1699,7 +1699,7 @@ until `claude plugin enable` runs (Claude Code docs, "Plugins reference" → not enable it. `agent-bundle doctor --host claude` reads the flag: the inventory entry carries `enabled: false` with `state: 'disabled'` (instead of `installed`), and a `--from` comparison of that copy carries `enabled: false` -next to its content verdict — a disabled copy can still be `current` or +next to its content verdict, a disabled copy can still be `current` or `stale`, and both facts are reported. Rows without a boolean `enabled` carry no flag and are `installed`. A row with `errors` is `failed` (`AB7325`) whatever its `enabled` value. A plugin that ships `defaultEnabled: false` in @@ -1726,9 +1726,9 @@ expanding `${PLUGIN_ROOT}` / `${PLUGIN_DATA}` in `args`, `env` values, or plugin-relative `./` commands resolved against the workspace folder (spec §7.2.1); see `docs/audits/2026-09-03-agent-plugins-cursor-ide-proof.md`. The emitted portable `install.mjs` therefore rewrites `mcp.json` in the Cursor copy -only — absolute plugin root, `~/.cursor/agent-bundle/plugin-data/` +only, absolute plugin root, `~/.cursor/agent-bundle/plugin-data/` (created) for the data directory, plugin-root `cwd`, resolved `./` command, and -`PLUGIN_ROOT` / `PLUGIN_DATA` in every stdio server's environment — and records +`PLUGIN_ROOT` / `PLUGIN_DATA` in every stdio server's environment, and records the substituted values plus the pre-expansion document in the install receipt (`cursorExpansion`). Doctor validates the Agent Plugins contract (`AB7320`) against that recorded document and proves the expansion against the installed @@ -1737,7 +1737,7 @@ placeholders itself. | Code | Severity | Meaning | Recovery | | --- | --- | --- | --- | -| `AB7326` | info / warning / error | Info (`launch.state = expanded`): the receipt's expansion still describes the installed copy — same plugin root, existing data directory, no placeholder left, absolute `cwd` and plugin-root `command`/`args` paths that exist, `PLUGIN_ROOT` / `PLUGIN_DATA` equal to the recorded values. Warning (`unexpanded`): an Agent Plugins install without a recorded expansion whose stdio servers still rely on the spec forms Cursor does not resolve (the message lists the forms per server); Cursor reports `spawn … ENOENT` / `MODULE_NOT_FOUND` for them. Error (`drifted`, entry `corrupt`): the installed `mcp.json` is not byte-identical to the expansion Doctor recomputes from the recorded document (edited, replaced, or removed after install), the recorded expansion names another plugin root (the copy was moved or duplicated), the data directory or an expanded path no longer exists, or the environment no longer carries the recorded values. Only a byte-identical copy has its recorded document validated by `AB7320`; a drifted copy is validated as the bytes on disk. Packages without stdio servers, and copies already carrying absolute paths with the §9.1 variables, produce no finding. | Reinstall with the bundle's emitted `install.mjs` at the copy's current location; the Cursor-target (`.cursor-plugin/plugin.json`) bundle is never rewritten and is not subject to this check. | +| `AB7326` | info / warning / error | Info (`launch.state = expanded`): the receipt's expansion still describes the installed copy, same plugin root, existing data directory, no placeholder left, absolute `cwd` and plugin-root `command`/`args` paths that exist, `PLUGIN_ROOT` / `PLUGIN_DATA` equal to the recorded values. Warning (`unexpanded`): an Agent Plugins install without a recorded expansion whose stdio servers still rely on the spec forms Cursor does not resolve (the message lists the forms per server); Cursor reports `spawn … ENOENT` / `MODULE_NOT_FOUND` for them. Error (`drifted`, entry `corrupt`): the installed `mcp.json` is not byte-identical to the expansion Doctor recomputes from the recorded document (edited, replaced, or removed after install), the recorded expansion names another plugin root (the copy was moved or duplicated), the data directory or an expanded path no longer exists, or the environment no longer carries the recorded values. Only a byte-identical copy has its recorded document validated by `AB7320`; a drifted copy is validated as the bytes on disk. Packages without stdio servers, and copies already carrying absolute paths with the §9.1 variables, produce no finding. | Reinstall with the bundle's emitted `install.mjs` at the copy's current location; the Cursor-target (`.cursor-plugin/plugin.json`) bundle is never rewritten and is not subject to this check. | ## Built-artifact validation (`AB6000`–`AB6018`, `AB6023`–`AB6025`, `AB6039`) @@ -1759,7 +1759,7 @@ names the host target namespace when the check is per target. artifact root and lists it in `agent-bundle.manifest.json` as a `generated` file. The record is what the compiler service reported about each file it emitted, bound to those bytes: one `assets[]` entry per compiled file -(`bundle` kind — `bin/*.mjs`, `scripts/*.mjs`, `hooks/*.mjs`, `mcp/*.mjs`, +(`bundle` kind, `bin/*.mjs`, `scripts/*.mjs`, `hooks/*.mjs`, `mcp/*.mjs`, Flight workers, `mcp-apps/*.html`) holds `path`, `sha256`, the kept `externals` (`kind` `artifact-relative` or `builtin`, `externalType`, `issuers`, `request`, `userRequest`, and `target` for a sibling), and the @@ -1779,27 +1779,27 @@ therefore does not prove the absence of such a load. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB6000` | error | `Artifact root is not a readable directory.` — the artifact root cannot be walked; `Artifact manifest is missing or cannot be read.` — the tree could not be inspected, or `agent-bundle.manifest.json` is absent, is not a regular file, or could not be read (the manifest is read between two identity checks, so a manifest replaced mid-read reports here too). Validation stops at this code. | Restore a readable artifact root and canonical manifest, then rebuild the artifact. | -| `AB6001` | error | `Artifact manifest is not a strict canonical manifest.` — `agent-bundle.manifest.json` does not parse as a strict canonical artifact manifest. This includes a `files[]` row with no matching `compiler.provenance` row: the parser's exact-path rule rejects it before inspection, and the diagnostic keeps this generic message. `Artifact manifest changed during validation.` — its bytes or identity differ between the first read and the re-read after validation. | Regenerate the strict canonical manifest without concurrent writes, then rerun validation. | +| `AB6000` | error | `Artifact root is not a readable directory.`, the artifact root cannot be walked; `Artifact manifest is missing or cannot be read.`, the tree could not be inspected, or `agent-bundle.manifest.json` is absent, is not a regular file, or could not be read (the manifest is read between two identity checks, so a manifest replaced mid-read reports here too). Validation stops at this code. | Restore a readable artifact root and canonical manifest, then rebuild the artifact. | +| `AB6001` | error | `Artifact manifest is not a strict canonical manifest.`, `agent-bundle.manifest.json` does not parse as a strict canonical artifact manifest. This includes a `files[]` row with no matching `compiler.provenance` row: the parser's exact-path rule rejects it before inspection, and the diagnostic keeps this generic message. `Artifact manifest changed during validation.`, its bytes or identity differ between the first read and the re-read after validation. | Regenerate the strict canonical manifest without concurrent writes, then rerun validation. | | `AB6002`–`AB6003` | error | Reserved: both codes are declared in the artifact diagnostic registry, but no validator emits either today. | `AB6002`: Rebuild the artifact from complete project source, then rerun validation. `AB6003`: Rebuild the artifact with canonical generated output, then rerun validation. | -| `AB6004` | error | `Artifact files do not match the manifest.` — the regular files on disk differ from the manifest file table (a path, byte length, mode, or SHA-256; a missing or unmanifested file). `Artifact file changed during validation: "".` — a file differed between the initial and final inspection, or between a validated staging tree and its re-check after `build` renamed it into place. `Artifact file table changed during validation.` — the final inspection could not be taken. | Rebuild the artifact so its file table and contents match the manifest. | -| `AB6005` | error | Primary compile-time form: `Compiled module "" keeps "" external () from ; a generated executable bundles everything but Node built-ins.` `` is the run-time load target; when an object-map external redirected the authored specifier, `, imported as "",` follows the type, a relative target that names no emitted asset of the artifact (or escapes it) ends `; it names no module emitted by this artifact.` instead, and a request kept under an external type that does not load a module (`var`, `global`, `this`, `window`, `assign`, `umd`, `amd`, `system`, `jsonp`, `promise`, `script`, …) — even a Node built-in — ends `; external type reads a variable instead of loading a module.` — the compiler service lowered a host-pack surface or package-build entry (`dist/bin/*.js`, the Flight workers, or the `lib` entry) and Rspack kept something other than a Node built-in, `pnpapi`, or an emitted sibling of that artifact external, whatever spelling Rspack emitted (`import`, `require`, or its `createRequire` shim); `generatedPath` names the asset. An expression request (`import(expr)`, `require(expr)`) is outside the compiler's view: Rslib's profile leaves it verbatim without parsing it, so it is neither bundled nor external; so is a literal import marked `rspackIgnore`/`webpackIgnore`, which Rspack leaves verbatim with no module, external, or warning. The emitted-module walk reports both in a compiled module the record proves: `Generated JavaScript import from "" has a non-literal dynamic import.` and `Generated JavaScript import from "" loads "", which the compiler neither bundled nor recorded as an external; an import the build ignored is a run-time load outside the artifact.` (a literal request that is neither a Node built-in nor one of the record's externals for that file). MCP App view form (the `, imported as` clause applies to both forms): `Compiled MCP App view "mcp-apps/.html" keeps "" external () from ; a view inlines every module it loads.` — a browser document has no allowable external, so the view's Rsbuild compilation (which carries the same audit plugin) fails on any `ExternalModule`, whatever the hatch mapped it to. Residual walk forms, `Generated JavaScript import from "" .`, reported only for what the compiler cannot see: `has a non-literal dynamic import` for an expression `import()` in any emitted module, compiled ones included, since Rslib's profile leaves that form verbatim; and, for a module the compile evidence record does not prove — JavaScript the framework did not compile (`install.mjs`, a copied script), every module of an artifact without a record, and every module of a build whose `tools` hatch may have rewritten the emitted bytes (`coverage.rewritable`) — `has invalid syntax` from a full parse, `uses unsupported specifier` or `uses invalid specifier` for a bare or malformed import, `cannot be read`, and the relative-target findings `is missing`, `resolves outside the artifact root`, `is not listed in the artifact manifest`, `does not resolve to a regular file`, `references invalid JSON`, and `uses unsupported target`. A compiled module the record covers with the same bytes is lexed for syntax and its literal imports are not resolved again — the compiler resolved them (bundled, built-in, or an emitted sibling). Prebuilt payloads are not walked; a `dist` finding names `dist/`. | Bundle every JavaScript dependency into the artifact, then rebuild it. | -| `AB6006` | error | `Generated JSON cannot be parsed.` — a `.json` file in the artifact is not valid JSON (prebuilt payload files are exempt). Doctor's Claude document lane reports the same code inside an `AB7319` message for a Claude bundle document that is unreadable or not valid JSON. | Regenerate the affected JSON document as valid JSON, then rebuild the artifact. | -| `AB6007` | error | `MCP manifest references missing generated server "".` — a root-level MCP manifest (pre-manifest pass) or a target's MCP manifest names a local server entry that the artifact does not contain. | Repair MCP manifest references to generated servers, then rebuild the artifact. | -| `AB6008` | error | `Artifact Agent Skills provenance does not match the pinned schema contract.` — the manifest's `agentSkills` schema SHA-256, source revision, or specification differs from the framework's pinned Agent Skills revision. | Rebuild the artifact with the pinned Agent Skills contract. | -| `AB6009` | error | `Artifact declares unknown target "".` — a manifest target is not registered in the target registry the validator was given (the project's configured registry for `build`, `validate --artifact`, and `dev`). | Rebuild the artifact with a registered target. | -| `AB6010` | error | `Artifact metadata and adapter identity for target "" do not match its registered contract.` — the manifest's per-target metadata or `builtInHost` adapter identity differs from the adapter registered under that name. | Rebuild the artifact with the current target registry. | -| `AB6011` | error | `Target "" is missing required document "".` — a document the target's artifact-validation contract marks required (for example a host manifest) is absent from the target namespace. Also reported inside Doctor's `AB7319` message for a missing required Claude bundle document. | Generate the required target document, then rebuild the artifact. | -| `AB6012` | error | `Target "" document "" is invalid for schema "" at : .` — a generated host document fails its vendored pinned schema (only the first issue is reported per document); unparsable documents are skipped here and reported as `AB6006`. Also reported inside Doctor's `AB7319` message for a Claude document that fails its schema or whose contract pattern could not be listed. | Correct the target document source so it satisfies its schema, then rebuild the artifact. | -| `AB6013` | error | `Artifact contains unsupported filesystem entry "".` — the tree holds a symlink or another entry that is neither a regular file nor a directory, or such an entry appeared between the initial and final inspection. | Remove unsupported filesystem entries and rebuild the artifact. | +| `AB6004` | error | `Artifact files do not match the manifest.`, the regular files on disk differ from the manifest file table (a path, byte length, mode, or SHA-256; a missing or unmanifested file). `Artifact file changed during validation: "".`, a file differed between the initial and final inspection, or between a validated staging tree and its re-check after `build` renamed it into place. `Artifact file table changed during validation.`, the final inspection could not be taken. | Rebuild the artifact so its file table and contents match the manifest. | +| `AB6005` | error | Primary compile-time form: `Compiled module "" keeps "" external () from ; a generated executable bundles everything but Node built-ins.` `` is the run-time load target; when an object-map external redirected the authored specifier, `, imported as "",` follows the type, a relative target that names no emitted asset of the artifact (or escapes it) ends `; it names no module emitted by this artifact.` instead, and a request kept under an external type that does not load a module (`var`, `global`, `this`, `window`, `assign`, `umd`, `amd`, `system`, `jsonp`, `promise`, `script`, …), even a Node built-in, ends `; external type reads a variable instead of loading a module.`, the compiler service lowered a host-pack surface or package-build entry (`dist/bin/*.js`, the Flight workers, or the `lib` entry) and Rspack kept something other than a Node built-in, `pnpapi`, or an emitted sibling of that artifact external, whatever spelling Rspack emitted (`import`, `require`, or its `createRequire` shim); `generatedPath` names the asset. An expression request (`import(expr)`, `require(expr)`) is outside the compiler's view: Rslib's profile leaves it verbatim without parsing it, so it is neither bundled nor external; so is a literal import marked `rspackIgnore`/`webpackIgnore`, which Rspack leaves verbatim with no module, external, or warning. The emitted-module walk reports both in a compiled module the record proves: `Generated JavaScript import from "" has a non-literal dynamic import.` and `Generated JavaScript import from "" loads "", which the compiler neither bundled nor recorded as an external; an import the build ignored is a run-time load outside the artifact.` (a literal request that is neither a Node built-in nor one of the record's externals for that file). MCP App view form (the `, imported as` clause applies to both forms): `Compiled MCP App view "mcp-apps/.html" keeps "" external () from ; a view inlines every module it loads.`, a browser document has no allowable external, so the view's Rsbuild compilation (which carries the same audit plugin) fails on any `ExternalModule`, whatever the hatch mapped it to. Residual walk forms, `Generated JavaScript import from "" .`, reported only for what the compiler cannot see: `has a non-literal dynamic import` for an expression `import()` in any emitted module, compiled ones included, since Rslib's profile leaves that form verbatim; and, for a module the compile evidence record does not prove, JavaScript the framework did not compile (`install.mjs`, a copied script), every module of an artifact without a record, and every module of a build whose `tools` hatch may have rewritten the emitted bytes (`coverage.rewritable`), `has invalid syntax` from a full parse, `uses unsupported specifier` or `uses invalid specifier` for a bare or malformed import, `cannot be read`, and the relative-target findings `is missing`, `resolves outside the artifact root`, `is not listed in the artifact manifest`, `does not resolve to a regular file`, `references invalid JSON`, and `uses unsupported target`. A compiled module the record covers with the same bytes is lexed for syntax and its literal imports are not resolved again, the compiler resolved them (bundled, built-in, or an emitted sibling). Prebuilt payloads are not walked; a `dist` finding names `dist/`. | Bundle every JavaScript dependency into the artifact, then rebuild it. | +| `AB6006` | error | `Generated JSON cannot be parsed.`, a `.json` file in the artifact is not valid JSON (prebuilt payload files are exempt). Doctor's Claude document lane reports the same code inside an `AB7319` message for a Claude bundle document that is unreadable or not valid JSON. | Regenerate the affected JSON document as valid JSON, then rebuild the artifact. | +| `AB6007` | error | `MCP manifest references missing generated server "".`, a root-level MCP manifest (pre-manifest pass) or a target's MCP manifest names a local server entry that the artifact does not contain. | Repair MCP manifest references to generated servers, then rebuild the artifact. | +| `AB6008` | error | `Artifact Agent Skills provenance does not match the pinned schema contract.`, the manifest's `agentSkills` schema SHA-256, source revision, or specification differs from the framework's pinned Agent Skills revision. | Rebuild the artifact with the pinned Agent Skills contract. | +| `AB6009` | error | `Artifact declares unknown target "".`, a manifest target is not registered in the target registry the validator was given (the project's configured registry for `build`, `validate --artifact`, and `dev`). | Rebuild the artifact with a registered target. | +| `AB6010` | error | `Artifact metadata and adapter identity for target "" do not match its registered contract.`, the manifest's per-target metadata or `builtInHost` adapter identity differs from the adapter registered under that name. | Rebuild the artifact with the current target registry. | +| `AB6011` | error | `Target "" is missing required document "".`, a document the target's artifact-validation contract marks required (for example a host manifest) is absent from the target namespace. Also reported inside Doctor's `AB7319` message for a missing required Claude bundle document. | Generate the required target document, then rebuild the artifact. | +| `AB6012` | error | `Target "" document "" is invalid for schema "" at : .`, a generated host document fails its vendored pinned schema (only the first issue is reported per document); unparsable documents are skipped here and reported as `AB6006`. Also reported inside Doctor's `AB7319` message for a Claude document that fails its schema or whose contract pattern could not be listed. | Correct the target document source so it satisfies its schema, then rebuild the artifact. | +| `AB6013` | error | `Artifact contains unsupported filesystem entry "".`, the tree holds a symlink or another entry that is neither a regular file nor a directory, or such an entry appeared between the initial and final inspection. | Remove unsupported filesystem entries and rebuild the artifact. | | `AB6014` | error | Ownership and layout: `Artifact file "" is outside declared target emitted layouts.` (a manifested file under a target that no emitted layout, hook manifest, MCP runtime manifest, validation contract, or prebuilt entry accounts for), `Artifact directory "" does not name a declared target namespace.` (a root directory that is not a manifest target), `Artifact directory "" is empty.`, `Declared target "" has no emitted namespace.`, or `Artifact directory changed during validation: "".` | Rebuild the artifact with files only in declared target namespaces. | | `AB6015` | error | Emitted Skill layout and frontmatter: a Skill document outside the canonical `skills//SKILL.md` layout, a Skill resource directory without its `SKILL.md`, Skill Markdown that cannot be read, does not start with YAML frontmatter, has invalid or schema-violating frontmatter (`Emitted Skill frontmatter .`), or whose frontmatter `name` differs from its directory name. | Restore canonical Skill Markdown and copied resources, then rebuild the artifact. | -| `AB6016` | error | `Emitted Skill reference "" escapes its Skill root.` or `Emitted Skill references missing regular resource "".` — a relative reference inside a `SKILL.md` body points outside the Skill directory or at a file the artifact does not contain as a regular file. | Copy every referenced Skill resource inside its Skill root, then rebuild the artifact. | -| `AB6017` | error | Target MCP manifest coherence (`generatedPath` is the target's MCP manifest): the manifest is not strict JSON or does not contain only modern supported servers; a server's runtime values cannot be resolved; a `cwd`, `command`, or `args` path escapes the target, references a missing or unmanifested file, or names a non-executable command file; a compiler-emitted MCP entry is referenced by no server or by more than one; `projections[host].documents.mcp` does not point at the target's MCP manifest; a launchable `executables.mcpServers[]` server declared for the target is missing from the target's document under its name; or the document's server disagrees with the launch record of the same name — it is not a stdio server, the first artifact-local path its command and arguments name is not the record's `entry`, or the record's `artifact` arguments do not follow the entry in the record's order (the host document and ` web` would start different bytes; the document may add flags and literal arguments around them). | Rebuild the artifact so every target MCP manifest references its exact compiler outputs. | +| `AB6016` | error | `Emitted Skill reference "" escapes its Skill root.` or `Emitted Skill references missing regular resource "".`, a relative reference inside a `SKILL.md` body points outside the Skill directory or at a file the artifact does not contain as a regular file. | Copy every referenced Skill resource inside its Skill root, then rebuild the artifact. | +| `AB6017` | error | Target MCP manifest coherence (`generatedPath` is the target's MCP manifest): the manifest is not strict JSON or does not contain only modern supported servers; a server's runtime values cannot be resolved; a `cwd`, `command`, or `args` path escapes the target, references a missing or unmanifested file, or names a non-executable command file; a compiler-emitted MCP entry is referenced by no server or by more than one; `projections[host].documents.mcp` does not point at the target's MCP manifest; a launchable `executables.mcpServers[]` server declared for the target is missing from the target's document under its name; or the document's server disagrees with the launch record of the same name. It is not a stdio server, the first artifact-local path its command and arguments name is not the record's `entry`, or the record's `artifact` arguments do not follow the entry in the record's order (the host document and ` web` would start different bytes; the document may add flags and literal arguments around them). | Rebuild the artifact so every target MCP manifest references its exact compiler outputs. | | `AB6018` | error | Hook coherence, judged from `agent-bundle.manifest.json` `executables.hooks[]`: a hook row selects an undeclared or hook-incompatible host, or references a missing or invalid wrapper; a host with hook rows is missing its native hook document, or that document is invalid for command enumeration; a manifest hook row does not map to exactly one native command, or a native command has no manifest row (or more than one). | Rebuild the artifact so native hook commands and the manifest's hook rows agree. | -| `AB6023` | error | `Artifact is missing required install surface "INSTALL.md".` — the selection includes a built-in host (`amp`, `claude`, `codex`, `cursor`, `portable`, judged by adapter identity, so an advanced registry's own adapter named like one requires nothing) but the composite root has no `INSTALL.md`; the surface is emitted once at the root, never per target. | Rebuild the artifact so the root carries its generated `INSTALL.md`. | -| `AB6024` | error | `Artifact is missing required install surface "install.mjs".` — the selection includes the shipped `cursor` or `portable` adapter (judged by adapter identity, like `AB6023`) but the composite root has no `install.mjs` (a root selecting only `claude` and/or `codex` requires none). | Rebuild the artifact so the root carries its generated `install.mjs`. | -| `AB6025` | error | `Plugin logo "" escapes the artifact for target "".` or `Plugin logo "" references missing artifact file "".` — a `plugin.json` `logo` string resolves outside the target directory or to a file the artifact does not contain. | Rebuild the artifact so every manifest-declared logo path copies into the deploy tree. | +| `AB6023` | error | `Artifact is missing required install surface "INSTALL.md".`, the selection includes a built-in host (`amp`, `claude`, `codex`, `cursor`, `portable`, judged by adapter identity, so an advanced registry's own adapter named like one requires nothing) but the composite root has no `INSTALL.md`; the surface is emitted once at the root, never per target. | Rebuild the artifact so the root carries its generated `INSTALL.md`. | +| `AB6024` | error | `Artifact is missing required install surface "install.mjs".`, the selection includes the shipped `cursor` or `portable` adapter (judged by adapter identity, like `AB6023`) but the composite root has no `install.mjs` (a root selecting only `claude` and/or `codex` requires none). | Rebuild the artifact so the root carries its generated `install.mjs`. | +| `AB6025` | error | `Plugin logo "" escapes the artifact for target "".` or `Plugin logo "" references missing artifact file "".`, a `plugin.json` `logo` string resolves outside the target directory or to a file the artifact does not contain. | Rebuild the artifact so every manifest-declared logo path copies into the deploy tree. | | `AB6039` | error | The artifact or package-only compile evidence sidecar is unreadable, invalid, judged under another policy, does not cover a compiled file, describes different bytes, names a path the corresponding file table does not list as compiled, misclassifies an external, or references a missing compiled sibling. An MCP App view also fails when its record names any external. A record with any `AB6039` finding, or with `coverage.rewritable` set, proves nothing to the emitted-module walk, which then parses every module in full. | Rebuild the artifact or generated npm root so its compile evidence describes the emitted files; do not edit generated evidence or outputs. | ## Workbench artifact inspection (`AB6200`–`AB6202`) @@ -1813,8 +1813,8 @@ diagnostics in the response body. | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB6200` | error | `Artifact inspection could not validate the published artifact.` — the strict artifact validator threw over the epoch; `Artifact file has no manifest provenance record.` — an internal defense-in-depth invariant failed after parsing (an on-disk missing row is rejected first as `AB6001`); `Artifact file provenance references an unknown project source input.` — an output's `sourceInputs` name an input the manifest project does not declare; `Artifact manifest project inputs are invalid.` — the manifest's project inputs are structurally invalid. An epoch whose validation merely reports diagnostics is refused with those diagnostics instead of this code. | Rebuild the epoch from a project whose artifact validates cleanly. | -| `AB6201` | error | `Artifact inspection could not release every acquired epoch reference.` — releasing an epoch reference after an inspection or diff failed. | None in the project: the failure is internal to the development server's epoch bookkeeping. | +| `AB6200` | error | `Artifact inspection could not validate the published artifact.`, the strict artifact validator threw over the epoch; `Artifact file has no manifest provenance record.`, an internal defense-in-depth invariant failed after parsing (an on-disk missing row is rejected first as `AB6001`); `Artifact file provenance references an unknown project source input.`, an output's `sourceInputs` name an input the manifest project does not declare; `Artifact manifest project inputs are invalid.`, the manifest's project inputs are structurally invalid. An epoch whose validation merely reports diagnostics is refused with those diagnostics instead of this code. | Rebuild the epoch from a project whose artifact validates cleanly. | +| `AB6201` | error | `Artifact inspection could not release every acquired epoch reference.`, releasing an epoch reference after an inspection or diff failed. | None in the project: the failure is internal to the development server's epoch bookkeeping. | | `AB6202` | error | Runtime metadata derived from the validated snapshot is unsafe: an MCP server's `entryPaths` name a file outside its target or absent from the manifest (`Validated MCP evidence references an unmanifested target file.`), or another runtime-evidence check named in the message failed. | Rebuild the epoch so its MCP runtime evidence references manifested target files. | ## Host installation, uninstallation, and project preparation (`AB7000`–`AB7004`) @@ -1832,10 +1832,10 @@ the uninstall refusals `AB7007`–`AB7009`, have their own sections above. | Code | Severity | Meaning | Recovery | | --- | --- | --- | --- | -| `AB7000` | error | Install/uninstall: `Unsupported install host .` / `Unsupported uninstall host .` — the exhaustive host switch received a host that is not `amp`, `claude`, `codex`, or `cursor`. Project preparation: `Unable to load project source.` — evaluating the configuration module or discovering source threw before validation. | Install: pass `--host amp`, `claude`, `codex`, or `cursor`. Preparation: fix the Agent Bundle configuration and source files, then inspect again. | -| `AB7001` | error | Install/uninstall/doctor: the bundle identity or authoritative file inventory is unreadable from `agent-bundle.manifest.json` — no manifest directly under the `--from` directory (the composite root is every selected host's bundle root, so `/` is never probed and host documents are never read for identity); a manifest that is not the canonical `manifestVersion: 5` document (the message carries the parser's reason); a manifest with no projection whose `builtInHost` is the requested host (identity is the shipped adapter, never the selected name), whose projection has neither its required `documents.plugin` nor Amp `documents.entry`, or whose `documents.entry` / `documents.plugin` / `documents.marketplace` pointer names a file the root does not contain; a `files[]` row whose path is missing or whose size, digest, bytes, or executable state is invalid after installation (a declared package bin must remain executable; a file the manifest does not declare executable must remain non-executable; another manifest executable may have lost its bit while being packed from a filesystem without executable modes); a Cursor or Amp `application.name` that is not a safe local plugin name; a Claude or Codex projection with no `marketplace.name`. `install` restores manifest modes before copying an npm-installed artifact into a host, while Doctor only compares. Project preparation: `Unable to validate project source.`, `Unable to normalize project source.`, `Unable to validate normalized project.`, or `Unable to create project context.` — the source validator, normalizer, adapter planner, or project-context factory threw; `inspectProject` adds `Unable to prepare inspection plans.` and, for `inspect --bundler`, `Unable to compose the bundler inspection: ` — loading entries, generating the declaration tsconfig, or lowering and asserting the build's own Rslib/Rsbuild configuration failed. The reason carries the underlying source, project-tsconfig, toolchain, or invariant error, including a `tools` value the build would refuse. | Install: point `--from` at the unchanged composite root `agent-bundle build` wrote, rebuilt with the host among `targets`; if a listed file is missing or changed, rebuild or restore that file from the matching artifact. Preparation: fix normalized project configuration and source references, then inspect again. Bundler inspection: fix the source, project tsconfig, toolchain, or refused `tools` value named by the reason. | -| `AB7002` | error | Install/uninstall: ` is not installed or is not available on PATH.`, `Cursor is not installed in "".` / `Cursor home "" is not a directory.`, or `git` is missing for `--mode marketplace`. Project preparation: `Unable to prepare project paths.` — the project root or a configured output root could not be resolved inside the project. | Install: install the host CLI the message names; for the `git` refusal, install git or use `--mode local`. Preparation: ensure the project root and configured output roots are readable and remain inside the project root, then inspect again. | -| `AB7003` | error | Install/uninstall scope and mode refusals: `--mode` on a host other than `cursor`; `--scope` other than `user` for Codex or Cursor; Amp `--scope local` instead of `project` or `user`; `--mode marketplace` without `.cursor-plugin/plugin.json` or with bundle-internal Git metadata. Project preparation: `Unable to snapshot project source.` — the source snapshot could not be taken, including when a discovered identity is not a relocatable POSIX path (a POSIX filename containing `\`, or another segment the manifest cannot carry). | Install: use a documented host scope, drop `--mode` for non-Cursor hosts, or — as the message says — stage a Cursor Plugin bundle without `.git`, or use `--mode local`. Preparation: ensure project source files and ignore rules are readable, remain inside the project root, and use relocatable POSIX path segments, then inspect again. | +| `AB7000` | error | Install/uninstall: `Unsupported install host .` / `Unsupported uninstall host .`, the exhaustive host switch received a host that is not `amp`, `claude`, `codex`, or `cursor`. Project preparation: `Unable to load project source.`, evaluating the configuration module or discovering source threw before validation. | Install: pass `--host amp`, `claude`, `codex`, or `cursor`. Preparation: fix the Agent Bundle configuration and source files, then inspect again. | +| `AB7001` | error | Install/uninstall/doctor: the bundle identity or authoritative file inventory is unreadable from `agent-bundle.manifest.json`, no manifest directly under the `--from` directory (the composite root is every selected host's bundle root, so `/` is never probed and host documents are never read for identity); a manifest that is not the canonical `manifestVersion: 5` document (the message carries the parser's reason); a manifest with no projection whose `builtInHost` is the requested host (identity is the shipped adapter, never the selected name), whose projection has neither its required `documents.plugin` nor Amp `documents.entry`, or whose `documents.entry` / `documents.plugin` / `documents.marketplace` pointer names a file the root does not contain; a `files[]` row whose path is missing or whose size, digest, bytes, or executable state is invalid after installation (a declared package bin must remain executable; a file the manifest does not declare executable must remain non-executable; another manifest executable may have lost its bit while being packed from a filesystem without executable modes); a Cursor or Amp `application.name` that is not a safe local plugin name; a Claude or Codex projection with no `marketplace.name`. `install` restores manifest modes before copying an npm-installed artifact into a host, while Doctor only compares. Project preparation: `Unable to validate project source.`, `Unable to normalize project source.`, `Unable to validate normalized project.`, or `Unable to create project context.`, the source validator, normalizer, adapter planner, or project-context factory threw; `inspectProject` adds `Unable to prepare inspection plans.` and, for `inspect --bundler`, `Unable to compose the bundler inspection: `, loading entries, generating the declaration tsconfig, or lowering and asserting the build's own Rslib/Rsbuild configuration failed. The reason carries the underlying source, project-tsconfig, toolchain, or invariant error, including a `tools` value the build would refuse. | Install: point `--from` at the unchanged composite root `agent-bundle build` wrote, rebuilt with the host among `targets`; if a listed file is missing or changed, rebuild or restore that file from the matching artifact. Preparation: fix normalized project configuration and source references, then inspect again. Bundler inspection: fix the source, project tsconfig, toolchain, or refused `tools` value named by the reason. | +| `AB7002` | error | Install/uninstall: ` is not installed or is not available on PATH.`, `Cursor is not installed in "".` / `Cursor home "" is not a directory.`, or `git` is missing for `--mode marketplace`. Project preparation: `Unable to prepare project paths.`, the project root or a configured output root could not be resolved inside the project. | Install: install the host CLI the message names; for the `git` refusal, install git or use `--mode local`. Preparation: ensure the project root and configured output roots are readable and remain inside the project root, then inspect again. | +| `AB7003` | error | Install/uninstall scope and mode refusals: `--mode` on a host other than `cursor`; `--scope` other than `user` for Codex or Cursor; Amp `--scope local` instead of `project` or `user`; `--mode marketplace` without `.cursor-plugin/plugin.json` or with bundle-internal Git metadata. Project preparation: `Unable to snapshot project source.`, the source snapshot could not be taken, including when a discovered identity is not a relocatable POSIX path (a POSIX filename containing `\`, or another segment the manifest cannot carry). | Install: use a documented host scope, drop `--mode` for non-Cursor hosts, or, as the message says, stage a Cursor Plugin bundle without `.git`, or use `--mode local`. Preparation: ensure project source files and ignore rules are readable, remain inside the project root, and use relocatable POSIX path segments, then inspect again. | | `AB7004` | error | Install/uninstall command and safety failures: ` plugin failed: ` (a host CLI verb exited nonzero); ` plugin list --json` was unusable when `--replace` or an uninstall needed it; an installed copy could not be compared and `--replace` was not given; a Codex replacement whose plugin list row is `enabled: false` or omits `enabled` (the native plugin CLI has no qualified settings-preserving update API, and native `plugin add` would set enabled to true); a rollback after a failed install also failed (the message lists the host verbs to run by hand); a Cursor marketplace `git` step failed or the committed tree differs from the staged bytes; or any non-diagnostic error thrown by a Cursor installer. `inspectProject`: `Requested inspection target "" is not selected for this project.` | Install: read the host's detail in the message, then rerun (with `--replace` where the message says so). For a Codex disabled/unknown-enablement refusal, enable the plugin in Codex first. Inspection: choose a target selected by the project configuration, then inspect again. | ## Development server (`AB80xx`) @@ -1843,7 +1843,7 @@ the uninstall refusals `AB7007`–`AB7009`, have their own sections above. `agent-bundle dev` serves the Workbench and its JSON API from one loopback foreground server (`dev/foreground-server.ts`) whose route modules under `dev/**` share the helpers in `dev/http.ts`. Every refusal is a request -diagnostic — `{ code, message, status }` — written as the JSON body +diagnostic, `{ code, message, status }`, written as the JSON body `{ "diagnostic": { "code", "message" } }` with the listed HTTP status; a thrown request diagnostic anywhere in a handler is written the same way. The codes are fixed per route module, so the same code covers every message the @@ -1858,128 +1858,128 @@ foreground server accepts. | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8000` | — | `ForegroundServerError` thrown while constructing the server: the bind host is not `127.0.0.1` or `::1`, the port is not a safe TCP port number, the instance ID is empty, longer than 128 characters, or not trimmed, or a `--workbench-dev-origin` is not a loopback `http(s)` origin such as `http://localhost:3000`. | Pass a valid `--port` and loopback `--workbench-dev-origin` to `agent-bundle dev`; host and instance ID are programmatic `ForegroundServer` options. | -| `AB8001` | 400 | `Request body must be valid JSON.` — a JSON route body did not parse (or, through `readJsonBody`, contained duplicate keys). | Send a single well-formed JSON document. | -| `AB8002` | 400 | `Request body may contain only an optional paths array.` — the `POST /api/project/rebuild` body is not an object, has a key other than `paths`, or lists a value that is not a project-relative path. | Send `{}` or `{ "paths": ["", …] }`. | -| `AB8003` | 403 | `Request origin is not this foreground server.` — a browser route received an `Origin` header that is neither the foreground origin nor a listed `--workbench-dev-origin`, or no `Origin` and no `sec-fetch-site: same-origin`; on `/mcp`, an `Origin` that is not exactly the foreground origin. Also emitted by `agent-bundle serve-app` (`Request origin is not this MCP App host.`). | Open the Workbench at the foreground URL, or start `agent-bundle dev` with `--workbench-dev-origin ` to allow a separate dev-server origin. | -| `AB8004` | 403 | `A valid same-session token is required.` — a mutating browser route lacks the `x-agent-bundle-session` header matching this server's session token; `A valid foreground session cookie is required.` — the `/api/project/events` stream lacks the per-origin session cookie issued by `/api/project/session`. Also emitted by `agent-bundle serve-app` (`A valid MCP App host token is required.`). | Reload the Workbench so it re-bootstraps its session from `/api/project/session`; a token or cookie issued by an earlier server instance no longer matches. | -| `AB8005` | 400 | `Asset path is not valid.` — a static asset request has no leading `/`, a segment that does not URL-decode, or a segment that is empty, `.`, `..`, or contains `/`, `\`, or NUL. | Request the asset by its plain path under the Workbench root. | +| `AB8000` | none | `ForegroundServerError` thrown while constructing the server: the bind host is not `127.0.0.1` or `::1`, the port is not a safe TCP port number, the instance ID is empty, longer than 128 characters, or not trimmed, or a `--workbench-dev-origin` is not a loopback `http(s)` origin such as `http://localhost:3000`. | Pass a valid `--port` and loopback `--workbench-dev-origin` to `agent-bundle dev`; host and instance ID are programmatic `ForegroundServer` options. | +| `AB8001` | 400 | `Request body must be valid JSON.`, a JSON route body did not parse (or, through `readJsonBody`, contained duplicate keys). | Send a single well-formed JSON document. | +| `AB8002` | 400 | `Request body may contain only an optional paths array.`, the `POST /api/project/rebuild` body is not an object, has a key other than `paths`, or lists a value that is not a project-relative path. | Send `{}` or `{ "paths": ["", …] }`. | +| `AB8003` | 403 | `Request origin is not this foreground server.`, a browser route received an `Origin` header that is neither the foreground origin nor a listed `--workbench-dev-origin`, or no `Origin` and no `sec-fetch-site: same-origin`; on `/mcp`, an `Origin` that is not exactly the foreground origin. Also emitted by `agent-bundle serve-app` (`Request origin is not this MCP App host.`). | Open the Workbench at the foreground URL, or start `agent-bundle dev` with `--workbench-dev-origin ` to allow a separate dev-server origin. | +| `AB8004` | 403 | `A valid same-session token is required.`, a mutating browser route lacks the `x-agent-bundle-session` header matching this server's session token; `A valid foreground session cookie is required.`, the `/api/project/events` stream lacks the per-origin session cookie issued by `/api/project/session`. Also emitted by `agent-bundle serve-app` (`A valid MCP App host token is required.`). | Reload the Workbench so it re-bootstraps its session from `/api/project/session`; a token or cookie issued by an earlier server instance no longer matches. | +| `AB8005` | 400 | `Asset path is not valid.`, a static asset request has no leading `/`, a segment that does not URL-decode, or a segment that is empty, `.`, `..`, or contains `/`, `\`, or NUL. | Request the asset by its plain path under the Workbench root. | | `AB8006` | 400 | `/api/project/events` cursor: `Project event cursor must be singular.` (more than one `after` query value), `… must be a non-negative integer.`, or `… must not be ahead of the project event stream.` (a `Last-Event-ID` header or `after` value beyond the latest sequence). | Reconnect with a single `after` value or `Last-Event-ID` no later than the last sequence received, or none to replay from the start. | -| `AB8007` | 404 / 405 / 500 | `Route was not found.` — no asset at the path, or `/mcp` when the Agent API is not composed; `Route does not accept this method.` — a route received a method it does not serve; `Request could not be completed.` — a handler threw something other than a request diagnostic. | Check the method and path; for a 500, read the dev-server log for the underlying error. | -| `AB8008` | 400 | `Request host is not this foreground server.` — the `Host` header does not name this server's loopback URL. | Address the server by the URL `agent-bundle dev` printed. | -| `AB8009` | 415 | `Request body must use application/json.` — a JSON route received a body without an `application/json` content type. | Send `content-type: application/json`. | -| `AB8010` | 413 | `Request body exceeds 64 KiB.` — the default `readBody` bound; the runtime MCP routes apply the same bound. Playground routes raise a 1 MiB bound under `AB8085`. | Send a smaller body. | -| `AB8011` | 404 | `Skill workbench service is not available.` — a `/api/skills/**` route was requested but the server was composed without the Skill document service. | Nothing to fix in the project; the Skill pages need a server composed with the Skill service. | -| `AB8012` | 400 | `Skill route path is not valid.` — a `/api/skills/**` path does not match the source or generated Skill tree, document, or resource shapes, or a segment does not decode. | Use the Skill links the Workbench renders. | +| `AB8007` | 404 / 405 / 500 | `Route was not found.`, no asset at the path, or `/mcp` when the Agent API is not composed; `Route does not accept this method.`, a route received a method it does not serve; `Request could not be completed.`, a handler threw something other than a request diagnostic. | Check the method and path; for a 500, read the dev-server log for the underlying error. | +| `AB8008` | 400 | `Request host is not this foreground server.`, the `Host` header does not name this server's loopback URL. | Address the server by the URL `agent-bundle dev` printed. | +| `AB8009` | 415 | `Request body must use application/json.`, a JSON route received a body without an `application/json` content type. | Send `content-type: application/json`. | +| `AB8010` | 413 | `Request body exceeds 64 KiB.`, the default `readBody` bound; the runtime MCP routes apply the same bound. Playground routes raise a 1 MiB bound under `AB8085`. | Send a smaller body. | +| `AB8011` | 404 | `Skill workbench service is not available.`, a `/api/skills/**` route was requested but the server was composed without the Skill document service. | Nothing to fix in the project; the Skill pages need a server composed with the Skill service. | +| `AB8012` | 400 | `Skill route path is not valid.`, a `/api/skills/**` path does not match the source or generated Skill tree, document, or resource shapes, or a segment does not decode. | Use the Skill links the Workbench renders. | ### MCP sessions (`/api/mcp/sessions/**`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8013` | 400 | `MCP session route path is not valid.` — `/api/mcp` or a path under it that is not `sessions`, `sessions/`, or `sessions//` with a known operation (`connection`, `catalog`, `config`, `operations`, `trace`, `stream`, `restart`, `cancel`), or a segment that does not decode; the App routes claim `sessions//apps` and `apps/**` first. | Use the session routes the Workbench MCP page issues. | -| `AB8014` | 404 / 503 | `MCP session routes are not available.` — 404 when the server was composed without the MCP session service, 503 after the routes closed for shutdown. | Restart `agent-bundle dev`. | -| `AB8015` | 404 | `MCP session is not available.` — the session id is unknown, or the session was closed while the request was in flight. | Open a new session from the MCP page. | -| `AB8016` | 400 | `MCP session request has an invalid shape.` — the request body does not match the operation's expected fields. | Send the fields the operation defines. | -| `AB8017` | 400 / 409 | `MCP session trace cursor is not valid.` (400) — the trace `after` cursor is not a single non-negative safe integer; `MCP session trace cursor is ahead of the current trace.` (409). | Resume from a cursor no later than the trace's current sequence. | -| `AB8018` | 409 | `MCP session epoch is no longer available; the project changed underneath the session.` — the epoch the session was opened against is no longer available after the project changed. | Open a new session against the current epoch. | -| `AB8019` | 400 / 502 | `MCP session could not be opened.` (400, on create) or `MCP session operation could not be completed.` (502) — the service threw something the route does not map to a more specific code. | Read the dev-server log for the underlying error, then retry. | +| `AB8013` | 400 | `MCP session route path is not valid.`, `/api/mcp` or a path under it that is not `sessions`, `sessions/`, or `sessions//` with a known operation (`connection`, `catalog`, `config`, `operations`, `trace`, `stream`, `restart`, `cancel`), or a segment that does not decode; the App routes claim `sessions//apps` and `apps/**` first. | Use the session routes the Workbench MCP page issues. | +| `AB8014` | 404 / 503 | `MCP session routes are not available.`, 404 when the server was composed without the MCP session service, 503 after the routes closed for shutdown. | Restart `agent-bundle dev`. | +| `AB8015` | 404 | `MCP session is not available.`, the session id is unknown, or the session was closed while the request was in flight. | Open a new session from the MCP page. | +| `AB8016` | 400 | `MCP session request has an invalid shape.`, the request body does not match the operation's expected fields. | Send the fields the operation defines. | +| `AB8017` | 400 / 409 | `MCP session trace cursor is not valid.` (400), the trace `after` cursor is not a single non-negative safe integer; `MCP session trace cursor is ahead of the current trace.` (409). | Resume from a cursor no later than the trace's current sequence. | +| `AB8018` | 409 | `MCP session epoch is no longer available; the project changed underneath the session.`, the epoch the session was opened against is no longer available after the project changed. | Open a new session against the current epoch. | +| `AB8019` | 400 / 502 | `MCP session could not be opened.` (400, on create) or `MCP session operation could not be completed.` (502), the service threw something the route does not map to a more specific code. | Read the dev-server log for the underlying error, then retry. | ### MCP App previews (`/api/mcp/apps/**`, `/api/mcp/sessions//apps`, `/api/runtime/apps/**`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8020` | 400 / 404 | `MCP App route path is not valid.` — an App route whose binding id or operation segment is missing or does not decode, or an unknown operation under `/api/mcp/apps//`. `agent-bundle serve-app` answers unknown paths with `Not found.` (404) under the same code. | Use the App routes the Workbench MCP page issues. | -| `AB8021` | 400 | `MCP App request has an invalid shape.` — the request body does not match the operation's expected fields. | Send the fields the operation defines. | -| `AB8022` | 404 / 410 / 503 | `MCP App routes are not available.` — 404 without the preview service, 503 after shutdown; `MCP App preview is not available.` (404) — the binding id is unknown; `Runtime MCP App preview was revoked.` (410) — the runtime binding has been revoked. `agent-bundle serve-app` reports `MCP App host is not ready.` (503) before its host finishes starting. | Re-open the App preview; after 410 the page must create a new binding. | -| `AB8023` | 404 / 409 / 413 / 502 | `MCP App operation could not be completed.` (502) — an unmapped service failure; `Runtime MCP App operation exceeded its 30 second deadline.` (502); `Runtime MCP App operation response could not be encoded.` (502) or `… exceeds its transport bound.` (413) — the result of a runtime App operation could not cross the bounded host-to-App channel. On `/web//`: `MCP App could not be opened.` (502) — the launch, opening call, or page render failed; `Target "…" is not a declared projection that launches MCP server …` (404) — an invalid `?target=`, never a fallback; `No declared projection of this artifact launches MCP server …` (404); `The declared projections launch MCP server … differently; pick one explicitly with ?target=<…>.` (409). | Read the dev-server log; shrink or split the App operation result if the bound was hit; on `/web`, pass a `?target=` the message names. | +| `AB8020` | 400 / 404 | `MCP App route path is not valid.`, an App route whose binding id or operation segment is missing or does not decode, or an unknown operation under `/api/mcp/apps//`. `agent-bundle serve-app` answers unknown paths with `Not found.` (404) under the same code. | Use the App routes the Workbench MCP page issues. | +| `AB8021` | 400 | `MCP App request has an invalid shape.`, the request body does not match the operation's expected fields. | Send the fields the operation defines. | +| `AB8022` | 404 / 410 / 503 | `MCP App routes are not available.`, 404 without the preview service, 503 after shutdown; `MCP App preview is not available.` (404), the binding id is unknown; `Runtime MCP App preview was revoked.` (410), the runtime binding has been revoked. `agent-bundle serve-app` reports `MCP App host is not ready.` (503) before its host finishes starting. | Re-open the App preview; after 410 the page must create a new binding. | +| `AB8023` | 404 / 409 / 413 / 502 | `MCP App operation could not be completed.` (502), an unmapped service failure; `Runtime MCP App operation exceeded its 30 second deadline.` (502); `Runtime MCP App operation response could not be encoded.` (502) or `… exceeds its transport bound.` (413), the result of a runtime App operation could not cross the bounded host-to-App channel. On `/web//`: `MCP App could not be opened.` (502), the launch, opening call, or page render failed; `Target "…" is not a declared projection that launches MCP server …` (404), an invalid `?target=`, never a fallback; `No declared projection of this artifact launches MCP server …` (404); `The declared projections launch MCP server … differently; pick one explicitly with ?target=<…>.` (409). | Read the dev-server log; shrink or split the App operation result if the bound was hit; on `/web`, pass a `?target=` the message names. | ### Hook playground (`/api/hooks/**`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8030` | 400 | `Hook playground route path is not valid.` — an unknown path under `/api/hooks/` or a segment that does not decode. | Use the routes the Workbench Hooks page issues. | -| `AB8031` | 404 / 503 | `Hook playground routes are not available.` — 404 without the hook playground service, 503 once shutdown has begun. | Restart `agent-bundle dev`. | -| `AB8032` | 400 | `Hook playground request has an invalid shape.` — the request body does not match the operation's expected fields. | Send the fields the operation defines. | -| `AB8033` | 502 | `Hook playground operation could not be completed.` — the service threw something other than a request diagnostic. | Read the dev-server log for the underlying error, then retry. | -| `AB8034` | — | `HookPlaygroundCloseError`: `Hook playground routes could not drain every in-flight operation.` — thrown from the routes' `close()` during server shutdown when a cancelled operation failed to settle; the foreground server records it as a `hook-playground` close failure rather than an HTTP response. `failures` lists each operation and its error. | Nothing to fix in the project; inspect the listed errors if shutdown reports them. | +| `AB8030` | 400 | `Hook playground route path is not valid.`, an unknown path under `/api/hooks/` or a segment that does not decode. | Use the routes the Workbench Hooks page issues. | +| `AB8031` | 404 / 503 | `Hook playground routes are not available.`, 404 without the hook playground service, 503 once shutdown has begun. | Restart `agent-bundle dev`. | +| `AB8032` | 400 | `Hook playground request has an invalid shape.`, the request body does not match the operation's expected fields. | Send the fields the operation defines. | +| `AB8033` | 502 | `Hook playground operation could not be completed.`, the service threw something other than a request diagnostic. | Read the dev-server log for the underlying error, then retry. | +| `AB8034` | none | `HookPlaygroundCloseError`: `Hook playground routes could not drain every in-flight operation.`, thrown from the routes' `close()` during server shutdown when a cancelled operation failed to settle; the foreground server records it as a `hook-playground` close failure rather than an HTTP response. `failures` lists each operation and its error. | Nothing to fix in the project; inspect the listed errors if shutdown reports them. | ### Prompt playground (`/api/playground/**`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8040` | 400 | `Playground route path is not valid.` — an unknown path under `/api/playground/` or a segment that does not decode. | Use the routes the Workbench Playground page issues. | -| `AB8041` | 404 / 503 | `Playground routes are not available.` — 404 without the playground service, 503 after the routes closed. | Restart `agent-bundle dev`. | -| `AB8042` | 400 | `Playground request has an invalid shape.` — the request body or query does not match the operation's expected fields. | Send the fields the operation defines. | -| `AB8043` | 502 | `Playground operation could not be completed.` — the service threw something the route does not map to a `PlaygroundServiceError` code below. | Read the dev-server log for the underlying error, then retry. | -| `AB8044` | 404 | `Playground session was not found.` — the store holds no session with that id. | Open a session this project recorded, or start a new one. | -| `AB8045` | 409 | `Playground session already exists.` — the store already holds a session with the requested id. | Create the session with a fresh id. | -| `AB8046` | 409 | `Playground session is already finalized.` — a write against a session the store has finalized. | Start a new session. | -| `AB8047` | 409 | `Playground session is owned by another writer.` — the session's owner lock is held by another foreground service, could not be acquired, or changed during admission or cleanup. | Continue in the `agent-bundle dev` instance that owns the session, or start a new one. | -| `AB8048` | 409 | `Playground cursor is ahead of persisted history.` — a replay or subscription cursor beyond the session's persisted history. | Resume from a cursor no later than the persisted history. | -| `AB8049` | 400 | `Playground cursor is not valid.` — the `after` cursor is not a single non-negative safe integer. | Send one non-negative integer cursor. | -| `AB8050` | 400 | `Playground request has an invalid value.` — a field the route accepted is refused by the store: not a nonempty string, not JSON-compatible (cycles, accessors, non-finite numbers), an unsupported event source, or an outcome or event that is not an object. | Send JSON-compatible values with the shapes the operation defines. | -| `AB8051` | 400 | `Playground session id is not valid.` — the id is not a path-safe identifier. | Use the session id the create response returned. | -| `AB8052` | 400 | `A durable playground outcome is required first.` — promoting a session to a draft eval requires a finalized or closed session with a recorded outcome; a finalized session missing its outcome reports here too. | Let the session finalize with an outcome before promoting it. | -| `AB8053` | 400 | `Playground values may not carry provider credentials.` — a record to be persisted contains provider credential material. | Remove the credential material from the value. | -| `AB8054` | 503 | `Playground service is closed.` — the store is shutting down. | Restart `agent-bundle dev`. | -| `AB8055` | 500 | `Playground store is corrupt.` — the store's owner lock is malformed or invalid, or its pending index could not be pinned safely. | Remove or repair the store under `.agent-bundle/playground`, then restart `agent-bundle dev`. | -| `AB8056` | 500 | `Playground storage root is not valid.` — `.agent-bundle/playground` is not an absolute, project-contained real directory (a symbolic link, a root outside the project, or a session or index root resolving outside the storage root). | Restore `.agent-bundle/playground` as a real directory inside the project root. | -| `AB8057` | 409 | `Playground session belongs to a different project.` — the persisted session records another project's identity. | Open the session from the project that created it. | +| `AB8040` | 400 | `Playground route path is not valid.`, an unknown path under `/api/playground/` or a segment that does not decode. | Use the routes the Workbench Playground page issues. | +| `AB8041` | 404 / 503 | `Playground routes are not available.`, 404 without the playground service, 503 after the routes closed. | Restart `agent-bundle dev`. | +| `AB8042` | 400 | `Playground request has an invalid shape.`, the request body or query does not match the operation's expected fields. | Send the fields the operation defines. | +| `AB8043` | 502 | `Playground operation could not be completed.`, the service threw something the route does not map to a `PlaygroundServiceError` code below. | Read the dev-server log for the underlying error, then retry. | +| `AB8044` | 404 | `Playground session was not found.`, the store holds no session with that id. | Open a session this project recorded, or start a new one. | +| `AB8045` | 409 | `Playground session already exists.`, the store already holds a session with the requested id. | Create the session with a fresh id. | +| `AB8046` | 409 | `Playground session is already finalized.`, a write against a session the store has finalized. | Start a new session. | +| `AB8047` | 409 | `Playground session is owned by another writer.`, the session's owner lock is held by another foreground service, could not be acquired, or changed during admission or cleanup. | Continue in the `agent-bundle dev` instance that owns the session, or start a new one. | +| `AB8048` | 409 | `Playground cursor is ahead of persisted history.`, a replay or subscription cursor beyond the session's persisted history. | Resume from a cursor no later than the persisted history. | +| `AB8049` | 400 | `Playground cursor is not valid.`, the `after` cursor is not a single non-negative safe integer. | Send one non-negative integer cursor. | +| `AB8050` | 400 | `Playground request has an invalid value.`, a field the route accepted is refused by the store: not a nonempty string, not JSON-compatible (cycles, accessors, non-finite numbers), an unsupported event source, or an outcome or event that is not an object. | Send JSON-compatible values with the shapes the operation defines. | +| `AB8051` | 400 | `Playground session id is not valid.`, the id is not a path-safe identifier. | Use the session id the create response returned. | +| `AB8052` | 400 | `A durable playground outcome is required first.`, promoting a session to a draft eval requires a finalized or closed session with a recorded outcome; a finalized session missing its outcome reports here too. | Let the session finalize with an outcome before promoting it. | +| `AB8053` | 400 | `Playground values may not carry provider credentials.`, a record to be persisted contains provider credential material. | Remove the credential material from the value. | +| `AB8054` | 503 | `Playground service is closed.`, the store is shutting down. | Restart `agent-bundle dev`. | +| `AB8055` | 500 | `Playground store is corrupt.`, the store's owner lock is malformed or invalid, or its pending index could not be pinned safely. | Remove or repair the store under `.agent-bundle/playground`, then restart `agent-bundle dev`. | +| `AB8056` | 500 | `Playground storage root is not valid.`, `.agent-bundle/playground` is not an absolute, project-contained real directory (a symbolic link, a root outside the project, or a session or index root resolving outside the storage root). | Restore `.agent-bundle/playground` as a real directory inside the project root. | +| `AB8057` | 409 | `Playground session belongs to a different project.`, the persisted session records another project's identity. | Open the session from the project that created it. | ### Artifact epochs (`/api/artifacts/**`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8060` | 400 | `Artifact route path is not valid.` — a path under `/api/artifacts/` other than `diff` or `epochs/`, or a segment that does not decode. | Use the artifact links the Workbench renders. | -| `AB8061` | 404 / 503 | `Artifact routes are not available.` — 404 without the inspection service, 503 after the routes closed. | Restart `agent-bundle dev`. | -| `AB8062` | 400 | `Artifact request has an invalid shape.` — `/api/artifacts/diff` lacks exactly one nonempty `base` and `candidate` query value or carries other keys; an epoch request carries a query string. | Request `/api/artifacts/diff?base=&candidate=` or `/api/artifacts/epochs/` with no query. | -| `AB8063` | 502 | `Artifact inspection could not be completed.` — the service threw something other than an inspection or epoch-store error. | Read the dev-server log for the underlying error, then retry. | -| `AB8064` | 422 | `Artifact epoch failed validation.` — the epoch's artifact reports validation diagnostics, or the inspection service refused it with `AB6200`; the response body carries those diagnostics. | Fix the reported artifact diagnostics; the next rebuild publishes a valid epoch. | -| `AB8065` | 422 | `Artifact runtime metadata is not valid.` — the inspection service refused the epoch with `AB6202`. | Rebuild the epoch so its MCP runtime evidence references manifested target files. | -| `AB8066` | 500 | `Artifact epoch reference could not be released.` — the inspection service reported `AB6201`. | None in the project: the failure is internal to the development server's epoch bookkeeping. | -| `AB8067` | 404 | `Artifact epoch was not found.` — the epoch id names no published epoch. | Pick an epoch from the current epoch list. | -| `AB8068` | 400 | `Artifact epoch id is not valid.` — the epoch store rejected the id's shape. | Use an epoch id the Workbench lists. | +| `AB8060` | 400 | `Artifact route path is not valid.`, a path under `/api/artifacts/` other than `diff` or `epochs/`, or a segment that does not decode. | Use the artifact links the Workbench renders. | +| `AB8061` | 404 / 503 | `Artifact routes are not available.`, 404 without the inspection service, 503 after the routes closed. | Restart `agent-bundle dev`. | +| `AB8062` | 400 | `Artifact request has an invalid shape.`, `/api/artifacts/diff` lacks exactly one nonempty `base` and `candidate` query value or carries other keys; an epoch request carries a query string. | Request `/api/artifacts/diff?base=&candidate=` or `/api/artifacts/epochs/` with no query. | +| `AB8063` | 502 | `Artifact inspection could not be completed.`, the service threw something other than an inspection or epoch-store error. | Read the dev-server log for the underlying error, then retry. | +| `AB8064` | 422 | `Artifact epoch failed validation.`, the epoch's artifact reports validation diagnostics, or the inspection service refused it with `AB6200`; the response body carries those diagnostics. | Fix the reported artifact diagnostics; the next rebuild publishes a valid epoch. | +| `AB8065` | 422 | `Artifact runtime metadata is not valid.`, the inspection service refused the epoch with `AB6202`. | Rebuild the epoch so its MCP runtime evidence references manifested target files. | +| `AB8066` | 500 | `Artifact epoch reference could not be released.`, the inspection service reported `AB6201`. | None in the project: the failure is internal to the development server's epoch bookkeeping. | +| `AB8067` | 404 | `Artifact epoch was not found.`, the epoch id names no published epoch. | Pick an epoch from the current epoch list. | +| `AB8068` | 400 | `Artifact epoch id is not valid.`, the epoch store rejected the id's shape. | Use an epoch id the Workbench lists. | ### Evals (`/api/evals/**`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8070` | 400 | `Eval route path is not valid.` — an unknown path under `/api/evals/` or a segment that does not decode. | Use the routes the Workbench Evals page issues. | -| `AB8071` | 404 / 503 | `Eval routes are not available.` — 404 without the eval service, 503 once shutdown has begun. | Restart `agent-bundle dev`. | -| `AB8072` | 400 | `Eval request has an invalid shape.` — the body does not match the operation's fields, `trials` is not an integer between 1 and 100, or the service refused the trial count. | Send the fields the operation defines. | -| `AB8073` | 502 | `Eval operation could not be completed.` — the service threw something the route does not map to a code below. | Read the dev-server log for the underlying error, then retry. | -| `AB8074` | 404 | `Eval run was not found.` — the run id is unknown or not a valid run id. | Read a run this project recorded, or start a new one. | +| `AB8070` | 400 | `Eval route path is not valid.`, an unknown path under `/api/evals/` or a segment that does not decode. | Use the routes the Workbench Evals page issues. | +| `AB8071` | 404 / 503 | `Eval routes are not available.`, 404 without the eval service, 503 once shutdown has begun. | Restart `agent-bundle dev`. | +| `AB8072` | 400 | `Eval request has an invalid shape.`, the body does not match the operation's fields, `trials` is not an integer between 1 and 100, or the service refused the trial count. | Send the fields the operation defines. | +| `AB8073` | 502 | `Eval operation could not be completed.`, the service threw something the route does not map to a code below. | Read the dev-server log for the underlying error, then retry. | +| `AB8074` | 404 | `Eval run was not found.`, the run id is unknown or not a valid run id. | Read a run this project recorded, or start a new one. | | `AB8075` | 422 | `The requested eval harness is unknown or unsupported.` | Use `deterministic`, `claude`, or `codex`. | | `AB8076` | 422 | `No discovered eval suite or case matched this selection.` | Select a suite or case the Evals page lists as discovered. | | `AB8077` | 422 | `The evaluated artifact has no target for a pinned eval host.` | Select the targets the pinned eval hosts name, then evaluate again. | -| `AB8078` | 422 | `A recorded eval run could not be read.` — the run store reports a corrupt or invalid persisted run. | Repair or remove the corrupt persisted eval run. | -| `AB8079` | 422 | `Project eval configuration is not valid.` — the eval service threw an `EvalConfigError`; the route hides its message. | Fix the project's `evals` configuration; running `agent-bundle eval` from the CLI surfaces the underlying error message. | -| `AB8080` | 422 | `An authored eval suite is not valid.` — the eval service threw an `EvalDefinitionError` or `EvalDiscoveryError`; the route hides its message. | Fix the authored suite; running `agent-bundle eval` from the CLI surfaces the underlying error message. | -| `AB8081` | 422 | `An eval fixture could not be prepared.` — the eval service threw an `EvalFixtureError`. | Fix the fixture the eval case references; the CLI surfaces the underlying error message. | -| `AB8082` | 422 | `The artifact under evaluation could not be prepared.` — the eval service threw an `EvalHarnessError`. | Rebuild the artifact under evaluation; the CLI surfaces the underlying error message. | +| `AB8078` | 422 | `A recorded eval run could not be read.`, the run store reports a corrupt or invalid persisted run. | Repair or remove the corrupt persisted eval run. | +| `AB8079` | 422 | `Project eval configuration is not valid.`, the eval service threw an `EvalConfigError`; the route hides its message. | Fix the project's `evals` configuration; running `agent-bundle eval` from the CLI surfaces the underlying error message. | +| `AB8080` | 422 | `An authored eval suite is not valid.`, the eval service threw an `EvalDefinitionError` or `EvalDiscoveryError`; the route hides its message. | Fix the authored suite; running `agent-bundle eval` from the CLI surfaces the underlying error message. | +| `AB8081` | 422 | `An eval fixture could not be prepared.`, the eval service threw an `EvalFixtureError`. | Fix the fixture the eval case references; the CLI surfaces the underlying error message. | +| `AB8082` | 422 | `The artifact under evaluation could not be prepared.`, the eval service threw an `EvalHarnessError`. | Rebuild the artifact under evaluation; the CLI surfaces the underlying error message. | | `AB8083` | 422 | `Configured semantic grading requires the native Claude eval harness.` | Run the configured semantic grader with the `claude` harness and a Claude-pinned eval case. | -| `AB8085` | 404 / 413 | Two meanings share this code: `Recorded raw evidence was not found.` (404, evals) — the requested raw-evidence artifact does not exist for the trial; `Request body exceeds 1 MiB.` (413, playground) — the prompt playground's larger body bound. | Evals: select raw evidence the recorded trial persisted. Playground: send a smaller body. | -| `AB8086` | 422 | `Recorded raw evidence is not available.` — the run persisted no readable raw evidence for the trial. | Regenerate the recorded eval run before reading its raw evidence. | -| `AB8087` | 400 | `Eval event cursor is not valid.` — the event stream cursor is not a non-negative integer no later than the durable event sequence. | Reconnect from a non-negative cursor no later than the durable event sequence. | -| `AB8088` | 413 | `Eval event replay exceeds the stream limit.` — the retained events after the requested cursor would exceed the stream byte limit when replayed. | Reconnect from a later cursor so fewer retained events replay. | +| `AB8085` | 404 / 413 | Two meanings share this code: `Recorded raw evidence was not found.` (404, evals), the requested raw-evidence artifact does not exist for the trial; `Request body exceeds 1 MiB.` (413, playground), the prompt playground's larger body bound. | Evals: select raw evidence the recorded trial persisted. Playground: send a smaller body. | +| `AB8086` | 422 | `Recorded raw evidence is not available.`, the run persisted no readable raw evidence for the trial. | Regenerate the recorded eval run before reading its raw evidence. | +| `AB8087` | 400 | `Eval event cursor is not valid.`, the event stream cursor is not a non-negative integer no later than the durable event sequence. | Reconnect from a non-negative cursor no later than the durable event sequence. | +| `AB8088` | 413 | `Eval event replay exceeds the stream limit.`, the retained events after the requested cursor would exceed the stream byte limit when replayed. | Reconnect from a later cursor so fewer retained events replay. | ### Development logs (`/api/logs/replay`, `/api/logs/stream`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8090` | 400 | `Dev Log route path is not valid.` — a path under `/api/logs/` other than `replay` or `stream`. | Use the Logs page routes. | -| `AB8091` | 400 | `Dev Log cursor is not valid.` — the `after` cursor is not a single non-negative safe integer. | Send one non-negative integer cursor. | -| `AB8092` | 409 | `Dev Log cursor is ahead of retained history.` — the cursor is beyond the log ring's latest sequence (the Logs page treats this as a reset and replays from the start). | Reconnect from a cursor no later than the latest retained sequence. | -| `AB8093` | 404 / 503 | `Dev Log routes are not available.` — 404 without the log service, 503 once shutdown has begun or when the service reports any other failure. | Restart `agent-bundle dev`. | +| `AB8090` | 400 | `Dev Log route path is not valid.`, a path under `/api/logs/` other than `replay` or `stream`. | Use the Logs page routes. | +| `AB8091` | 400 | `Dev Log cursor is not valid.`, the `after` cursor is not a single non-negative safe integer. | Send one non-negative integer cursor. | +| `AB8092` | 409 | `Dev Log cursor is ahead of retained history.`, the cursor is beyond the log ring's latest sequence (the Logs page treats this as a reset and replays from the start). | Reconnect from a cursor no later than the latest retained sequence. | +| `AB8093` | 404 / 503 | `Dev Log routes are not available.`, 404 without the log service, 503 once shutdown has begun or when the service reports any other failure. | Restart `agent-bundle dev`. | ### Route manifest (`/api/routes/manifest`) | Code | Status | Trigger | Recovery | | --- | --- | --- | --- | -| `AB8120` | 400 | `Route manifest path is not valid.` — `/api/routes` or a path under it other than `/api/routes/manifest`. | Request `/api/routes/manifest` exactly. | -| `AB8121` | 404 / 409 / 503 | `Route manifest is not available.` — 404 without the route manifest service, 503 after the routes closed, 409 when no valid prepared project exists yet (the manifest is a projection of the latest valid compiler pass, never an empty catalog). | Fix the project preparation failures the Overview reports so a valid pass exists; after shutdown, restart `agent-bundle dev`. | -| `AB8122` | 400 | `Route manifest request has an invalid shape.` — the request carries a query string. | Send a bare `GET /api/routes/manifest`. | -| `AB8123` | — | Workbench route manifest client: `Route manifest request failed with HTTP .` — the response carried no diagnostic body; `Route manifest route returned an invalid response.` — the body does not match the manifest schema. | Reload the Workbench against a running `agent-bundle dev`; a mismatched Workbench and server build produces the invalid response. | +| `AB8120` | 400 | `Route manifest path is not valid.`, `/api/routes` or a path under it other than `/api/routes/manifest`. | Request `/api/routes/manifest` exactly. | +| `AB8121` | 404 / 409 / 503 | `Route manifest is not available.`, 404 without the route manifest service, 503 after the routes closed, 409 when no valid prepared project exists yet (the manifest is a projection of the latest valid compiler pass, never an empty catalog). | Fix the project preparation failures the Overview reports so a valid pass exists; after shutdown, restart `agent-bundle dev`. | +| `AB8122` | 400 | `Route manifest request has an invalid shape.`, the request carries a query string. | Send a bare `GET /api/routes/manifest`. | +| `AB8123` | none | Workbench route manifest client: `Route manifest request failed with HTTP .`, the response carried no diagnostic body; `Route manifest route returned an invalid response.`, the body does not match the manifest schema. | Reload the Workbench against a running `agent-bundle dev`; a mismatched Workbench and server build produces the invalid response. | ## Eval refusals through the API and CLI (`AB9001`–`AB9005`, `AB9007`–`AB9011`) @@ -1991,13 +1991,13 @@ conditions under their own `AB807x`/`AB808x` codes (see "Development server"). | Code | Severity | Trigger | Recovery | | --- | --- | --- | --- | -| `AB9001` | error | `EVAL_HARNESS_UNSUPPORTED` — the requested harness is unknown or unsupported. | Use `deterministic`, `claude`, or `codex`, or correct an unknown harness name. | -| `AB9002` | error | `EVAL_SELECTION_EMPTY` — no discovered eval suite or case matched the selection. | Select a suite or case that `agent-bundle eval --json` reports as discovered. | -| `AB9003` | error | `EVAL_RUN_NOT_FOUND` — the run id names no recorded run (`compareEvals` also maps the run store's not-found error here). | Read a run that this project recorded, or start a new one. | -| `AB9004` | error | `EVAL_TARGET_MISSING` — the evaluated artifact has no target for a pinned eval host. | Select the targets the pinned eval hosts name, then evaluate again. | -| `AB9005` | error | `EVAL_TRIALS_INVALID` — the trial count is not an integer between 1 and 100. | Request an integer trial count between 1 and 100. | -| `AB9007` | error | `A persisted eval run is corrupt and cannot be compared.` — `compareEvals` read a run the store reports as corrupt or with an invalid record. | Repair or remove the corrupt persisted eval run, then compare two completed runs. | -| `AB9008` | error | `EVAL_SEMANTIC_GRADER_UNSUPPORTED` — the configured semantic grader needs the native Claude harness. | Run the configured semantic grader with `--harness claude` and a Claude-pinned eval case. | -| `AB9009` | error | `EVAL_ARTIFACT_NOT_FOUND` — the requested raw evidence does not exist for the recorded trial. | Select raw evidence that the recorded eval trial persisted. | -| `AB9010` | error | `EVAL_ARTIFACT_UNAVAILABLE` — the recorded run has no readable raw evidence for the trial. | Regenerate the recorded eval run before reading its raw evidence. | -| `AB9011` | error | `EVAL_EVENTS_CURSOR_INVALID` — an eval event cursor is negative or beyond the durable event sequence. | Reconnect from a non-negative cursor no later than the durable event sequence. | +| `AB9001` | error | `EVAL_HARNESS_UNSUPPORTED`, the requested harness is unknown or unsupported. | Use `deterministic`, `claude`, or `codex`, or correct an unknown harness name. | +| `AB9002` | error | `EVAL_SELECTION_EMPTY`, no discovered eval suite or case matched the selection. | Select a suite or case that `agent-bundle eval --json` reports as discovered. | +| `AB9003` | error | `EVAL_RUN_NOT_FOUND`, the run id names no recorded run (`compareEvals` also maps the run store's not-found error here). | Read a run that this project recorded, or start a new one. | +| `AB9004` | error | `EVAL_TARGET_MISSING`, the evaluated artifact has no target for a pinned eval host. | Select the targets the pinned eval hosts name, then evaluate again. | +| `AB9005` | error | `EVAL_TRIALS_INVALID`, the trial count is not an integer between 1 and 100. | Request an integer trial count between 1 and 100. | +| `AB9007` | error | `A persisted eval run is corrupt and cannot be compared.`, `compareEvals` read a run the store reports as corrupt or with an invalid record. | Repair or remove the corrupt persisted eval run, then compare two completed runs. | +| `AB9008` | error | `EVAL_SEMANTIC_GRADER_UNSUPPORTED`, the configured semantic grader needs the native Claude harness. | Run the configured semantic grader with `--harness claude` and a Claude-pinned eval case. | +| `AB9009` | error | `EVAL_ARTIFACT_NOT_FOUND`, the requested raw evidence does not exist for the recorded trial. | Select raw evidence that the recorded eval trial persisted. | +| `AB9010` | error | `EVAL_ARTIFACT_UNAVAILABLE`, the recorded run has no readable raw evidence for the trial. | Regenerate the recorded eval run before reading its raw evidence. | +| `AB9011` | error | `EVAL_EVENTS_CURSOR_INVALID`, an eval event cursor is negative or beyond the durable event sequence. | Reconnect from a non-negative cursor no later than the durable event sequence. | diff --git a/docs/effect-conventions.md b/docs/effect-conventions.md index b9a2238c9..7bf28b9c8 100644 --- a/docs/effect-conventions.md +++ b/docs/effect-conventions.md @@ -22,9 +22,9 @@ Read `repos/effect/LLMS.md` before writing Effect code. Refresh Each Effect-consuming package has exactly one `src/effect/boundary.ts`: -- [`packages/rsc-runtime/src/effect/boundary.ts`](../packages/rsc-runtime/src/effect/boundary.ts) — runtime + state kernel internals. -- [`packages/agent-bundle/src/effect/boundary.ts`](../packages/agent-bundle/src/effect/boundary.ts) — the dev seam (Stage 3). Maps interruption to `AbortError` and rethrows the dev seam's typed contracts (`CodedError` / `YieldableCodedError` subclasses, `DiagnosticError`) unchanged. -- [`packages/create-agent-bundle/src/effect/boundary.ts`](../packages/create-agent-bundle/src/effect/boundary.ts) — the scaffolder (FileSystem phase 1). Rethrows `UsageError` / `Error` unchanged; unwraps `PlatformError` to its Node cause. +- [`packages/rsc-runtime/src/effect/boundary.ts`](../packages/rsc-runtime/src/effect/boundary.ts): runtime + state kernel internals. +- [`packages/agent-bundle/src/effect/boundary.ts`](../packages/agent-bundle/src/effect/boundary.ts): the dev seam (Stage 3). Maps interruption to `AbortError` and rethrows the dev seam's typed contracts (`CodedError` / `YieldableCodedError` subclasses, `DiagnosticError`) unchanged. +- [`packages/create-agent-bundle/src/effect/boundary.ts`](../packages/create-agent-bundle/src/effect/boundary.ts): the scaffolder (FileSystem phase 1). Rethrows `UsageError` / `Error` unchanged; unwraps `PlatformError` to its Node cause. The boundary owns: @@ -58,13 +58,13 @@ Services: declare `class X extends Context.Service()("pkg/path/X")`. zod stays at every schema boundary (MCP SDK interop; recorded G-decisions). **Effect Schema is deferred** (re-evaluated 2026-09-01 for wire contracts; see -[Effect Schema wire contracts](#effect-schema-wire-contracts-schema-projections)) -— do not introduce `Schema.TaggedError` on the public or MCP-facing contracts. +[Effect Schema wire contracts](#effect-schema-wire-contracts-schema-projections)), +do not introduce `Schema.TaggedError` on the public or MCP-facing contracts. Internals keep the existing classes. ### Yieldable framework errors (`Data.Error`, decided 2026-09-03) -Framework-process error classes — the ones raised inside Effect programs in +Framework-process error classes, the ones raised inside Effect programs in the dev seam and the eval service whose declarations no package export reaches (today: `DevCoordinatorCloseError`, `RuntimeMcpRegistryError` / `RuntimeMcpRegistryCloseError`, `RuntimeGenerationStoreError` / @@ -72,7 +72,7 @@ reaches (today: `DevCoordinatorCloseError`, `RuntimeMcpRegistryError` / `ScriptPlaygroundFailure` / `ScriptPlaygroundAbortError`, `LifecycleReplayRequestError`, `ArtifactInspectionServiceError`, `HookSimulationAbortError` / `HookSimulationTerminationError`, -`CodexEvalHarnessError`, `SmokeStepError`) — extend the yieldable bases in +`CodexEvalHarnessError`, `SmokeStepError`), extend the yieldable bases in [`packages/agent-bundle/src/effect/errors.ts`](../packages/agent-bundle/src/effect/errors.ts): `YieldableFrameworkError` (the `Data.Error` twin of `Error`, same `(message?, options?)` constructor) and `YieldableCodedError` (the @@ -87,15 +87,15 @@ What does not change: `instanceof Error` / `instanceof X`, `.name`, `.message`, `.code`, `.cause`, `.stack`, the boundary's identity-preserving rethrow (`isTypedDevError` matches the string `code`), `JSON.stringify`, `stableJson`, `{ ...error }`, and `util.inspect`. rc.112 `Data.Error` would -otherwise change the last four — its prototype `toJSON` spreads the +otherwise change the last four, its prototype `toJSON` spreads the constructor fields (`message`, `cause`) into the JSON and its -`[nodejs.util.inspect.custom]` prints that instead of the stack — so the +`[nodejs.util.inspect.custom]` prints that instead of the stack, so the base shadows both with non-functions and installs `cause` non-enumerable; `tests/effect-errors.test.ts` pins byte-identical output against the plain twin. Migration is mechanical per file: swap the `extends` clause, import the base; constructors and call sites stay. -Carve-outs — these stay on plain `Error` / `CodedError`, and a class that +Carve-outs. These stay on plain `Error` / `CodedError`, and a class that moves into one of these positions moves back: - **Effect-free entry graphs.** `src/effect/errors.ts` imports `effect`. @@ -108,7 +108,7 @@ moves into one of these positions moves back: on all five entries. `McpAppBridgeCloseError` stays plain for the same reason (`agent-bundle/rstest` and `agent-bundle/test/browser` reach it). - **The public declaration graph.** Any class whose declaration file a - `package.json` export's `types` reaches — not only classes that are + `package.json` export's `types` reaches, not only classes that are themselves exported. A consumer's `tsc` resolves every `.d.ts` the entry imports, so `class X extends YieldableCodedError` in a reachable file makes `effect` a type dependency of the package (`public-api.test.ts`'s @@ -178,7 +178,7 @@ on the Promise edge. Do not widen public error types to satisfy Effect. with no reason, and never hands out the controller. A contract that must abort *with a reason* on a caller's request (the MCP session's `cancel()` and close-time `#cancelAll`, whose reason reaches the SDK rejection) owns - its `AbortController` as the scoped resource instead — `acquireRelease` + its `AbortController` as the scoped resource instead, `acquireRelease` admits the slot and returns the controller, release aborts it and frees the slot, and the host signal is joined with `AbortSignal.any` (#512, `dev/mcp-session/mcp-session.ts` `#admitRequest`). That is the same @@ -208,7 +208,7 @@ Stage 2 uses Effect `Stream` for the #145 dispatcher: Flight bytes via `Stream.unfold` that waits for event-stream demand *before* `reader.read()`, pending boundaries via `Stream.paginate`, contract bounds as the emit stage (`emitBoundRenderEvent` / `createAgentRenderEventSequence`), and progress -via `Stream.merge` + `takeUntil(complete)` (not `Stream.callback` — a failed +via `Stream.merge` + `takeUntil(complete)` (not `Stream.callback`, a failed callback producer does not fail the stream). A `Latch` opened from the public event-stream pull gates Flight bytes after the shell. Host `AbortSignal` becomes `Stream.interruptWhen` + `abortToInterrupt` at the @@ -224,9 +224,9 @@ public edge. Pattern files: One subsystem per PR, all behind unchanged Promise APIs and wire contracts (#158 boundary, #159 MCP session lifecycles, #160 rebuild scheduler, #161 EpochStore). Leaf filesystem/SDK helpers stay imperative and are -identity-lifted through `src/effect/lift.ts`; the orchestration — +identity-lifted through `src/effect/lift.ts`; the orchestration, lifecycles, mutual exclusion, coalescing, compensation, failure -aggregation — is Effect. +aggregation, is Effect. **`ProjectEventHub` (the SSE hub) stays imperative.** Its public contract is *synchronous* re-entrant fan-out: `publish` delivers to listeners in the same @@ -264,9 +264,9 @@ Helped: Hurt / gotchas: -- Scope finalizers are infallible by type — `Effect.acquireRelease` takes a +- Scope finalizers are infallible by type: `Effect.acquireRelease` takes a `release: (a, exit) => Effect` and `Effect.addFinalizer` - an `Effect` — so a release that can fail cannot even be + an `Effect`, so a release that can fail cannot even be written there. `Effect.acquireUseRelease`'s `release` and `Effect.onExit` / `Effect.ensuring` handlers are **not** infallible: rc.112 types them `Effect` and merges a failing handler into the result @@ -278,8 +278,8 @@ Hurt / gotchas: chains, `DevCoordinatorCloseError` in `dev/coordinator.ts` #513, the IPC claim release and socket removal in `events/ipc.ts` #516, the staging-root removal that replaces the publish outcome) are therefore explicit effect - sequences — `Effect.exit` on the attempt, run the cleanup, aggregate, - then unwrap — under `Effect.uninterruptibleMask` where the original + sequences, `Effect.exit` on the attempt, run the cleanup, aggregate, + then unwrap, under `Effect.uninterruptibleMask` where the original `finally` was uninterruptible. Neither scope finalizers nor `acquireUseRelease` express that ordering. - Bare `Effect.tryPromise(fn)` wraps rejections in `Cause.UnknownError`; @@ -312,19 +312,19 @@ The discovery module owns report loading plus ephemeral live-probe consent and result state. Atoms live in `effect/unstable/reactivity`; React bindings come from `@effect/atom-react`. -- No `Atom` or `AsyncResult` types in DTOs, public exports, or examples — +- No `Atom` or `AsyncResult` types in DTOs, public exports, or examples, atoms consume the strictly-decoded outputs of the existing zod clients and never replace wire contracts. - One root `RegistryProvider` mounted in the app shell (`src/main.tsx`); the module-level default registry is never used. -- Components never call `Effect.run*` — the registry owns effect execution; +- Components never call `Effect.run*`: the registry owns effect execution; components interact through the `@effect/atom-react` hooks only (rslint `effect-boundary/no-ad-hoc-run` already enforces the run ban). - Imperative clients (`ProjectClient`, `RuntimeClient`, `AgentDocumentClient`, …) stay the lifecycle authorities; atoms are read-side caches over their decoded outputs. - Known caveat: `4.0.0-rc.112` has a stream-backed derived-atom disposal bug - (fixed upstream post-rc.112, unpublished) — no stream-backed derived atoms + (fixed upstream post-rc.112, unpublished), no stream-backed derived atoms in the Workbench until a re-pin past the fix; the root `RegistryProvider` (not the default registry) avoids the reported React case. - Every effect re-pin must bump `@effect/atom-react` to the same RC in the @@ -344,8 +344,8 @@ implementations come from `@effect/platform-node` (`NodeServices.layer`) in `NodeFileSystem` / `NodePath` / `NodeChildProcessSpawner` / `NodeStdio` / `NodeTerminal` / `NodeCrypto`; `@effect/platform-node`'s modules are re-exports of it) in `agent-bundle`, which every consumer installs: -`@effect/platform-node@rc.112` would add `undici`, `mime`, and — through a -non-optional `redis` peer that npm auto-installs — a Redis client (+23 MB, +`@effect/platform-node@rc.112` would add `undici`, `mime`, and, through a +non-optional `redis` peer that npm auto-installs, a Redis client (+23 MB, +17 packages) to each consumer install. `agent-bundle`'s `platformLayer` composes the same six services the same way `NodeServices.layer` does. The `ws` / `@types/ws` / `@types/node` dependencies of `platform-node-shared` @@ -355,7 +355,7 @@ encodes: the pinned `FileSystem` has no `lstat`, `OpenFlag` accepts only string flags (no `O_NOFOLLOW`), and there is no directory fsync. `NodeRuntime.runMain` stays banned (the 130/143 signal-distinct exit contract). The same package's `Terminal` and `Stdio` services are adopted for -the first-party CLI's user-facing text — see +the first-party CLI's user-facing text, see [Terminal and Stdio](#terminal-and-stdio-user-facing-cli-text). ### Adopt @@ -363,7 +363,7 @@ the first-party CLI's user-facing text — see - Ordinary reads, writes, `mkdir`, `readDirectory`, `stat`, `exists`, `remove`, `rename`, `copy` in code that already runs (or is being moved) inside an Effect program: `yield* FileSystem.FileSystem`, then the method. - `readDirectory` returns names only — `stat(...).type === 'Directory'` + `readDirectory` returns names only, `stat(...).type === 'Directory'` replaces `Dirent.isDirectory()`. - `Path.Path` for `join` / `resolve` / `dirname` / `fromFileUrl` in the same modules. `fromFileUrl` fails with `BadArgument`; `Effect.orDie` it when the @@ -371,7 +371,7 @@ the first-party CLI's user-facing text — see - Temporary directories whose lifetime ends with the enclosing operation: in `agent-bundle`, `withTempDirectory(options, use)` from `src/effect/platform.ts`, the bracket that reproduces `mkdtemp` + - `try`/`finally` `rm(dir, { recursive: true, force: true })` exactly — + `try`/`finally` `rm(dir, { recursive: true, force: true })` exactly, `force`, cleanup failure as a typed `PlatformError` that wins over the operation's failure, cleanup on interruption. Not `fs.makeTempDirectoryScoped` in library code: the rc.112 finalizer removes @@ -412,7 +412,7 @@ the first-party CLI's user-facing text — see [Terminal and Stdio](#terminal-and-stdio-user-facing-cli-text)) and widens to `platformLayer` there when CLI code adopts the filesystem services; the dev server has one `makeScopedEffectRuntime(platformLayer)`, created - inside `startDevServer` (never at module top level — `effect` is a CLI + inside `startDevServer` (never at module top level, `effect` is a CLI cold-start cost) and disposed from the returned session's `close` after every service has closed (`createDevPlatformRuntime` in `src/dev/platform-run.ts`). Every dev service takes the runtime as an @@ -425,7 +425,7 @@ the first-party CLI's user-facing text — see over the long-lived runtime, `PlatformError` unwrapped the same way); absent a handle, `platformRunOf` returns `runWithPlatform`, so the services stay usable on their own. Both modules live under `dev/`, not in - `platform.ts` — that module is bundled into the emitted installer, which + `platform.ts`, that module is bundled into the emitted installer, which stays byte-identical. Never provide a platform layer deep inside library code. - Errors: `PlatformError` flows through the Effect error channel and is @@ -435,13 +435,13 @@ the first-party CLI's user-facing text — see Node error" (the scaffolder), unwrap `PlatformError.cause` to the `ErrnoException` so messages stay byte-identical. - Tests: `FileSystem.layerNoop({ ...overrides })` for call/result/error - protocol tests — its defaults fail with `NotFound` or die, so override + protocol tests, its defaults fail with `NotFound` or die, so override every operation the code under test performs. Keep real temp directories (`makeTempDirectoryScoped` under `it.effect` / `it.live`) for anything about symlinks, permissions, atomic rename, SQLite, or packed executables. Do not convert Promise-contract tests solely for fixture cleanup. -### Keep raw (`node:fs` / `node:path`) — explicit carve-outs +### Keep raw (`node:fs` / `node:path`), explicit carve-outs - `core/durable-fs.ts` and everything that publishes through it: epoch store, playground stores, eval run-store, dev-lock, receipts. They need @@ -480,8 +480,8 @@ the first-party CLI's user-facing text — see `dev/playground/lifecycle-replay-service.ts`'s synchronous `existsSync` probe. - `dev/watcher.ts`: chokidar stays. `FileSystem.watch` is a thin `fs.watch` - with create/update/remove only — no `ignored` callbacks, readiness, or the - other event kinds — and the watcher's `dev:ino` signatures need `stat` + with create/update/remove only, no `ignored` callbacks, readiness, or the + other event kinds, and the watcher's `dev:ino` signatures need `stat` semantics we do not want to change. - Synchronous config/discovery on the compiler and cold-start path (`config/validate.ts`, `config/conventional-entry.ts`, @@ -533,7 +533,7 @@ and `native-codex-contract.ts`, `services/{hook-service,mcp-service,mcp-run}.ts` `mcp-run`'s SIGINT/SIGTERM forwarding is a scoped `acquireRelease`), the `eval/*` harness readers, `fixtures.ts` materialization and the Codex trial home, and the post-build readers `build/validate-artifact*.ts`, -`pack-inventory.ts` — each through `runWithPlatform` at its existing +`pack-inventory.ts`, each through `runWithPlatform` at its existing Promise signature, with the link-identity checks kept raw per the carve-outs above. The sibling `routes/graph.ts` reads stay raw: `compileRouteGraph` is the compiler/cold-start discovery path, and lifting @@ -557,8 +557,8 @@ contract (`runtime-store-contracts.ts`, exported from `agent-bundle/api`) has no session runtime to hand it. Two directories outlive their call and are therefore not `withTempDirectory` brackets: the MCP session's plugin-data directory is acquired into its own session-lifetime `Scope` whose only -finalizer removes it — the session closes that scope from `close()`, and -until the session exists the open scope's release closes it instead — and +finalizer removes it, the session closes that scope from `close()`, and +until the session exists the open scope's release closes it instead, and the script playground's workspace lease keeps `close` as a separate step so a removal failure is reported in the result's `cleanupFailures`, not in place of the script's outcome. @@ -578,7 +578,7 @@ stdout-only; **diagnostics** (the canonical JSON diagnostics document) go through `Stdio.stderr()`, and **machine output** (`--json`, stable JSON lines) goes through `Stdio.stdout()` so its bytes stay exact. The helpers live in `src/effect/terminal.ts` (`display`, `writeStderr`, `writeStdout`). -**Argv-layer text** — Commander's `--help`, `--version`, and argv errors — +**Argv-layer text**, Commander's `--help`, `--version`, and argv errors, is the one exception: it is written synchronously to the process streams before any command runs (see the cold-start budget below). @@ -586,7 +586,7 @@ Cold-start budget (measured 2026-09-03, Node v22.23.2, 30 runs, median wall time of the built `bin/agent-bundle.js`; `node -e 0` is ≈28 ms on the same machine): `--version` ≈60 ms before the adoption, ≈300 ms with the runtime built eagerly in `runCli`, ≈60 ms with the lazy runtime; `--help` the same; -`validate` on `examples/host-test` unchanged (≈1.7–2.3 s, dominated by the +`validate` on `examples/host-test` unchanged (≈1.7 to 2.3 s, dominated by the compiler). Where the +240 ms went: loading the `effect` module graph (≈300 ms for the `effect` barrel in an unbundled process; ≈100 ms of it is `effect/Terminal` alone, and the minimal `effect/Effect` + `Layer` + @@ -631,8 +631,8 @@ Wiring rules: - The scaffolder (`packages/create-agent-bundle/src/index.ts`) follows the same split: `runCli` parses flags and writes `--help` (stdout) and flag errors (stderr) synchronously to its `CliStreams` (default: the process - streams), then loads `src/scaffold-cli.ts` — the `NodeServices.layer` - root, the scaffold program, and Clack — with a dynamic `import()`. Clack + streams), then loads `src/scaffold-cli.ts`, the `NodeServices.layer` + root, the scaffold program, and Clack, with a dynamic `import()`. Clack stays the prompt renderer and is not replaced by `readLine`. - Keep `display` text explicit about line endings (`\n`); the service writes what it is given. @@ -650,9 +650,9 @@ Wiring rules: `process.stdout`/`process.stderr` adapters: emitted artifacts must not carry a platform runtime, and byte-exact protocol frames are not terminal text. - **The route-facing terminal capability is plain Node, not `Terminal`.** - `request.terminal` (#511) — TTY-ness, color depth, and `columns`/`rows` per + `request.terminal` (#511), TTY-ness, color depth, and `columns`/`rows` per output stream, reported to routes, rendered scripts, and `main`-envelope - executables — is probed by the dependency-free `src/terminal-capability.ts` + executables, is probed by the dependency-free `src/terminal-capability.ts` (aliased into emitted executables as `agent-bundle/terminal-capability`) because those artifacts must not carry the Effect runtime; the first-party CLI mounts no route request scope, so it has nothing to read from the @@ -679,8 +679,8 @@ identical. That claim does not cover every export under `contracts/*`. `McpSessionTraceEntry.occurredAt` preserves a numeric Unix timestamp, and `DevRuntimeAsset.body` carries a `Uint8Array` through `contracts/runtime.ts`; neither is one of the audited JSON DTO routes. `toType` / `toEncoded` would -therefore be near-identity at the audited seams. The real dual maintenance — -contract types plus separately hand-written Workbench decoders — is a +therefore be near-identity at the audited seams. The real dual maintenance, +contract types plus separately hand-written Workbench decoders, is a single-source-of-truth problem that the pinned zod can already solve with `z.infer`; it does not require a second schema runtime. @@ -702,8 +702,8 @@ fixtures measure Effect Schema at 101.5 kB / 33.4 kB gzip versus zod at The diagnostics do not fit these seams either. Workbench clients deliberately collapse decode failures into single AB-coded errors such as `AB8233` and `AB8063`, so `SchemaIssue` / `Formatter` trees add nothing there. -`config/validate.ts` contains business-rule diagnostics — AB code, severity, -and recovery per rule — rather than shape validation. +`config/validate.ts` contains business-rule diagnostics, AB code, severity, +and recovery per rule, rather than shape validation. zod stays at every wire/schema boundary under the existing G-decisions, and the hand-rolled exact-key guards stay. If a future contract needs real @@ -716,7 +716,7 @@ projections on that single contract before any wider adoption. `*Exit` siblings) outside `src/effect/boundary.ts`. - Imports from `repos/**`. - Effect Schema on public or zod boundaries. -- `@effect/vitest` — this repo uses rstest. +- `@effect/vitest`: this repo uses rstest. - `NodeRuntime.runMain` / `BunRuntime` as a substitute for the boundary. - Ad-hoc `ManagedRuntime` outside a boundary module. - `effect/unstable/*` until listed below (Stages 2, 3, and the #99 notice ledger listed none). @@ -736,7 +736,7 @@ wire contracts](#effect-schema-wire-contracts-schema-projections). | Module | Adopted in | Re-verify | | --- | --- | --- | | `effect/unstable/reactivity` (+ `@effect/atom-react` bindings) | Workbench Agent Document panel (#105 phase 1) and route editor (#105 phase 2) | re-pin bumps @effect/atom-react in lockstep; re-run disposal regression + bundle measurement; stream-backed derived atoms stay banned until the rc.112 disposal fix ships | -| `@effect/platform-node` (`NodeServices.layer`, `create-agent-bundle`) and `@effect/platform-node-shared` (`agent-bundle`'s `platformLayer`); `FileSystem` / `Path` services live in `effect` | **adopted** (2026-09-03) for ordinary I/O — `create-agent-bundle` scaffolder and the `agent-bundle` temp directories in `api.ts` / the Codex validator (phase 1); host-contracts validators, `services/*`, `eval/*`, and the post-build readers (phase 2, ordinary-I/O modules, 2026-09-03); the dev server's services on one session-scoped runtime created in `startDevServer` (phase 2, dev server, 2026-09-03); see [Effect platform services](#effect-platform-services-effectplatform-node) for the keep-raw list and the consumer-footprint reason for the split | re-pin bumps both in lockstep with `effect`; re-check whether `@effect/platform-node` still forces a `redis` peer (if it stops, `agent-bundle` can move to `NodeServices.layer`); re-check whether `lstat` / `O_NOFOLLOW` / directory fsync landed (would shrink the keep-raw list) and the `runMain` 130/143 exit contract | +| `@effect/platform-node` (`NodeServices.layer`, `create-agent-bundle`) and `@effect/platform-node-shared` (`agent-bundle`'s `platformLayer`); `FileSystem` / `Path` services live in `effect` | **adopted** (2026-09-03) for ordinary I/O, `create-agent-bundle` scaffolder and the `agent-bundle` temp directories in `api.ts` / the Codex validator (phase 1); host-contracts validators, `services/*`, `eval/*`, and the post-build readers (phase 2, ordinary-I/O modules, 2026-09-03); the dev server's services on one session-scoped runtime created in `startDevServer` (phase 2, dev server, 2026-09-03); see [Effect platform services](#effect-platform-services-effectplatform-node) for the keep-raw list and the consumer-footprint reason for the split | re-pin bumps both in lockstep with `effect`; re-check whether `@effect/platform-node` still forces a `redis` peer (if it stops, `agent-bundle` can move to `NodeServices.layer`); re-check whether `lstat` / `O_NOFOLLOW` / directory fsync landed (would shrink the keep-raw list) and the `runMain` 130/143 exit contract | | `@effect/platform-node-shared` (`NodeTerminal` / `NodeStdio`) + `effect/Terminal`, `effect/Stdio` | first-party CLI command output, diagnostics, and machine output (`src/cli.ts`, `src/effect/terminal.ts`, `src/effect/cli-runtime.ts`), loaded lazily on the first command write (2026-09-03); Commander's help/version/argv-error text and the scaffolder's `--help` / flag-error text stay on synchronous process writes for the cold-start budget | re-pin re-checks `Terminal.display` stays stdout-only, `readLine` EOF → `QuitError`, the `Stdio` sink contract, and re-measures `agent-bundle --version` startup against the recorded ≈60 ms (`cli.test.ts` fails the build if the trivial invocations resolve an `effect` module) | | `Schema` / `SchemaAST` / `SchemaParser` projections (`toType` / `toEncoded`) for wire contracts | **declined** (2026-09-01) | revisit at Effect GA or on the first encoded/decoded-divergent wire contract; re-pin re-checks the projections API and the `onExcessProperty` parse-option default | @@ -748,7 +748,7 @@ is the official installer; this repo pins the plugin config in `tsconfig.base.json` so the same rules apply to every package that extends it. `.vscode/settings.json` enables the TypeScript 7 / tsgo workspace SDK (`js/ts.experimental.useTsgo`). We do not add a `prepare` hook that runs -`effect-tsgo patch` — that mutates `typescript` on every install. Re-run +`effect-tsgo patch`, that mutates `typescript` on every install. Re-run `npx @effect/tsgo setup --non-interactive` during a re-pin if the editor wiring drifts. @@ -757,7 +757,7 @@ wiring drifts. Generated stdio hooks run under host deadlines. Stage 0 baseline (generated `claude` SessionStart hook, bare `node ` process, 7 samples, Node v22.23.1, 2026-09-01): **median 39.74 ms**, min 36.41 ms, max 43.06 ms. -Adding the `effect` dependency changed no generated artifact — no runtime +Adding the `effect` dependency changed no generated artifact, no runtime entry (`index.js`, `state.js`, `state/sqlite.js`, `plugin.js`) imports it until Stage 1+. Machine-readable copy: [effect-cold-start-baseline.json](effect-cold-start-baseline.json). Stage 2 @@ -814,7 +814,7 @@ resolved the current repo practice stands, and new code follows it. budget for hooks that would newly import `effect/Predicate`. Either way the emitted-string copies stay: generated host-side JS has no `effect` import by design. -- **`Data.Error` for internal class errors — decided 2026-09-03: adopt, +- **`Data.Error` for internal class errors: decided 2026-09-03: adopt, internals only.** The rule and its carve-outs live in [Yieldable framework errors](#yieldable-framework-errors-dataerror-decided-2026-09-03). Summary: framework-process error classes in Effect-native modules extend @@ -824,15 +824,15 @@ resolved the current repo practice stands, and new code follows it. restore plain-`Error` `toJSON` / `util.inspect`), so programs write `return yield* new X(...)`. `Schema.TaggedError` stays deferred. Plain `Error` / `CodedError` remain for the bases on Effect-free entry graphs - (`CodedError`, `DiagnosticError`, `DevLockError`, `McpAppBridgeCloseError` - — the swap was measured to add a static `effect` import to + (`CodedError`, `DiagnosticError`, `DevLockError`, `McpAppBridgeCloseError`, + the swap was measured to add a static `effect` import to `agent-bundle/config`, `meta`, `rstest`, the CLI `--help` path, and the host - MCP proxy), every class on a public declaration graph — exported from a + MCP proxy), every class on a public declaration graph, exported from a package entry (the `Agent*` classes included) or merely reached by one through the emitted `.d.ts` files (`McpSessionError`, `EpochStoreError`, `ProjectEventHubError`, …), because `Cause.YieldableError` would make `effect` a type dependency for consumers; `public-api.test.ts` walks every - export's declaration graph and pins it — + export's declaration graph and pins it, emitted artifacts (66 `examples/host-test` files byte-identical in size before/after; the +12 kB figure was `effect/PlatformError`'s `Schema.TaggedError`, not `Data.Error`, which lives in the Effect core the @@ -852,6 +852,6 @@ soon as the trigger fires and retire the row. | Recorded | Pin (where) | Observed registry state | Trigger / action | | --- | --- | --- | --- | -| 2026-09-03 | `effect-rstest` **pkg.pr.new preview `e5f8d5f`** (`https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f`) — `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies (three pins). Needs a real release pin once published. | `npm view effect-rstest versions`: **E404 — not published to npm** (no versions, no dist-tags). | First npm publish of `effect-rstest`. Replace all three preview URLs with the exact published version, refresh `pnpm-lock.yaml`, re-run `pnpm test:unit` (`it.effect` / `it.live` suites). | -| 2026-09-03 | `effect` **`4.0.0-rc.112`** (`packages/agent-bundle`, `packages/rsc-runtime`, `packages/workbench`, `packages/create-agent-bundle`), `@effect/atom-react` `4.0.0-rc.112` (`packages/workbench`), `@effect/platform-node` `4.0.0-rc.112` (`packages/create-agent-bundle`), `@effect/platform-node-shared` `4.0.0-rc.112` (`packages/agent-bundle`), `@effect/language-service` `0.87.2` and `@effect/tsgo` `0.39.0` (root). Auto re-pin in lockstep + `repos/effect` subtree + Workbench atom phase 4 unblock (stream-backed derived atoms) once the post-rc.112 disposal fix ships. | `npm view effect dist-tags`: `rc` **`4.0.0-rc.112`** (unchanged), `beta` `4.0.0-beta.107`, `latest` `3.22.1`. `@effect/atom-react`: `rc` `4.0.0-rc.112`. `@effect/language-service`: `latest` `0.87.2`. `@effect/tsgo`: `latest` `0.39.1` (patch ahead of the `0.39.0` pin; rides the lockstep chore). | `effect@rc` advances past `4.0.0-rc.112`. Run the re-pin chore steps 1–6 above, bumping `effect`, `@effect/atom-react`, `@effect/language-service`, and `@effect/tsgo` together, then lift the stream-backed derived-atom ban in the Workbench if the disposal fix is in the new RC. | -| 2026-09-03 | Agent Plugins specification **`1.0.0`** — `packages/agent-bundle/src/adapters/schemas/portable/{plugin,mcp}.schema.json` + `PROVENANCE.json` (spec repo `agentplugins/agent-plugins-spec` @ `ff8ab5e392cc87bd88d87c060815a87490e51003`, 2026-08-19), portable `adapterRevision` `1.8.0`, pins in `tests/adapter-metadata.test.ts`. Spec watch for #426; not an npm pin, so re-verify with `curl`/`gh api`, not `npm view`. | Live `https://agent-plugins.org/schemas/1.0.0/{plugin,mcp}.schema.json` rehash to the pinned sha256 (1805 / 3408 bytes). Repo `main` HEAD unchanged at the pinned commit; **no tags, no GitHub releases**. `spec/1.1.0.md` is "Status: Working Draft" (started 2026-08-15, `a2afd7ec`); in-repo `schemas/1.1.0/*.schema.json` differ from 1.0.0 only in the `$id`/`const`/`description` version strings; `https://agent-plugins.org/schemas/1.1.0/*.schema.json` → 404. Observed latest published version: **1.0.0**. | `spec/1.1.0.md` (or later) flips to "Published" **and** `agent-plugins.org/schemas//` serves both schemas. Re-pin under `schemas/portable/` with a dated `PROVENANCE.json` (sha/bytes/date/commit), bump the portable `adapterRevision`, refresh the metadata pins, run `pnpm test:unit` (portable adapter + plugin-validation suites) and `pnpm test:host-install:build`, and add a capability row per additive field. | +| 2026-09-03 | `effect-rstest` **pkg.pr.new preview `e5f8d5f`** (`https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f`), `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies (three pins). Needs a real release pin once published. | `npm view effect-rstest versions`: **E404, not published to npm** (no versions, no dist-tags). | First npm publish of `effect-rstest`. Replace all three preview URLs with the exact published version, refresh `pnpm-lock.yaml`, re-run `pnpm test:unit` (`it.effect` / `it.live` suites). | +| 2026-09-03 | `effect` **`4.0.0-rc.112`** (`packages/agent-bundle`, `packages/rsc-runtime`, `packages/workbench`, `packages/create-agent-bundle`), `@effect/atom-react` `4.0.0-rc.112` (`packages/workbench`), `@effect/platform-node` `4.0.0-rc.112` (`packages/create-agent-bundle`), `@effect/platform-node-shared` `4.0.0-rc.112` (`packages/agent-bundle`), `@effect/language-service` `0.87.2` and `@effect/tsgo` `0.39.0` (root). Auto re-pin in lockstep + `repos/effect` subtree + Workbench atom phase 4 unblock (stream-backed derived atoms) once the post-rc.112 disposal fix ships. | `npm view effect dist-tags`: `rc` **`4.0.0-rc.112`** (unchanged), `beta` `4.0.0-beta.107`, `latest` `3.22.1`. `@effect/atom-react`: `rc` `4.0.0-rc.112`. `@effect/language-service`: `latest` `0.87.2`. `@effect/tsgo`: `latest` `0.39.1` (patch ahead of the `0.39.0` pin; rides the lockstep chore). | `effect@rc` advances past `4.0.0-rc.112`. Run the re-pin chore steps 1 to 6 above, bumping `effect`, `@effect/atom-react`, `@effect/language-service`, and `@effect/tsgo` together, then lift the stream-backed derived-atom ban in the Workbench if the disposal fix is in the new RC. | +| 2026-09-03 | Agent Plugins specification **`1.0.0`**, `packages/agent-bundle/src/adapters/schemas/portable/{plugin,mcp}.schema.json` + `PROVENANCE.json` (spec repo `agentplugins/agent-plugins-spec` @ `ff8ab5e392cc87bd88d87c060815a87490e51003`, 2026-08-19), portable `adapterRevision` `1.8.0`, pins in `tests/adapter-metadata.test.ts`. Spec watch for #426; not an npm pin, so re-verify with `curl`/`gh api`, not `npm view`. | Live `https://agent-plugins.org/schemas/1.0.0/{plugin,mcp}.schema.json` rehash to the pinned sha256 (1805 / 3408 bytes). Repo `main` HEAD unchanged at the pinned commit; **no tags, no GitHub releases**. `spec/1.1.0.md` is "Status: Working Draft" (started 2026-08-15, `a2afd7ec`); in-repo `schemas/1.1.0/*.schema.json` differ from 1.0.0 only in the `$id`/`const`/`description` version strings; `https://agent-plugins.org/schemas/1.1.0/*.schema.json` → 404. Observed latest published version: **1.0.0**. | `spec/1.1.0.md` (or later) flips to "Published" **and** `agent-plugins.org/schemas//` serves both schemas. Re-pin under `schemas/portable/` with a dated `PROVENANCE.json` (sha/bytes/date/commit), bump the portable `adapterRevision`, refresh the metadata pins, run `pnpm test:unit` (portable adapter + plugin-validation suites) and `pnpm test:host-install:build`, and add a capability row per additive field. | diff --git a/docs/entry-conventions.md b/docs/entry-conventions.md index bbf503ea5..a40eec7b3 100644 --- a/docs/entry-conventions.md +++ b/docs/entry-conventions.md @@ -37,9 +37,9 @@ package-only entries: `npm publish ./dist --ignore-scripts`. - 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/`. + `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. @@ -48,7 +48,7 @@ package-only entries: - An artifact `--output` that overlaps `dist` is rejected (`AB4706`). - The `lib` profile is deliberately thin. A package that needs a multi-format library matrix (UMD, multiple entries, per-format tsconfig) has outgrown the - profile and genuinely wants Rslib — that is the one case where a second + profile and genuinely wants Rslib, that is the one case where a second bundler config remains, by choice. ### Declarations @@ -57,7 +57,7 @@ package-only entries: from the project (add it as a devDependency) and compiles the lib entry's source directory as its own program: compiler options come from the project `tsconfig.json` (via `extends`), `rootDir` is pinned to the entry's directory, -and only that subtree is included — test files never fail or pollute the +and only that subtree is included, test files never fail or pollute the package build. Declarations land flat under `dist/`, one `.d.ts` per source module. @@ -72,14 +72,14 @@ entries carry `provenance.kind: 'conventional'` in the normalized model. | `src/index.ts` | Library output with declarations. | `lib: false` | | `src/mcp/.ts` | Stdio entry for the declared MCP server `` that names no `entry`, `command`, or `url`. | Declare `entry` explicitly | | `src/mcp//{tools,resources,prompts}/*.{ts,tsx}` | Generated MCP server routes; path supplies identity and each executable module supplies static `config`, schemas, and one default Server Component. | Set `routes.servers.` to `custom`, `command`, or `remote` | -| `src/mcp//apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required (`AB4812`), and two App routes of one server sharing a URI are `AB4829` (the same URI on different servers is not a collision). 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('')` from `agent-bundle/routes` or a shared `const` string literal instead of repeating the `ui://` literal. The view talks to its host through `createAppClient()` from the browser-safe `agent-bundle/app` (see [`agent-bundle/app`](#agent-bundleapp--the-app-side-bridge-client)), typed by the generated `AppRegister` augmentation. | Use a custom server or prefix the file with `_` | -| `src/scripts/.ts` | Plain script compiled once to `scripts/.mjs` in the plugin root, shared by every selected host — 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/.tsx` | Rendered script: the 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/.mjs` plus a `scripts/-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/mcp//apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required (`AB4812`), and two App routes of one server sharing a URI are `AB4829` (the same URI on different servers is not a collision). 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('')` from `agent-bundle/routes` or a shared `const` string literal instead of repeating the `ui://` literal. The view talks to its host through `createAppClient()` from the browser-safe `agent-bundle/app` (see [`agent-bundle/app`](#agent-bundleapp-the-app-side-bridge-client)), typed by the generated `AppRegister` augmentation. | Use a custom server or prefix the file with `_` | +| `src/scripts/.ts` | Plain script compiled once to `scripts/.mjs` in the plugin root, shared by every selected host, 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/.tsx` | Rendered script: the 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/.mjs` plus a `scripts/-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), plus the same executable as `bin/.mjs` in the plugin root whenever a selected host publishes the `cli` capability (all built-in hosts do). Nesting is identity: `src/cli/library/audit.ts` runs as ` 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//.{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; `.ts` handlers return lightweight results; `.tsx` handlers render JSX; `.ts` handlers can return `ctx.render('./name.view.js', data)` to load a separate `.view.tsx` sibling. Rendering defaults to standalone. The default Server Component receives `AgentEventRouteProps` (`{ canonical, native, signal }`) and returns `Agent.*` output that the selected host adapter encodes into its native hook envelope. `canonical.payload` is the family's cross-host reading of the envelope (#466) — the fields at least two hosts report (`toolName`, `toolInput`, `toolResponse`, `sessionId`, `transcriptPath`, `cwd`, `prompt`, `agentId`/`agentType`, `reentry`, …), each as `{ value, nativeKey }` naming the host key it came from and absent when the host did not send it; `E` narrows it to the route's family. The per-family field table is `agentEventPayloadFields` and the per-host key table `agentEventPayloadNativeKeys` (`routes/events.ts`), mirrored under `hooks.eventRoutes..payload` in each pinned capability table so the generated events reference documents the mapping per host. 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/events//.{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; `.ts` handlers return lightweight results; `.tsx` handlers render JSX; `.ts` handlers can return `ctx.render('./name.view.js', data)` to load a separate `.view.tsx` sibling. Rendering defaults to standalone. The default Server Component receives `AgentEventRouteProps` (`{ canonical, native, signal }`) and returns `Agent.*` output that the selected host adapter encodes into its native hook envelope. `canonical.payload` is the family's cross-host reading of the envelope (#466), the fields at least two hosts report (`toolName`, `toolInput`, `toolResponse`, `sessionId`, `transcriptPath`, `cwd`, `prompt`, `agentId`/`agentType`, `reentry`, …), each as `{ value, nativeKey }` naming the host key it came from and absent when the host did not send it; `E` narrows it to the route's family. The per-family field table is `agentEventPayloadFields` and the per-host key table `agentEventPayloadNativeKeys` (`routes/events.ts`), mirrored under `hooks.eventRoutes..payload` in each pinned capability table so the generated events reference documents the mapping per host. 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` | -| `src/providers/.{ts,tsx}` | Request context provider: default-exports a factory receiving `{ invocation, signal, host, session, workspace, plugin, lineage, state?, notices? }` — the request's observed identity (plugin root included) and lineage plus read-only views of the mounted state (`read`) and notice (`inbox`, `published`) handles; its value is mounted at `await (await agent()).provider("")` for generated MCP and event routes, projected MCP commands, plain and rendered routed CLI commands, and rendered scripts. | Prefix the file with `_` | -| `src/layout.{ts,tsx}` | Shared document layout: default-exports one component receiving `{ children, route, signal }` that renders `Agent.Result` around every rendered route — generated MCP tools, resources, and prompts, rendered routed CLI commands, projected MCP commands, and rendered scripts. Event routes are never wrapped. | Rename to `_layout.tsx` | +| `src/providers/.{ts,tsx}` | Request context provider: default-exports a factory receiving `{ invocation, signal, host, session, workspace, plugin, lineage, state?, notices? }`, the request's observed identity (plugin root included) and lineage plus read-only views of the mounted state (`read`) and notice (`inbox`, `published`) handles; its value is mounted at `await (await agent()).provider("")` for generated MCP and event routes, projected MCP commands, plain and rendered routed CLI commands, and rendered scripts. | Prefix the file with `_` | +| `src/layout.{ts,tsx}` | Shared document layout: default-exports one component receiving `{ children, route, signal }` that renders `Agent.Result` around every rendered route, generated MCP tools, resources, and prompts, rendered routed CLI commands, projected MCP commands, and rendered scripts. Event routes are never wrapped. | Rename to `_layout.tsx` | | `src/mcp//layout.{ts,tsx}` | Per-server layout nested inside the root layout for that generated server's routes. | Rename to `_layout.tsx`, or set `routes.servers.` to a non-generated mode | Route and package entry conventions match `.ts` and `.tsx` files exactly; @@ -96,7 +96,7 @@ from every artifact output, and the bin envelope and the artifact-script envelope run the same `main`, so a direct `src/scripts/` child a `bin` entry references stays a conventional script and ships on both surfaces. (A nested `src/scripts//` module or one whose stem is not a safe route -identity — which the flat scripts artifact could not ship anyway — stays +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 @@ -109,7 +109,7 @@ its module like every other key. | `hooks.[].handler` | every route directory | Claimed: the module is a hook handler compiled under `hooks/`, not an artifact script. | | `mcp.servers..entry`, `mcp.servers..apps..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/.js`, not an artifact script. | -| `bin.` | `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/` child | Not claimed: the module ships as both `dist/bin/.js` and `scripts/.mjs`. The module must export `main` or be self-executing: a plain `default`-only module is `AB4738`, and a rendered `src/scripts/.tsx` must export both the default component and `main`, otherwise `AB4737`. | +| `bin.` | `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/` child | Not claimed: the module ships as both `dist/bin/.js` and `scripts/.mjs`. The module must export `main` or be self-executing: a plain `default`-only module is `AB4738`, and a rendered `src/scripts/.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 @@ -118,14 +118,14 @@ while the `bin` entry still references it explicitly. ### Config beside a route-generated MCP server A `mcp.servers.` block whose `` the route graph compiles in -`generated` mode does not redeclare the server — its entry is the route -modules — it **augments** it. This is the precedence table for one generated +`generated` mode does not redeclare the server, its entry is the route +modules. It **augments** it. This is the precedence table for one generated server (config wins, conventions fill): | Field | Source of truth | Config declaration | | --- | --- | --- | | Entry, transport (`stdio`), `cwd` (plugin root) | `src/mcp//{tools,resources,prompts}/*` and the generated stdio shell | `entry`, `command`, or `url` is `AB4340` under `routes.servers.: 'generated'` and `AB4800` without an explicit mode; `transport: 'stdio'` is accepted, any other transport is `AB4308`; `cwd` is `AB4309`; `headers` is `AB4310`. | -| `env` | — | Applied verbatim beneath the injected plugin-root anchor (`AB4312` shape rules). | +| `env` | none | Applied verbatim beneath the injected plugin-root anchor (`AB4312` shape rules). | | `args` | The content-hashed entry path | Appended after the entry path (`AB4311` shape rules). | | `targets` | The project's selected targets | Replaces the default selection (`AB4305` shape rules). | | `apps` | `src/mcp//apps/*` routes | Config-side Apps are compiled and registered on the generated server beside the route-declared ones (`AB432x` rules; `AB4334` checks App targets against the declared server targets). The route-declared Apps take part in the collision checks: reusing a route App's name is `AB4325`, reusing its `resourceUri` under another name is `AB4330`. | @@ -145,9 +145,9 @@ Volatile lifetimes use the memory driver. Request lifetime opens and releases fresh project and notice stores per invocation; process lifetime shares them for the generated worker or executable process. -Workspace-durable artifact shells — generated MCP workers, artifact CLI bins +Workspace-durable artifact shells, generated MCP workers, artifact CLI bins and their render workers, rendered script workers, and standalone hook -wrappers — call `resolvePluginRoot` +wrappers, call `resolvePluginRoot` with `stateAnchor: 'user-data'`. `AGENT_BUNDLE_PLUGIN_ROOT` still names the code root and otherwise falls back to the artifact root derived from the shell's own location. An expanded, non-blank `AGENT_BUNDLE_STATE_ROOT` @@ -173,9 +173,9 @@ native or derived; `stateSource` does the same independently for the state root. An unexpanded `${…}` token in either root override is treated as unset (and reported once on stderr), never joined into a path. Notice authorization is deliberately permissive in generated mounting v1 (`authorized`); recipient/principal matching remains -enforced by the ledger — every generated scope mounts the request's `lineage` +enforced by the ledger, every generated scope mounts the request's `lineage` on the notice principal, so `recipient.conversation` / `recipient.root` are -matched against `request.lineage` on every surface — while application +matched against `request.lineage` on every surface, while application authorization policy is deferred. Each cross-request notice route is selected from the host's pinned @@ -211,14 +211,14 @@ to both the worker's `createGeneratedRuntimeState` and the server's row may name a `sensitivity` ceiling (`public | internal | secret`, absent means `internal`) with dated `sensitivityEvidence`, and the ledger withholds a notice whose author-declared `sensitivity` exceeds the ceiling of the route -about to carry it — the inbox omits it, event admission neither authorizes nor -attempts it, the signaller never announces it — recording the refusal on the +about to carry it, the inbox omits it, event admission neither authorizes nor +attempts it, the signaller never announces it, recording the refusal on the notice (`withheld[route]`) instead of moving its state. `internal` content (the default) is passed through the runtime's secret pass on every route -before it leaves the store — `flare-redact`, an exact-pinned dependency of +before it leaves the store, `flare-redact`, an exact-pinned dependency of `@agent-bundle/runtime`, with its default detectors and every finding replaced whole by `[REDACTED]`; the runtime README's notices section lists the coverage -and the libraries evaluated — `public` travels as authored; +and the libraries evaluated, `public` travels as authored; `secret` travels as authored only where the row admits it. The built-in hosts admit `secret` on `current-response` and `next-event` and `internal` on `mcp-inbox` and `mcp-resource-updated`; the pinned tables carry the dated @@ -241,13 +241,13 @@ facts of one installed store, read through `AgentNoticeLedger.inspect()`. `defineState({ ... })` accepts an optional `budgets` runtime policy. Omitted fields resolve to these fail-closed defaults: -- `maxEventBytes: 262_144` — UTF-8 bytes in the canonical JSON of each +- `maxEventBytes: 262_144`: UTF-8 bytes in the canonical JSON of each schema-validated event payload. -- `maxStateBytes: 1_048_576` — UTF-8 bytes in the canonical JSON of the +- `maxStateBytes: 1_048_576`: UTF-8 bytes in the canonical JSON of the initial state and each event, reset, or migration result. -- `maxRevisions: 100_000` — total journal revisions admitted for +- `maxRevisions: 100_000`: total journal revisions admitted for caller-initiated events and resets. -- `maxCommitMs: 5_000` — wall-clock milliseconds from mutation validation +- `maxCommitMs: 5_000`: wall-clock milliseconds from mutation validation start until the commit is ready to append. Each override must be an integer of at least 1. A definition whose initial @@ -288,10 +288,10 @@ interface AgentProviderContext { values the route will read, provenance and unavailable reasons included. `state` is present for projects that declare `src/state.ts` and `notices` for projects whose scope mounts the notice ledger; both are the real request -handles narrowed by construction to their read paths (#459) — `inbox()` is +handles narrowed by construction to their read paths (#459), `inbox()` is what is pending for this request's principal, `published()` what became of -the notices it published (#460) — so a provider can expose a derived view of -shared state — a topology, a summary, a peers list — but never dispatch a +the notices it published (#460), so a provider can expose a derived view of +shared state, a topology, a summary, a peers list, but never dispatch a state event or publish, acknowledge, or withdraw a notice: those stay route-only. Providers also run outside the request's async context, so `agent()` and `useAgent()` inside a factory throw `outside-invocation` rather @@ -324,7 +324,7 @@ spawn the artifact. ### Shared layouts A layout is the conventional composition point around every rendered route -of a project — the `layout.tsx` idea from page frameworks, applied to Agent +of a project, the `layout.tsx` idea from page frameworks, applied to Agent Documents. `src/layout.{ts,tsx}` wraps every rendered route (generated MCP tools, resources, and prompts; rendered `src/cli/**` commands; projected MCP commands; rendered `src/scripts/*.tsx`), and `src/mcp//layout.{ts,tsx}` @@ -349,15 +349,15 @@ export default function Layout({ children, route }: AgentLayoutProps) { The layout renders `Agent.Result` around `children`, the route's rendered element. An `Agent.Result` that declares no `value` is a **container**: when -it directly holds a result that does carry a value — the route's own -`` — the runtime merges the two while decoding the +it directly holds a result that does carry a value, the route's own +``, the runtime merges the two while decoding the document. The route's value becomes the document value, its children take the inner result's place, and `metadata` combines: two JSON objects merge key by key with the container winning conflicts, any other shape lets the container win outright, and a container without metadata adopts the inner one. A route therefore keeps its result value, its `structuredContent`, and its rendered content whether or not a layout exists; what the layout adds is the shared -shell around it — a heading, a trailing `Agent.Context` note, document +shell around it, a heading, a trailing `Agent.Context` note, document metadata. Because the MCP projector exposes root metadata as the result's `_meta`, a layout that declares metadata does change the MCP response there; a layout without metadata leaves `_meta` exactly as the route authored it. @@ -367,9 +367,9 @@ document contract under a layout exactly as it does without one. The generated worker resolves the route's element **before** the layout chain renders, then wraps it. That keeps failure semantics identical with and -without a layout — a route that throws rejects the whole render (CLI exit 1 +without a layout, a route that throws rejects the whole render (CLI exit 1 with the route's message; on MCP the SDK's default `isError` tool result or a -JSON-RPC error, with no layout `_meta` — see +JSON-RPC error, with no layout `_meta`, see [What happens when a route throws](framework-mode.md#what-happens-when-a-route-throws)) instead of being downgraded to a represented `boundary` error beneath the layout's shell. The trade-off is @@ -407,7 +407,7 @@ convention rather than a property of the module. Conventional route components receive only their surface props, such as `{ input, signal }`. They read transport-owned request context with -`await agent()` from `@agent-bundle/runtime` — or, in a synchronous component +`await agent()` from `@agent-bundle/runtime`, or, in a synchronous component or utility, `useAgent()`, which returns the identical handle under the same lease rules without suspending. The handle exposes the invocation plus `host`, `session`, `actor`, and `workspace` identity axes. @@ -421,9 +421,9 @@ axes remain honestly unavailable. `actor` is the HTTP-authenticated MCP client and nothing else: hook-driven event scopes observe it as unavailable rather than receiving a fabricated value, and the framework never derives an operator identity (a signed-in user, an email) from any host payload or -environment — hosts that send one (Cursor's `user_email`) have it passed -through inside `native` untouched and unread. Who the *conversation* is — -its parent, its root, whether it is a subagent — is the +environment, hosts that send one (Cursor's `user_email`) have it passed +through inside `native` untouched and unread. Who the *conversation* is, +its parent, its root, whether it is a subagent, is the [`lineage` axis](#conversation-lineage-requestlineage). Handlers authored with `defineTool` receive parsed schema output and the request context as @@ -457,7 +457,7 @@ surface-specific `invocation.kind` the generated executable would use. conversation, and are we a subagent (of whom)?". It is the only place the framework places a request in the host's conversation tree; there is no separate operator or user identity axis, by design. `(await agent()).lineage` -is an `Observed` with one shape on every surface — event routes, +is an `Observed` with one shape on every surface, event routes, generated MCP tools, routed CLI commands, and rendered scripts: ```ts @@ -490,8 +490,8 @@ interface AgentLineagePeer { `tree` is the other half of "where am I": the live conversations around this one, read from the same registry that placed the request (#457). It lists -only what the registry holds — no node is invented, and a stopped node is not -listed — scoped to what the conversation may see: everything alive under its +only what the registry holds, no node is invented, and a stopped node is not +listed, scoped to what the conversation may see: everything alive under its own root (`siblings`, oldest first, the root itself included for a subagent, so a coordinator sees the whole live tree it belongs to; filter by `parent` for same-parent siblings), its direct `children`, and the other live `roots` @@ -505,14 +505,14 @@ still answers "who am I" but not "who else is here". It travels as plain frozen data, so the Flight worker receives it unchanged and route-unit tests inject it through the same `context.lineage` seam. -`resolution` is the trust level of `parent`/`root`/`depth`: `native` — the +`resolution` is the trust level of `parent`/`root`/`depth`: `native`, the host named them on this payload (a Claude/Codex root, a Codex tool call's -`_meta`); `registry` — the warm runtime's registry placed the conversation +`_meta`); `registry`, the warm runtime's registry placed the conversation when its subagent started, matching the start to the newest unclaimed spawn -call; `confirmed` — that registry edge, and every edge above it up to the +call; `confirmed`, that registry edge, and every edge above it up to the root, was afterwards named by the host itself (Claude's `Agent` PostToolUse carries the spawn `tool_use_id`, the caller's identity and -`tool_response.agentId`, the child); `inferred` — ordering inference the host +`tool_response.agentId`, the child); `inferred`, ordering inference the host forced (Cursor binds a child conversation to the single pending `subagentStart`). @@ -536,7 +536,7 @@ for every event by the id the payload carries. The observed host vocabulary A Claude subagent is placed only when its spawning pre-tool hook (`Agent`/`Task`) was observed, so projects that want `parent`/`depth` for subagents route `tool/before` alongside `agent/start`; a start with no -claimable spawn — none open, or several parents with one — stays +claimable spawn, none open, or several parents with one, stays `id-not-resolvable`, and the registry keeps what the start said (id, type, time, and a stop that follows) as an unplaced start. @@ -544,14 +544,14 @@ A Codex thread carries its own evidence: the rollout its payload names opens with a `session_meta` line recording the spawning thread, the depth, and the `agent_path`, so the registry places it from that file (`resolution: 'transcript'`, provenance `derived`) at `SubagentStart`, on the first hook of -a thread it never saw start (or held unplaced), and at `SubagentStop` — no +a thread it never saw start (or held unplaced), and at `SubagentStop`, no spawn ordering involved, and two parents with unclaimed spawns are no longer ambiguous. The `spawn_agent` call is still claimed, by parent and by the `agent_path` its `PostToolUse` `tool_response.task_name` carried, so `subagent.toolCallId` is exact even for same-parent siblings. Only when the rollout is unreadable does Codex fall back to the Claude rule (`resolution: 'registry'`), and a parent inferred that way is corrected at `SubagentStop`, -— from the child's own rollout (`agent_transcript_path`, exact parent and +from the child's own rollout (`agent_transcript_path`, exact parent and depth) when it is readable by then, else from the parent rollout in `transcript_path` (`rollout--.jsonl`); descendants move with it. @@ -607,8 +607,8 @@ elimination and refuses when elimination is not possible: stays `id-not-resolvable` until all but one have stopped; after a registry restart it stays unresolved, because nothing distinguishes it from a root. - A blind binding is undone the moment the bound conversation carries any - root-shaped event (`prompt/submit`, `stop`, `session/end`, `compact/*`) — - subagents never do — so a second chat tab whose prompt predates the registry + root-shaped event (`prompt/submit`, `stop`, `session/end`, `compact/*`), + subagents never do, so a second chat tab whose prompt predates the registry (Cursor desktop restarts mid-conversation, and many conversations are first seen on a tool hook) becomes the root it is, anything it started meanwhile is re-rooted beneath it, and the pending child waits for its real conversation @@ -623,17 +623,17 @@ storeless registry keeps an in-memory ledger of applied keys). A project with several generated MCP servers attaches its event routes to one of them; the others resolve tool calls by re-reading the shared journal, so their `request.lineage` is populated only when the project's state is -workspace-durable — volatile and stateless multi-server projects report +workspace-durable, volatile and stateless multi-server projects report `id-not-resolvable` from the servers that host no event routes. `resolution` says which of those paths produced the answer. When none can, the axis is `unavailable` with a typed reason: `no-subagent-events` (the -target defines no subagent families — portable), `id-not-resolvable` (the +target defines no subagent families, portable), `id-not-resolvable` (the payload names an agent the registry never saw start, e.g. a cold runtime), `cloud-agent-no-user-hooks` (Cursor cloud agents run no user hooks), `no-shared-runtime` (a standalone hook process holds no registry; Claude and Codex root payloads still resolve to depth 0 from the payload alone, and a -Codex subagent payload resolves fully from the rollout it names — +Codex subagent payload resolves fully from the rollout it names, `resolveStandaloneLineage`), `unsupported-surface` (routed CLI and rendered scripts run outside any host conversation), or `not-provided` (no registry was mounted). Per-host @@ -652,7 +652,7 @@ process's output streams are, computed **once per invocation by the framework shell** with the same rules that pick the CLI output mode, so a route that colors its own stderr or sizes its own table agrees with the framework's rendering instead of re-probing `process.stdout` per plugin. It is information -only — never a writer — and it never changes what `Agent.*` components render. +only, never a writer, and it never changes what `Agent.*` components render. ```ts interface AgentTerminal { @@ -675,7 +675,7 @@ into emitted executables as `agent-bundle/terminal-capability`) reads the descriptors (`tty`; any other open descriptor is `pipe`; a closed one is `none`; `sharesTarget` compares device and inode), and resolves color in the informal standards' precedence: `FORCE_COLOR` decides outright when set -(`0`/`false` off; empty, `1`, or `true` basic; `2` 256; `3` truecolor — Node's +(`0`/`false` off; empty, `1`, or `true` basic; `2` 256; `3` truecolor, Node's reading), then `CLICOLOR_FORCE` forces color on even for a pipe at the depth `COLORTERM`/`TERM` advertise, `NO_COLOR` (any non-empty value) and `CLICOLOR=0` force it off, `TERM=dumb` renders none, and otherwise a terminal renders at its @@ -690,10 +690,10 @@ Per surface, the value the generated request scope mounts: | --- | --- | --- | --- | | Routed CLI executable (npm-root and plugin-root `bin/.mjs`), plain or rendered command, projected MCP command | `cli` | Probed from the executable's own process; a rendered command's worker thread receives the executable's probe, never its own pipes. Machine output owns fd 1, so `stdout` describes where the rendered document lands and `stderr` the channel a route may write to itself. | `native` | | Rendered script (`scripts/.mjs` from `src/scripts/.tsx`) | `script` | Probed, as above. | `native` | -| Generated MCP server (any transport) | `mcp` | `none` on both, `color: 'none'`, `sharesTarget: false` — stdout is the protocol wire and stderr the host's log. Never probed, whatever the descriptors are. | `derived` | -| Event route (shared runtime or standalone hook process) | `hook` | `none` on both — stdout is the host's hook envelope. Never probed. | `derived` | -| Workbench lifecycle replay | `workbench` | `none` on both — the document renders into a panel. | `derived` | -| Custom host calling `runAgentRequest` without `terminal` | — | `unavailable` (`not-provided`) | — | +| Generated MCP server (any transport) | `mcp` | `none` on both, `color: 'none'`, `sharesTarget: false`, stdout is the protocol wire and stderr the host's log. Never probed, whatever the descriptors are. | `derived` | +| Event route (shared runtime or standalone hook process) | `hook` | `none` on both, stdout is the host's hook envelope. Never probed. | `derived` | +| Workbench lifecycle replay | `workbench` | `none` on both, the document renders into a panel. | `derived` | +| Custom host calling `runAgentRequest` without `terminal` | none | `unavailable` (`not-provided`) | none | Plain `main`-exporting scripts and bins have no request scope, so the executable envelope hands them the same probe directly as the second argument @@ -711,7 +711,7 @@ real child process's probe (two pipes). A test that wants other values injects ### Migration nudges -Source validation reports **informational** nudges (never errors — migrations +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 @@ -735,7 +735,7 @@ synthesis and invariant assertions as all generated executables. ### The executable envelope (bin + Scripts) -A `bin` entry — or an artifact `Script` — whose module exports `main` (or a +A `bin` entry, or an artifact `Script`, whose module exports `main` (or a default function for bin entries) receives the generated process envelope: ```ts @@ -753,14 +753,14 @@ numeric return as the process exit code, and lets an escaped rejection surface through Node's top-level failure path (stack to stderr, exit code 1). `terminal` is the process's [terminal capability](#terminal-capability-requestterminal) (#511), probed once before `main` runs by the dependency-free -`agent-bundle/terminal-capability` module the envelope aliases in — plain +`agent-bundle/terminal-capability` module the envelope aliases in, plain scripts and bins load no Effect runtime and no `@agent-bundle/runtime` for it. Its `hostSurface` is `cli` for a package bin (`dist/bin/.js` for an authored bin, `dist/bin/.mjs` for the manifest-selected routed CLI) and `script` for an artifact script (`scripts/.mjs`); a module shipped on both surfaces sees the surface it was launched from. A `main` declared with -one parameter keeps working — the second argument is simply unread. -Self-executing modules (no `main` export) bundle directly, byte for byte — +one parameter keeps working, the second argument is simply unread. +Self-executing modules (no `main` export) bundle directly, byte for byte, existing Scripts keep their behavior and receive no probe. ### The routed CLI shell (#102 stages 2-3) @@ -810,7 +810,7 @@ stdout, and maps exit codes deterministically (0 success or the result's or input failure; 130/143 on SIGINT/SIGTERM, which reach the route's `AbortSignal`). `--json` is accepted on every command; plain commands already emit the canonical JSON document. Routed CLI projects need -`@agent-bundle/runtime` as a dependency — the generated executable installs +`@agent-bundle/runtime` as a dependency, the generated executable installs the request context through it. Runtime schemas may be imported or constructed by factories. Inspection reads literal @@ -848,7 +848,7 @@ invocation; raw Flight bytes never reach the terminal). The four output modes: an interactive TTY updates progress in place and prints the final document as Markdown; piped output emits exactly one final Markdown document with no partial fallbacks; `--json` emits the canonical validated final -value; `--ndjson` emits the sequence-numbered render-event stream — an +value; `--ndjson` emits the sequence-numbered render-event stream, an Agent Bundle CLI/script output dialect, not MCP JSON-RPC, and never written as non-MCP bytes to an MCP server's stdout. Diagnostics stay on stderr; machine output owns stdout. Rendered scripts @@ -859,8 +859,8 @@ machine output owns stdout. Rendered scripts Hooks, skills, script routes, and npm consumers all use the **plugin root**, so the build emits one compiled command graph there whenever a selected host's -adapter publishes the `cli` capability — all built-in hosts do (`claude`, -`codex`, `cursor`, `portable`) — because the artifact root is +adapter publishes the `cli` capability, all built-in hosts do (`claude`, +`codex`, `cursor`, `portable`), because the artifact root is already a plain directory Node executes `mcp/` and `scripts/` files from. The same `bin/.mjs` is also emitted when `web` is configured, even if `src/cli/**` compiled no commands (the bin then carries only the @@ -934,7 +934,7 @@ export default defineConfig({ ``` `true` selects every eligible tool. A tool that already has a colocated -`.cli.{ts,tsx}` is not eligible — the explicit projection is the +`.cli.{ts,tsx}` is not eligible, the explicit projection is the command for that operation. Object-form `include` defaults to every eligible tool when omitted; `exclude` removes matches afterward. Patterns match the `:` identity and support only literal text plus `*` @@ -959,8 +959,8 @@ A tool whose validated result carries an integer `exitCode` can declare domain failures exit nonzero. Other projected tools retain the success-status policy and exit zero only for a successful rendered document. -Every rendered route — generated MCP tool, resource, or prompt, projected MCP -command, rendered `src/cli/**` command — runs inside one render session whose +Every rendered route, generated MCP tool, resource, or prompt, projected MCP +command, rendered `src/cli/**` command, runs inside one render session whose wall clock defaults to `DEFAULT_AGENT_RENDER_LIMITS.maxElapsedMs` (60 s). A route whose legitimate work runs longer declares `config.render: { maxElapsedMs }` (#454): a positive integer of milliseconds up to @@ -992,26 +992,26 @@ The module exports a static `config` that satisfies `CliProjectionConfig` from `agent-bundle/routes` (the same extract grammar as a route `config`) and, optionally, a sync or async `mapInput`: -- `command` — path segments; default `[tool]`; each must pass +- `command`: path segments; default `[tool]`; each must pass `safeIdentitySegment`. -- `description` — help text; default: the tool's `config.description`. -- `positionals` — canonical keys consumed as bare arguments, in order +- `description`: help text; default: the tool's `config.description`. +- `positionals`: canonical keys consumed as bare arguments, in order (same rules as a `src/cli` route). -- `flags` — keyed by the canonical key of the tool's +- `flags`: keyed by the canonical key of the tool's `RouteContract.input`. Each entry may set `name` (CLI spelling, kebab-case, no leading dashes; default `kebab(key)`), `aliases` (extra long-form spellings), `description` (overrides schema `.describe()`), `default` (CLI-only, applied by the shell before `mapInput`), and `required: false` (relax a canonical-required key; legal only when `mapInput` is exported). -- `aliases` — command aliases (same rules as a `src/cli` route). -- `confirm` — default `!(tool config.annotations.readOnlyHint === true)`. -- `exitCode` — `'result'` or `'zero'`; default: the tool's +- `aliases`: command aliases (same rules as a `src/cli` route). +- `confirm`: default `!(tool config.annotations.readOnlyHint === true)`. +- `exitCode`: `'result'` or `'zero'`; default: the tool's `config.exitCode ?? 'zero'`. -- `input` — `'json'` (#746): the command takes the tool's canonical input +- `input`: `'json'` (#746): the command takes the tool's canonical input as one JSON object through `--input`, exactly as the bulk `mcpCommands` projection does (`parseMcpCommandInput` in `cli-entry.ts` runs the same - JSON path for both), and never reads the tool's argv grammar — the mode + JSON path for both), and never reads the tool's argv grammar, the mode for a schema the grammar cannot express (a nested object, a union, a transform), which a flag-bound projection over it reports as `AB4814` with a recovery that names this mode. `compileProjectedCliCommands` @@ -1028,7 +1028,7 @@ is recorded statically (`scanRouteModuleExports`) and loaded only by the CLI bin; the MCP worker never sees the module. `mapInput` is a surface adapter, not domain logic: it only reshapes or defaults argv into the canonical input (renames, splitting lists, deriving a working directory). -Domain validation and behaviour stay in the operation — its +Domain validation and behaviour stay in the operation, its `inputSchema` refinements and its component. A mapper that recreates command logic is the duplication the projection exists to remove. A contract problem is `AB4844`; a grammar that does not bind to the tool's @@ -1043,7 +1043,7 @@ id; at run time the tool runs with (`tool:/`), so a route can pick surface wording from `agent().invocation.kind` while the operation stays the tool. A route observes `kind: 'cli'` whenever it runs from the generated CLI -executable, whichever projection mechanism produced the command — the +executable, whichever projection mechanism produced the command, the bulk `--input` projection is a CLI surface too. The generated MCP server still passes `kind: 'tool'`. `inspect --routes` prints `cli.commands[].projection` @@ -1063,7 +1063,7 @@ export default () => new McpServer({ name: 'curator', version: '1.0.0' }); The generated shell provides, in order: console-to-stderr redirection before the consumer module evaluates (installed by the shell's first import, its -stdio prelude — see below), the factory call, raw `process.stdout.write` +stdio prelude, see below), the factory call, raw `process.stdout.write` restored for protocol frames, `StdioServerTransport` construction and connect, SIGINT → exit 130, SIGTERM → exit 143, stdin EOF → exit 0 (so the client can respawn), transport-close → exit 0, a 5-second bounded shutdown @@ -1071,8 +1071,8 @@ race against wedged transports, and heartbeat/activity logging on stderr (5-minute interval, 60-second activity throttle, labeled with the server name). -Self-connecting entries — modules that construct and connect a transport at -top level without a default export — keep today's behavior byte for byte: no +Self-connecting entries, modules that construct and connect a transport at +top level without a default export, keep today's behavior byte for byte: no lifecycle shell and no operator `.env` layer (#469); an entry that wants the layer calls `applyOperatorEnv` from `agent-bundle/launch-env` itself, passing its own declared `env` block as `manifestEnv` if a passed-through @@ -1109,7 +1109,7 @@ The Workbench Lifecycles view exposes the request context used for each deterministic replay. Host, session, actor, and workspace are separate observed axes beside invocation kind, operation, surface, and host-contract revision. Values parsed from the checked-in or pasted native receipt use the -`receipt` source — never `native`, because a Workbench replay is not evidence +`receipt` source, never `native`, because a Workbench replay is not evidence that the named host dispatched the event. A missing session, actor, or workspace remains visibly `unavailable` with its typed reason. @@ -1118,7 +1118,7 @@ rendering, so `await agent()` and the Workbench evidence panel describe one context rather than parallel snapshots. User-edited business input cannot replace these axes. -## `agent-bundle/meta` — build-time release identity +## `agent-bundle/meta`, build-time release identity Plugin code reads its own identity from the framework instead of maintaining a hand-written `src/lib/version.ts`: @@ -1129,7 +1129,7 @@ import meta, { name, packageName, packageVersion, version } from 'agent-bundle/m `version` is the resolved plugin version: the authored `plugin.version` when declared, otherwise the `package.json` version. `name` is the host-native -plugin slug — never the npm package name. `packageName` and `packageVersion` +plugin slug, never the npm package name. `packageName` and `packageVersion` are the validated npm axes, `undefined` for an unpackaged development project. Every value is exactly what artifact manifests, `inspect`, and dev status report for the same build. @@ -1142,7 +1142,7 @@ and no emitted bundle can still carry an unresolved import of it. Types ship with the package export, so no generated declaration file is involved. Outside Agent Bundle compilation the published module throws the -`AB4760` diagnostic rather than reporting a fabricated identity — a plugin +`AB4760` diagnostic rather than reporting a fabricated identity, a plugin slug exists only in the config, and a runtime guess at it would silently disagree with the artifact. A release build refuses a project with no release version at all (`AB4013`), so a compiled artifact never carries the @@ -1153,8 +1153,8 @@ Tests are not outside the compiler: `agentBundleRstest()` and `.agent-bundle/test/meta.mjs`, a module generated by the same `generatedMetaModuleSource` the build injects, fed from the same compiler pass's plugin identity. A source module importing `agent-bundle/meta` therefore -loads under any Rstest pool built from the preset — plain unit tests, the -route-unit level, `renderRoute`, and `invokeCli` alike — with the identity +loads under any Rstest pool built from the preset, plain unit tests, the +route-unit level, `renderRoute`, and `invokeCli` alike, with the identity `package.json` and `agent-bundle.config.ts` declare. A custom runner that does not use the preset must add the same alias; the `AB4760` recovery text spells it out (see [Diagnostics](diagnostics.md#build-time-identity-outside-the-compiler-ab4760)). @@ -1167,11 +1167,11 @@ normalization is about to stamp into the model (`plugin.name`, the prints the one its artifact manifest reports, under `validate`, `build`, `inspect`, dev, the Workbench's source documents, and `inspectWorkbenchSurface`. -## Prebuilt payloads — package what you compiled yourself +## Prebuilt payloads, package what you compiled yourself -Some projects legitimately own their compilation — a coordinated +Some projects legitimately own their compilation, a coordinated multi-environment bundler topology the per-entry `tools` hatch cannot -express — but still want framework-owned host packaging (manifests, hook +express, but still want framework-owned host packaging (manifests, hook documents, env anchors, provenance, validation). The `payload` block declares already-built directory trees the build packages **as-is**, and the `{ prebuilt: ... }` marker points MCP entries and hook handlers at files @@ -1211,8 +1211,8 @@ export default defineConfig({ - **Stable paths, not content-hashing.** Every payload file keeps its exact relative path under the destination directory. The framework did not - compile these files, so it cannot rewrite the references inside them — - sibling chunk imports, worker entries resolved from `import.meta.url` — + compile these files, so it cannot rewrite the references inside them, + sibling chunk imports, worker entries resolved from `import.meta.url`, and hosts, manuals, and tests pin the entry paths. Integrity stays content-addressed anyway: each payload file lands in the artifact manifest with its SHA-256 and the `prebuilt` file kind, and the payload files hash @@ -1225,7 +1225,7 @@ export default defineConfig({ `cwd: "./"`, `${PLUGIN_ROOT}/…`), the `AGENT_BUNDLE_PLUGIN_ROOT` env anchor is injected as usual, and artifact validation confirms the referenced file is present and manifested. A prebuilt hook emits its - native command as `node "/" ` — one config + native command as `node "/" `, one config declaration replaces a hand-rolled `hooks/hooks.json` per host. Prebuilt hook `args` (for example `--host claude`) accept shell-safe strings only. - **Prebuilt means opaque.** Payload files are exempt from generated-output @@ -1249,7 +1249,7 @@ export default defineConfig({ empty payload is a validation warning (`AB4743`/`AB4745`) so `dev` works from a clean checkout, but `agent-bundle build` refuses it (`AB4747`/`AB4748`). Payload directories must not overlap the artifact - `--output` root (`AB4749`) — with payloads under `dist/`, pass an output + `--output` root (`AB4749`), with payloads under `dist/`, pass an output like `dist/plugins`. See `docs/diagnostics.md` for the full `AB474x` table. @@ -1258,10 +1258,10 @@ owns a three-environment RSC compilation, and `agent-bundle build` packages the resulting `dist/runtime` and `dist/app` trees into the Claude, Codex, and portable artifacts. -## `tools` — THE escape hatch +## `tools`, THE escape hatch `tools.rsbuild` (an Rsbuild environment-config fragment) and `tools.rspack` -(an Rspack config object, mutator function, or array — Rslib semantics) merge +(an Rspack config object, mutator function, or array, Rslib semantics) merge **last** into every bundler config agent-bundle synthesizes: artifact scripts, MCP entries, hook wrappers, MCP App views, and the package build. This mirrors Rslib's user-config-highest priority and Rspress's `builderConfig` position, @@ -1284,7 +1284,7 @@ layer re-pins `output.autoExternal: false` after the hatch merge. Reserved module specifiers are protected the same way: a hatch that externalizes `agent-bundle/mcp-entry` or a generated module specifier (`agent-bundle/meta`, or a registry specifier such as `agent-bundle/mcp-apps`) fails the build with -a hard diagnostic — at config inspection for statically visible `externals`, +a hard diagnostic, at config inspection for statically visible `externals`, and from the emitted bundle's residual imports for function-form `externals`. The hatch customizes *how code compiles*, never *what the artifact promises*. The framework's own profile keeps the same promise: `output.autoExternal` is `false`, `bundle: @@ -1312,9 +1312,9 @@ verbatim. The hatch merges *beside* the framework profile, not over it: `plugins` arrays concatenate, and Rsbuild's plugin manager appends every plugin it is handed without deduping by name. So a `tools.rsbuild.plugins` entry that -re-adds a plugin the framework already registers — `@rsbuild/plugin-react` +re-adds a plugin the framework already registers, `@rsbuild/plugin-react` (`rsbuild:react`), carried by every synthesized Rslib entry and every MCP -App view — would run it twice. `agent-bundle validate` +App view, would run it twice. `agent-bundle validate` reports that as `AB4724` (an error, like the other `tools` shape checks) with the plugin and package name; remove the entry, the framework already registers it. @@ -1329,8 +1329,8 @@ dependency, not the consumer's: a class imported from a separately installed `@rspack/core` can have a different identity than the engine executing the config. So a hatch must never construct plugins or run `instanceof` checks against an imported `@rspack/core`. Use instead the utils argument -Rslib/Rsbuild pass to `tools.rspack` mutator functions — -`tools: { rspack: (config, { rspack }) => { ... } }` — which always hands the +Rslib/Rsbuild pass to `tools.rspack` mutator functions, +`tools: { rspack: (config, { rspack }) => { ... } }`, which always hands the engine's own `rspack` object. ### `agent-bundle inspect` component accounting @@ -1339,9 +1339,9 @@ engine's own `rspack` object. agent-bundle inspect [--target ] [--json] ``` -Every inspection plan accounts for each host component the project declares -— skills, commands, rules, config-declared hooks, filesystem event routes, -LSP servers, MCP servers, MCP Apps, and scripts — as either `selected` +Every inspection plan accounts for each host component the project declares, +skills, commands, rules, config-declared hooks, filesystem event routes, +LSP servers, MCP servers, MCP Apps, and scripts, as either `selected` (emitted for that target) or `skipped` (omitted), in one deterministic order. Each component carries its canonical `kind` (`AgentComponentKind`); event routes report as `event-route`, judged by the host's row for their canonical @@ -1351,8 +1351,8 @@ A skipped component names its cause: `excluded-by-targets` when the author's `targets` left the host out, or `unsupported-capability` when the host's pinned capability table does not support the surface. Components that need a host capability carry that target's own four-state judgment as -`capability` — `{ name, state: 'supported', evidence }` for emitted surfaces, -or `{ name, state: 'degraded' | 'unavailable' | 'prohibited', reason }` — so +`capability`, `{ name, state: 'supported', evidence }` for emitted surfaces, +or `{ name, state: 'degraded' | 'unavailable' | 'prohibited', reason }`, so the JSON explains why a Cursor rule is absent from a Claude bundle in the host's words rather than the compiler's. An adapter that publishes no row for a needed capability reads as an honest `unavailable`, never a silent pass. @@ -1364,7 +1364,7 @@ warnings `validate` reports (see [Component feature sets](framework-mode.md#component-feature-sets)). Every plan also carries `kinds`: one entry per canonical kind, in kind order, with the target's own row for that kind (`capability`) and the counts of selected and skipped -components of it — so a host with no `lsp`, `native-diagnostics`, +components of it, so a host with no `lsp`, `native-diagnostics`, `native-extension`, or `agent` surface says so in its own words even when the project declares none of them (`script` and `event-route` carry no kind-level row; event routes are judged per component). The human output prints one line @@ -1380,8 +1380,8 @@ agent-bundle inspect --bundler [--target ] [--json] ``` Dumps the synthesized bundler configuration for every output the build -composes — artifact scripts, MCP entries, hook wrappers, the composite root's -MCP Apps Rsbuild config, and the `dist/` package build — exactly as the build +composes, artifact scripts, MCP entries, hook wrappers, the composite root's +MCP Apps Rsbuild config, and the `dist/` package build, exactly as the build lowers it: in production mode whatever `NODE_ENV` says, the framework profile with the consumer `tools` hatch merged over it and the invariant hook appended last (functions render as `[function ]`). Entries the framework wraps also carry the generated @@ -1406,14 +1406,14 @@ 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` +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 +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 +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 @@ -1435,9 +1435,9 @@ temporary artifact is built first. ### Launch environment -The runner loads the project-root `.env` set by default — rsbuild's `loadEnv` +The runner loads the project-root `.env` set by default, rsbuild's `loadEnv` conventions (`.env`, `.env.local`, `.env.`, `.env..local`, with -`--mode` selecting the variants) — so operator credentials configured for the +`--mode` selecting the variants), so operator credentials configured for the plugin reach a bare `mcp run` without a wrapper script. This is a launch-time-only layer: the framework's programmatic Rslib builds never pass `loadEnv` to `createRslib`, so `agent-bundle build` and the package build read @@ -1457,10 +1457,10 @@ canonical precedence order (highest wins): | 1 (lowest) | Manifest env | Entries declared in the server config plus the injected plugin-root anchor, path tokens expanded. | Installed packs get the same layer and the same order without `mcp run` -(#469): every artifact shell that runs plugin code — the stdio MCP entry of a +(#469): every artifact shell that runs plugin code, the stdio MCP entry of a factory-exporting server (a self-connecting entry has no shell), the hook wrappers that execute handlers or render standalone, and the artifact CLI -`bin/.mjs` — applies `agent-bundle/launch-env` (`src/launch-env.ts`, +`bin/.mjs`, applies `agent-bundle/launch-env` (`src/launch-env.ts`, plain Node, inlined into the bundle) at startup. It reads `/.env` then `.env.local`, where the plugin root is the expanded `AGENT_BUNDLE_PLUGIN_ROOT` or the shell's parent directory, or the files @@ -1485,14 +1485,14 @@ Two details keep the installed order equal to the `mcp run` table above: from that default: a passed-through default yields to the file, a host or operator export is kept. The accepted ambiguity: an operator export equal to the manifest default reads as the default and yields too. A default carrying - a path token never equals its host-expanded value and is always kept — this + a path token never equals its host-expanded value and is always kept. This covers the injected `AGENT_BUNDLE_PLUGIN_ROOT`. Hook wrappers and the CLI bin have no manifest env and embed none. The host manifests are unchanged; hosts still show `env` in their UIs. - **The layer precedes every consumer module.** Rspack inlines the modules of a single-chunk bundle into one scope, evaluates all of them before the entry module's own body, and places a dynamic import's target ahead of the - static ones — so neither a statement in the shell body nor an awaited + static ones, so neither a statement in the shell body nor an awaited `import()` after it runs before a consumer module's top level. The layer is instead a generated virtual module that each shell imports first, and the server module, hook handler, routes, providers, and state definition are @@ -1502,7 +1502,7 @@ Two details keep the installed order equal to the `mcp run` table above: legitimately write stdout. The stdio MCP shell imports its prelude instead (`agent-bundle/stdio-prelude`, `src/build/entry-shell.ts`), which calls `redirectConsoleToStderr` from `agent-bundle/mcp-entry` and then applies - the layer — stdout is the protocol wire there, and the same ordering + the layer, stdout is the protocol wire there, and the same ordering argument means only an earlier import can put the guard ahead of a `console.log` or `process.stdout.write` at the server module's top level. The guard has one implementation: while a guard is installed, @@ -1525,8 +1525,8 @@ Two details keep the installed order equal to the `mcp run` table above: Under `mcp run` the artifact is an ephemeral build product, so both durable-state anchors point at the project root: state anchored on the plugin-data token persists under `.agent-bundle/mcp-run//`, -and plugin-root tokens in *env values* — including the injected -`AGENT_BUNDLE_PLUGIN_ROOT` anchor — expand to the project root itself. +and plugin-root tokens in *env values*, including the injected +`AGENT_BUNDLE_PLUGIN_ROOT` anchor, expand to the project root itself. Targets without token interpolation (Codex serializes the anchor as a `./` path) re-anchor their relative env values against the same durable root. `args` and `cwd` stay artifact-rooted (the first argument is the @@ -1566,8 +1566,8 @@ host, not a deployment target. `serveApp` in `agent-bundle/api` is the programmatic form (`{ url, close, closed }`). It is a host-process API: it belongs to processes the framework -does not compile — the first-party CLI, the Workbench, tests, a plugin's own -`package.json` scripts or a hand-written `.mjs` run from the checkout — and +does not compile, the first-party CLI, the Workbench, tests, a plugin's own +`package.json` scripts or a hand-written `.mjs` run from the checkout, and never to the MCP server shell. A routed CLI command inside the artifact cannot import it: routed CLI bins are self-contained (#387), so the bundler would inline `agent-bundle/dist/api.js` into the bin and fail on the @@ -1585,7 +1585,7 @@ installed artifact the supported command is ` web` on `bin/.mjs` (emitted when `web` is configured, even with no `src/cli/**` commands). It reads the manifest `web` section beside `bin/`, launches the plugin's own packed MCP server, and prints the same ready line -`MCP App / at (tool ; Ctrl-C stops the server)` — +`MCP App / at (tool ; Ctrl-C stops the server)`, `src/serve-app/command-contract.ts` is the shared contract for that line and for `--json` `{ app, server, tool, url, port, resourceUri, sandboxOrigin }`. `agent-bundle/serve-app-command` (`spawnServeApp`) is removed. The worked @@ -1629,7 +1629,7 @@ without one, every candidate's normalized launch descriptor (command, arguments, cwd, declared env, runtime binding) is compared, materially identical launches proceed unprompted whatever the host order, and materially different ones answer 409 naming the choices. No portable -projection or `mcp.json` is required — a Claude- or Codex-only build opens +projection or `mcp.json` is required, a Claude- or Codex-only build opens `/web//` from its own projection. Web sessions are cached by epoch, server, and resolved launch identity; a successful rebuild retires unused sessions of older epochs (pages still leasing one keep it until @@ -1646,15 +1646,15 @@ lineage journal likewise use `stateAnchor: 'user-data'` and live under unless `AGENT_BUNDLE_STATE_ROOT` overrides it, so a read-only install still launches. -## `agent-bundle/app` — the App-side bridge client +## `agent-bundle/app`, the App-side bridge client `agent-bundle/app` (`src/app/index.ts`, #594) is the half of the MCP Apps bridge that runs inside the App document. It is a browser-safe leaf of the package: its only imports are the shared protocol-version constant (`src/contracts/mcp-app-protocol.ts`, which `src/dev/mcp-app-profile-descriptors.ts` re-exports for the host side, so -`APP_PROTOCOL_VERSION` and the host's `MCP_APP_PROTOCOL_VERSION` are one value -— `tests/app-client.test.ts` asserts it), the browser-safe strict-JSON +`APP_PROTOCOL_VERSION` and the host's `MCP_APP_PROTOCOL_VERSION` are one value, +`tests/app-client.test.ts` asserts it), the browser-safe strict-JSON helpers, and the route-name parser, so `dist/app.js` names no Zod, Node built-in, Effect, route module, or compiler code, and the App compile inlines it into the self-contained HTML from the installed package inside the document @@ -1675,9 +1675,9 @@ cancellation: `createMcpAppBridge` now honors the client's and session lease (below). The `browser-app` proof level (`mountBrowserApp` in `agent-bundle/test/browser`, which hosts the compiled document over `createMcpAppBridge`) is where the client meets that host bridge -in a real browser — `examples/mcp-app/tests/browser-app` asserts the +in a real browser, `examples/mcp-app/tests/browser-app` asserts the App-to-host traffic is exactly the client's `ui/initialize` and -`ui/notifications/initialized` until the view acts — so there is one wire +`ui/notifications/initialized` until the view acts, so there is one wire contract, not a second host bridge. The checkout hosts are driven by the same client rather than by hand-written frames: `tests/serve-app.test.ts` connects `createAppClient` to the served example App over injected ports whose @@ -1685,8 +1685,8 @@ client rather than by hand-written frames: `tests/serve-app.test.ts` connects relay uses, and the Workbench real-App E2E (`packages/workbench/tests/mcp-app-real.e2e.test.ts`) compiles a fixture view on `createAppClient` and reads its `call()` result through the relay. The -client's own contract — envelopes, handshake, transport authentication, dispatch, cancellation, -rebind, disposal — is proven in `tests/app-client.test.ts` over injected +client's own contract, envelopes, handshake, transport authentication, dispatch, cancellation, +rebind, disposal, is proven in `tests/app-client.test.ts` over injected ports. The client never decides which server a call reaches or which capability needs consent. @@ -1696,12 +1696,12 @@ capability needs consent. | Member | Contract | | --- | --- | -| `connect(options?)` | Performs the `ui/initialize` handshake once and resolves the validated `AppInitializeResult` (`protocolVersion` `2026-01-26` — `APP_PROTOCOL_VERSION` — plus `hostInfo`, `hostCapabilities`, `hostContext`), then sends `ui/notifications/initialized` and records the opening tool name from `hostContext.toolInfo.tool.name` for the opening-notification listeners. Idempotent: a connected client resolves the cached result, a connecting one returns the in-flight promise; a handshake that `rebind()` overtakes rejects with `connection-rebound`. | -| `call(routeId, input, options?)` | `tools/call` for the tool a `tool:/` route id names — the standard wire name is the final segment, derived the way the generated server derives it (`src/routes/protocol-name.ts`); any other shape rejects with a `TypeError`. The client also carries the canonical id in framework-private `_meta`, and `createMcpAppBridge` rejects it with `-32602` unless both server and tool match the bound server before dispatch. `input` must be a finite strict-JSON object (`invalid-message` otherwise). Resolves the result's `structuredContent` **directly**; rejects a result with `isError: true` (`rpc`, whole result on `error.data`), one without an object `structuredContent`, or a malformed envelope (`invalid-message`). The MCP projection emits `structuredContent` for object-valued results only (`advertisedOutputSchema` in `src/mcp-server-runtime.ts`), so a tool whose `resultSchema` is scalar- or array-rooted is typed by the generated map but never resolves through `call()`. | +| `connect(options?)` | Performs the `ui/initialize` handshake once and resolves the validated `AppInitializeResult` (`protocolVersion` `2026-01-26`, `APP_PROTOCOL_VERSION`, plus `hostInfo`, `hostCapabilities`, `hostContext`), then sends `ui/notifications/initialized` and records the opening tool name from `hostContext.toolInfo.tool.name` for the opening-notification listeners. Idempotent: a connected client resolves the cached result, a connecting one returns the in-flight promise; a handshake that `rebind()` overtakes rejects with `connection-rebound`. | +| `call(routeId, input, options?)` | `tools/call` for the tool a `tool:/` route id names, the standard wire name is the final segment, derived the way the generated server derives it (`src/routes/protocol-name.ts`); any other shape rejects with a `TypeError`. The client also carries the canonical id in framework-private `_meta`, and `createMcpAppBridge` rejects it with `-32602` unless both server and tool match the bound server before dispatch. `input` must be a finite strict-JSON object (`invalid-message` otherwise). Resolves the result's `structuredContent` **directly**; rejects a result with `isError: true` (`rpc`, whole result on `error.data`), one without an object `structuredContent`, or a malformed envelope (`invalid-message`). The MCP projection emits `structuredContent` for object-valued results only (`advertisedOutputSchema` in `src/mcp-server-runtime.ts`), so a tool whose `resultSchema` is scalar- or array-rooted is typed by the generated map but never resolves through `call()`. | | `request(method, params?, options?)` | The typed JSON-RPC escape hatch for `resources/read` and supported `ui/*` methods; resolves the raw result. An empty method rejects with a `TypeError`. | -| `onToolInput(routeId, listener)` / `onToolResult(routeId, listener)` / `onToolError(routeId, listener)` | The opening call's `ui/notifications/tool-input` arguments, the decoded `structuredContent` of a successful `ui/notifications/tool-result`, and that notification's failures as an `AppClientError` — `isError: true` is `rpc` with the whole result on `data`; a malformed envelope or one without an object `structuredContent` is `invalid-message`; a failed result never reaches `onToolResult`. The notifications carry no tool name, so dispatch keys on the tool the handshake named: `hostContext.toolInfo.tool.name` from the initialize result, matched against the final segment of each registered route id. Listeners for other tools stay silent; when the initialize result names no tool, `tool-input` and `tool-result` reach no listener. Listeners run on a microtask, exceptions dropped. Each returns its unsubscribe function. | +| `onToolInput(routeId, listener)` / `onToolResult(routeId, listener)` / `onToolError(routeId, listener)` | The opening call's `ui/notifications/tool-input` arguments, the decoded `structuredContent` of a successful `ui/notifications/tool-result`, and that notification's failures as an `AppClientError`, `isError: true` is `rpc` with the whole result on `data`; a malformed envelope or one without an object `structuredContent` is `invalid-message`; a failed result never reaches `onToolResult`. The notifications carry no tool name, so dispatch keys on the tool the handshake named: `hostContext.toolInfo.tool.name` from the initialize result, matched against the final segment of each registered route id. Listeners for other tools stay silent; when the initialize result names no tool, `tool-input` and `tool-result` reach no listener. Listeners run on a microtask, exceptions dropped. Each returns its unsubscribe function. | | `onToolCancelled(listener)` | `ui/notifications/tool-cancelled` as `{ reason? }`, unfiltered; returns its unsubscribe function. | -| `rebind({ parent?, targetOrigin?, window? })` | Bumps the connection generation and rejects the previous generation's pending requests with `connection-rebound` — a `connect()` still in flight included; its late response can never become the live connection — clears the opening tool name, moves the message listener when `window` changes, adopts the new parent, keeps the configured `targetOrigin` unless the call names the key, and runs `connect()` again. | +| `rebind({ parent?, targetOrigin?, window? })` | Bumps the connection generation and rejects the previous generation's pending requests with `connection-rebound`, a `connect()` still in flight included; its late response can never become the live connection, clears the opening tool name, moves the message listener when `window` changes, adopts the new parent, keeps the configured `targetOrigin` unless the call names the key, and runs `connect()` again. | | `dispose()` | Idempotent. Removes the message listener, rejects pending requests with `disposed`, and drops every registration. A host `ui/resource-teardown` request is answered with `{}` and disposes the client; any other host request is answered `-32601`. | | `connected` / `disposed` | Read-only state. | @@ -1717,8 +1717,8 @@ A request that times out or is aborted rejects with `timeout` / `aborted` and, once connected, sends `notifications/cancelled` with the request id and the reason; the client does not wait to learn whether the host stopped anything. -Two error kinds, deliberately. Everything asynchronous — wire, host, and -lifecycle — rejects with the one `AppClientError` class: `code` is `timeout`, +Two error kinds, deliberately. Everything asynchronous, wire, host, and +lifecycle, rejects with the one `AppClientError` class: `code` is `timeout`, `aborted`, `disposed`, `connection-rebound`, `invalid-message`, `rpc`, `capability-unavailable`, or `consent-required`; a host JSON-RPC error maps `-32601` to `capability-unavailable`, `-32001` to `consent-required`, and @@ -1739,12 +1739,12 @@ involved: App-side failures are browser errors. The client's `notifications/cancelled` is honored by `createMcpAppBridge` (`src/dev/mcp-apps/mcp-app-bridge.ts`), which the Workbench, `serve-app`, and -`mountBrowserApp` run. Every operation request the bridge accepts — +`mountBrowserApp` run. Every operation request the bridge accepts, `tools/call`, `resources/read`, and the `ui/*` requests; `ping` is answered -inline — is tracked in flight under its JSON-RPC id with an `AbortController`; +inline, is tracked in flight under its JSON-RPC id with an `AbortController`; a well-formed cancellation for an in-flight id aborts that controller, drops the request's pending consent challenge (resolving it as denied, so a later -approval cannot run the action), and suppresses the late response — a +approval cannot run the action), and suppresses the late response, a cancelled request gets neither a result nor an error. The abort signal is threaded through `McpAppBindingService.callTool`/`readResource` and the `McpAppSessionLease` into `McpSession`, which composes it into the SDK request @@ -1770,7 +1770,7 @@ the opening tool (`hostContext.toolInfo.tool.name`), which is what the opening-notification listeners dispatch on. A malformed message that still names a pending id rejects that request as `invalid-message`. -A host that can name a trusted origin passes `targetOrigin` — an exact +A host that can name a trusted origin passes `targetOrigin`, an exact `http:` or `https:` origin; `'*'`, `'null'`, other schemes, and non-origin strings are a `TypeError`. The client then uses that exact origin for every outgoing frame and requires every incoming `event.origin` to match it. The @@ -1785,7 +1785,7 @@ injected ports. `generateRouteTypes` (`src/routes/typegen.ts`) emits, for a graph with at least one tool route, `AppToolRouteId` (the `tool:/` subset of `RouteId`), `AgentBundleAppRouteContracts` (one `{ input: RouteInput; -result: RouteResult }` per tool — `input` from the module's own +result: RouteResult }` per tool, `input` from the module's own `inputSchema` *input* type, what a caller sends before the server parses it, so a defaulted field is optional and a transformed field is spelled as the wire carries it; `result` from the `resultSchema` output; a structural schema diff --git a/docs/framework-mode.md b/docs/framework-mode.md index d5aecb4f5..4f922efa5 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -94,13 +94,13 @@ time. The full grammar and the `config.template` resolution rule are in [Diagnostics](diagnostics.md). A built App is previewed in the Workbench MCP page, or served standalone in a -plain browser tab with `agent-bundle serve-app /` — the same +plain browser tab with `agent-bundle serve-app /`, the same host stack (sandbox proxy, consent authority, bridge) bound to the plugin's own packed server, launched as `mcp run` launches it. `serveApp` in -`agent-bundle/api` is the programmatic form for host processes — the CLI, -the Workbench, tests, a plugin's own scripts — never the MCP shell, and a +`agent-bundle/api` is the programmatic form for host processes, the CLI, +the Workbench, tests, a plugin's own scripts, never the MCP shell, and a local preview host, not a deployment target. A plugin's own "open the -dashboard" CLI route cannot import it — the routed CLI bin is self-contained, +dashboard" CLI route cannot import it, the routed CLI bin is self-contained, and the route graph reports the value import as `AB4837`. From an installed artifact the supported command is ` web` on `bin/.mjs`, emitted when the `web` config key lists declared Apps; `agent-bundle dev` @@ -116,15 +116,15 @@ public host wire protocol. ## Request context and providers -Every generated request scope — MCP tools, resources, and prompts, event +Every generated request scope, MCP tools, resources, and prompts, event routes, plain and rendered routed CLI commands, rendered scripts, and Workbench -replay — installs the same typed `AgentRequestContext`. `await agent()` +replay, installs the same typed `AgentRequestContext`. `await agent()` returns the invocation plus `Observed` `host`, `session`, `actor`, `workspace`, and `lineage` axes (an `available` value with its provenance, or -a typed `unavailable` reason — never a fabricated string), request +a typed `unavailable` reason, never a fabricated string), request capabilities, progress, the request signal, and the `state`, `notices`, and `providers` slots. `lineage` places the request in the host's conversation -tree — `{ conversation, root, parent?, depth, generation?, subagent? }` — +tree, `{ conversation, root, parent?, depth, generation?, subagent? }`, resolved by the warm runtime's registry that the subagent start/stop and pre-tool event families feed; see [Conversation lineage](entry-conventions.md#conversation-lineage-requestlineage) @@ -171,10 +171,10 @@ values through `renderRoute(id, { context: { providers: { library } } })`; inclu the route requests. Missing keys fail instead of loading a real provider. The same file registers the route contracts themselves. One generated -`AgentBundleRouteContracts` — `{ input, result }` per route id, inferred from +`AgentBundleRouteContracts`, `{ input, result }` per route id, inferred from each module's own `inputSchema`/`resultSchema`; an event route registers the `{ canonical, native }` payload the harness accepts (it supplies `signal` -itself) and an `undefined` result — is registered on `@agent-bundle/runtime`'s +itself) and an `undefined` result, is registered on `@agent-bundle/runtime`'s `Register` interface in that one `declare module` block, so no per-route declaration file is emitted. `renderRoute('tool:library/summarize', { input })` then checks its id against @@ -184,17 +184,17 @@ and `RegisteredRouteResult` name that surface for a wrapper of your own. The seam is inert without the file: an id typed `string` stays legal for dynamic lookups, a directly imported module target is unaffected, and a project that excludes the generated declarations (or has not built yet) sees the -unregistered types — any string, `unknown` input, `unknown` result. +unregistered types, any string, `unknown` input, `unknown` result. That one registration is read by every public surface that takes or yields a route id or route payload, the way TanStack Router's `Register` reaches `Link to`, `useNavigate`, and `RoutesByPath`: `invokeMcpTool` and `getMcpPrompt` check their wire name against the registered tool/prompt names (the last -segment of a `tool:`/`prompt:` id) — of the literal `server` when one is -passed, since the session mounts only that server's routes — and type +segment of a `tool:`/`prompt:` id), of the literal `server` when one is +passed, since the session mounts only that server's routes, and type `input` from that route; `runContractMatrix` and the packed, dev-epoch, and installed-host matrices type -the inputs of each registered `fixtures` key (an App route key stays untyped — +the inputs of each registered `fixtures` key (an App route key stays untyped, Apps register no contract); `invokeCli` reports `routeId` as a registered id; and `agent-bundle/eval`'s `expectMcpCall`/`expectNoMcpCall` check a literal `tool` against the registered tools of a literal project `server`. @@ -214,7 +214,7 @@ The final Agent Document of a tool route lowers to one `CallToolResult`: | `Agent.Image`, `Agent.Audio`, `Agent.Resource` | Native `image`, `audio`, and `resource_link` blocks; a host without that capability fails the projection closed unless a text fallback is selected. | | `Agent.Result value` | `structuredContent` when the value is a JSON object; a non-object value emits none and is never wrapped. | | `Agent.Result metadata` | `CallToolResult._meta`. It must be a JSON object (snapshotted through the same wire boundary as `structuredContent`); anything else fails the projection closed with `McpProjectionError('invalid-result-metadata')`. Listing-level `_meta` still comes from static `config._meta`, so the MCP Apps convention stamps `_meta.ui.resourceUri` on both halves. In `config._meta.ui.resourceUri`, reference the App route instead of repeating its `ui://` literal: `appResourceUri('dashboard')` from `agent-bundle/routes` resolves at compile time to that App route's `config.resourceUri`, and a `const` string literal imported from a relative sibling module (`import { DASHBOARD_URI } from '../constants'`) is accepted too and stays available at run time for the result half. | -| `Agent.Progress` | Never a `content` block. Streamed inside a `shell` or `replace` document — normally as a `Suspense` fallback — it projects to one `notifications/progress` (`progress` from `completed`, plus `message` and `total` when present) when the request carried `_meta.progressToken`; a request without a token gets none. The same monotonic rule applies as to `progress.report()`: each notification's `progress` must exceed the last, so a fallback re-streamed on the next chunk, or one an explicit report already announced with the same `completed`, is not repeated. A fallback alone is enough — an `announce()`-style helper that repeats the fallback message through `progress.report()` adds nothing (#448). A progress node in the final document is content only. The rendered CLI's interactive TTY draws its in-place progress line from the same streamed node (redrawn only when the fallback changes); piped Markdown, `--json`, and `--ndjson` never print it. | +| `Agent.Progress` | Never a `content` block. Streamed inside a `shell` or `replace` document, normally as a `Suspense` fallback, it projects to one `notifications/progress` (`progress` from `completed`, plus `message` and `total` when present) when the request carried `_meta.progressToken`; a request without a token gets none. The same monotonic rule applies as to `progress.report()`: each notification's `progress` must exceed the last, so a fallback re-streamed on the next chunk, or one an explicit report already announced with the same `completed`, is not repeated. A fallback alone is enough, an `announce()`-style helper that repeats the fallback message through `progress.report()` adds nothing (#448). A progress node in the final document is content only. The rendered CLI's interactive TTY draws its in-place progress line from the same streamed node (redrawn only when the fallback changes); piped Markdown, `--json`, and `--ndjson` never print it. | | `Agent.Error code message` | `isError: true` plus one text block `[] `. The wire has no error-code field, so the code is deliberately kept in the text (the routed CLI prints the same `**[code]** message` form); choose codes that read well to the model. | | `resultSchema` | `outputSchema` in `tools/list` **only when the schema describes an object** (`z.object`, `z.record`, a discriminated union of objects). The MCP specification requires every result of a tool that declares `outputSchema` to carry `structuredContent`, so a text-only route declares `resultSchema = z.undefined()` (or any non-object schema), advertises no `outputSchema`, and returns no `structuredContent`. An object schema keeps the SDK's fail-closed output validation on every call. Both `inputSchema` and `outputSchema` are advertised through the interoperable 2020-12 projection (`src/mcp-schema-projection.ts`, #563): a zod tuple is emitted as `prefixItems` plus `items` set to the union of the positional schemas with `minItems` (and `maxItems` for a closed tuple), never `items: false`, so a host that validates with non-strict draft-07 keyword semantics (Cursor) accepts every result a 2020-12 validator accepts. | @@ -223,28 +223,28 @@ The final Agent Document of a tool route lowers to one `CallToolResult`: `Agent.Error` is the supported error path: the error is data inside the document, so the layout shell, `_meta`, `structuredContent`, and the `[code]` text all survive. A route that **throws** instead (or rejects, before any -document exists) is not projected by agent-bundle at all — there is no document -to project — and each surface's own default applies. These are decisions, pinned +document exists) is not projected by agent-bundle at all. There is no document +to project, and each surface's own default applies. These are decisions, pinned by `tests/route-unit/thrown-route-error.test.ts`, `tests/projection/mcp-in-memory.test.ts`, `tests/projection/cli-dispatch-rendered.test.ts`, `tests/generated-route-server.test.ts`, `tests/hooks.test.ts`, and `tests/event-ipc.test.ts` (#492): | Surface | A route whose default export throws | A nested `Suspense` boundary that rejects | | --- | --- | --- | -| MCP `tools/call` | The MCP SDK's default tool error: `{ content: [{ type: 'text', text: }], isError: true }`. No `_meta` (the layout never rendered), no `structuredContent`, no `[code]` prefix. The session stays usable. | A represented error: the reconciler folds the rejected boundary into the streamed document as an error node with code `boundary`, so the result is exactly what `` would produce — layout `_meta` kept, `structuredContent` from the route's `Agent.Result value`, `isError: true`, and a `[boundary] ` text block after the content that had already rendered. | +| MCP `tools/call` | The MCP SDK's default tool error: `{ content: [{ type: 'text', text: }], isError: true }`. No `_meta` (the layout never rendered), no `structuredContent`, no `[code]` prefix. The session stays usable. | A represented error: the reconciler folds the rejected boundary into the streamed document as an error node with code `boundary`, so the result is exactly what `` would produce, layout `_meta` kept, `structuredContent` from the route's `Agent.Result value`, `isError: true`, and a `[boundary] ` text block after the content that had already rendered. | | MCP `prompts/get`, `resources/read` | A JSON-RPC error response carrying the message; the client call rejects. These surfaces have no `isError` channel. | The same represented document; the generated server returns the route's `resultSchema`-parsed value, so the prompt or resource result is whatever the route's value said. | -| Rendered CLI command / rendered script | One `[render-failed] ` line on stderr; under `--json`, one canonical `{"error":{"code":"render-failed","message":"..."}}` line on stderr. Exit 1; nothing on stdout; no React Flight stack. | The `error` render event is written to stderr as `[boundary] message` as it happens; the final document then prints as usual — Markdown (TTY or piped) with `**[boundary]** message` beside the content that had rendered, or under `--json` the route's value alone, with the boundary message **not** in the JSON. `--ndjson` carries the `error` event itself. Exit 1, because any non-`success` document status exits 1 regardless of the command's `exitCode` policy. | -| Plain CLI command / plain script | Routed command: the message on stderr, exit 1 (only usage and input errors exit 2). Plain script: the rejection escapes through Node's top-level failure path — stack on stderr, exit 1. | n/a (no renderer). | -| Event route (hook) | The generated wrapper writes to stderr, nothing to stdout, and exits 1. What stderr says depends on the runtime mode: a `runtime: 'standalone'` route and a config-declared handler write the thrown message; a route in the **shared** runtime writes `Event route rendering failed.` — the shared runtime answers the wrapper with the generic `runtime-failed` error (`events/ipc.ts`) and the original message never leaves the runtime process. Every supported host documents exit 1 as a **non-blocking** error (Claude Code and Codex show the stderr; Cursor treats it as fail-open), so the pending action proceeds exactly as a pass-through would — a thrown `tool/before` does **not** deny. | The hook projection reads `Agent.Context` and the result value only; the error node contributes nothing, so the host receives the surviving context and decision as a normal response. | +| Rendered CLI command / rendered script | One `[render-failed] ` line on stderr; under `--json`, one canonical `{"error":{"code":"render-failed","message":"..."}}` line on stderr. Exit 1; nothing on stdout; no React Flight stack. | The `error` render event is written to stderr as `[boundary] message` as it happens; the final document then prints as usual, Markdown (TTY or piped) with `**[boundary]** message` beside the content that had rendered, or under `--json` the route's value alone, with the boundary message **not** in the JSON. `--ndjson` carries the `error` event itself. Exit 1, because any non-`success` document status exits 1 regardless of the command's `exitCode` policy. | +| Plain CLI command / plain script | Routed command: the message on stderr, exit 1 (only usage and input errors exit 2). Plain script: the rejection escapes through Node's top-level failure path, stack on stderr, exit 1. | n/a (no renderer). | +| Event route (hook) | The generated wrapper writes to stderr, nothing to stdout, and exits 1. What stderr says depends on the runtime mode: a `runtime: 'standalone'` route and a config-declared handler write the thrown message; a route in the **shared** runtime writes `Event route rendering failed.`, the shared runtime answers the wrapper with the generic `runtime-failed` error (`events/ipc.ts`) and the original message never leaves the runtime process. Every supported host documents exit 1 as a **non-blocking** error (Claude Code and Codex show the stderr; Cursor treats it as fail-open), so the pending action proceeds exactly as a pass-through would, a thrown `tool/before` does **not** deny. | The hook projection reads `Agent.Context` and the result value only; the error node contributes nothing, so the host receives the surviving context and decision as a normal response. | | `renderRoute` / `renderRouteEvents` (route-unit) | `AgentTestError('render-failed')` naming the route and the cause; no document, no events. | Resolves: `document.status === 'represented-error'`, an `error` node with code `boundary`, events `shell → error(boundaryId) → complete`. | Why the projector does not wrap a root throw into the `Agent.Error` shape: the -layout shell is a property of a document, and a root throw has none — giving it +layout shell is a property of a document, and a root throw has none, giving it `_meta` would mean rendering the layout around a synthetic child, which is the layout-level `error` prop the #492 discussion reserves for a consumer who asks for it. A `[code]` prefix would also create a third error shape beside the -SDK default (which every other handler failure — a `resultSchema` rejection, an -`McpProjectionError` — already uses) and the represented one. Both surveyed real +SDK default (which every other handler failure, a `resultSchema` rejection, an +`McpProjectionError`, already uses) and the represented one. Both surveyed real apps keep failures as data for exactly this reason; a route that wants a code, a shell, or structured content renders `Agent.Error`. @@ -274,13 +274,13 @@ import { name, version } from 'agent-bundle/meta'; The compiler replaces the specifier in every compiled surface with the exact `{ name, packageName, packageVersion, version }` the artifact manifests, `inspect`, and dev status report (see -[Entry conventions](entry-conventions.md#agent-bundlemeta--build-time-release-identity)). +[Entry conventions](entry-conventions.md#agent-bundlemeta-build-time-release-identity)). Unit tests need no build to load such a module: `agentBundleRstest()` and `agentBundleBrowserRstest()` (`agent-bundle/rstest`) alias `agent-bundle/meta` to a generated module carrying the same identity, written from the same compiler pass to `.agent-bundle/test/meta.mjs`. Run every pool that reaches -that source — plain unit tests included — through the preset (pass `include` +that source, plain unit tests included, through the preset (pass `include` to point it at the pool's files), and `renderRoute`, `invokeCli`, and direct imports all observe the package identity. Outside the compiler and outside those presets the published module raises `AB4760`, whose recovery names the @@ -316,7 +316,7 @@ export default () => ( ); ``` -The renderer supports a documented element subset (`h1`–`h6`, `p`, +The renderer supports a documented element subset (`h1` to `h6`, `p`, `ul`/`ol`/`li`, `strong`/`b`, `em`/`i`, `code`, `pre`, `blockquote`, `a`, `hr`, `br`, fragments, strings and numbers) and rejects anything outside it by name (`AB3005`), never a silent approximation; a module that fails to load or @@ -389,15 +389,15 @@ export default defineConfig({ }); ``` -Terminal notices — `expired`, `unavailable`, `withdrawn`, `acknowledged`, and -`attempted` with an exhausted retry budget — leave the ledger once they have +Terminal notices, `expired`, `unavailable`, `withdrawn`, `acknowledged`, and +`attempted` with an exhausted retry budget, leave the ledger once they have been settled for `terminalTtl`, or earliest-settled first once more than `maxTerminal` remain; the store's journal is compacted onto its head once it exceeds `maxJournalBytes`. Every field is optional and defaults to the values shown; pruning runs only on admitted events and explicit `retain()` calls, so -no timer is implied. A malformed policy — an unknown key, a non-positive or +no timer is implied. A malformed policy, an unknown key, a non-positive or fractional value, a duration outside that grammar, or a policy declared by a -project without a state module — is `AB4833`. `inspect --state` and the +project without a state module, is `AB4833`. `inspect --state` and the Workbench State panel show the resolved policy and whether it was declared or defaulted. @@ -407,7 +407,7 @@ Redaction is not configured here: it follows the notice's author-declared `noticeDelivery` table. `internal` content is passed through the runtime's secret pass (`flare-redact`, pinned exact; see the runtime README) on every route, `public` travels as authored, and `secret` travels only over a route -whose ceiling admits it — otherwise it stays in the store and the route +whose ceiling admits it, otherwise it stays in the store and the route records the refusal on the notice. ## Live development into hosts @@ -452,8 +452,8 @@ section carries the exact commands, install layouts, and event payloads; Every project component belongs to one canonical kind (`AgentComponentKind`, exported from `agent-bundle/api`), and every kind that needs a host surface names the capability row a target adapter must publish for it. Adapters judge -each row with the shared four-state contract — `supported` with pinned -evidence, or `degraded` / `unavailable` / `prohibited` with a dated reason — +each row with the shared four-state contract, `supported` with pinned +evidence, or `degraded` / `unavailable` / `prohibited` with a dated reason, and `agent-bundle inspect` reports the judgment per host (see [component accounting](entry-conventions.md#agent-bundle-inspect-component-accounting)). A host with no row for a kind reads as an honest `unavailable`, never a silent @@ -474,7 +474,7 @@ evidence strings themselves. | `lsp` | `claude.lspServers` (plugin-root `.lsp.json`) | `lsp` | supported | unavailable | unavailable | unavailable | | `native-diagnostics` | none | `nativeDiagnostics` | unavailable (LSP `diagnostics` option only) | unavailable | unavailable | unavailable | | `native-extension` | none | `nativeExtension` | unavailable | unavailable | unavailable | unavailable | -| `agent` | `src/agents` (deferred) | `agents` | unavailable — G5 deferral ([#220](https://github.com/ScriptedAlchemy/agent-bundle/pull/220)) | no row | unavailable (G5) | no row | +| `agent` | `src/agents` (deferred) | `agents` | unavailable, G5 deferral ([#220](https://github.com/ScriptedAlchemy/agent-bundle/pull/220)) | no row | unavailable (G5) | no row | | `script` | `src/scripts/**`, `scripts` config | none | emitted | emitted | emitted | emitted | | `cli` | routed `src/cli/**` bin (#387) | `cli` | supported | supported | supported | supported | @@ -504,7 +504,7 @@ their rows below mirror that contract rather than adding a second check. | Kind | Feature rows | Claude | Codex | Cursor | portable | | --- | --- | --- | --- | --- | --- | -| `command` | `commands.description`, `commands.argumentHint`, `commands.allowedTools`, `commands.model`, `commands.disableModelInvocation` | supported (documented kebab-case frontmatter) | no commands | unavailable — frontmatter-free Markdown, body only | no commands | +| `command` | `commands.description`, `commands.argumentHint`, `commands.allowedTools`, `commands.model`, `commands.disableModelInvocation` | supported (documented kebab-case frontmatter) | no commands | unavailable, frontmatter-free Markdown, body only | no commands | | `rule` | `rules.description`, `rules.globs`, `rules.alwaysApply` | no rules | no rules | supported (`.mdc` frontmatter, retrieved 2026-09-03) | no rules | | `hook` | `hooks.toolMatchers`, `hooks.timeout` | supported | supported | supported | no hooks | | `skill` | `skills.hostFrontmatter` (typed host extension / Codex `agents/openai.yaml` sidecar) | supported | supported | supported | unavailable (portable fields only) | @@ -530,10 +530,10 @@ compiled outputs in at most two stages into one staged directory that is published atomically once the artifact validates (`src/build/compile-stages.ts`): -1. **MCP Apps** — the browser environment, compiled through the workspace +1. **MCP Apps**, the browser environment, compiled through the workspace `@rsbuild/core`. This stage exists only when the project declares App routes and always runs first: the MCP entries embed its emitted HTML. -2. **Agent-host surfaces** — the routed CLI bin, bundled scripts, hook +2. **Agent-host surfaces**, the routed CLI bin, bundled scripts, hook wrappers, MCP stdio entries, and each surface's react-server Flight worker. All of them lower together through **one Rslib instance for the root**: one Rsbuild environment per output, compiled by one Rspack multi-compiler. @@ -542,12 +542,12 @@ published atomically once the artifact validates each surface keeps its own authored-source evidence for the manifest. Compiled surfaces are emitted once, not once per host: the manifest attributes -them to the composite identity — the selected host names sorted and joined +them to the composite identity, the selected host names sorted and joined with `+`, such as `claude+codex`. Selection order never changes the output; `['codex', 'claude']` and `['claude', 'codex']` build byte-identical roots. -Every synthesized bundler config — both stages plus the `dist/` package build -— composes the same way: the framework profile, then the consumer's +Every synthesized bundler config, both stages plus the `dist/` package build, +composes the same way: the framework profile, then the consumer's `tools.rsbuild` fragment, then the `tools.rspack` hatch, then the framework invariant layer that no hatch value can override (`src/build/compose-layers.ts`; see the `tools` section of the configuration @@ -562,8 +562,8 @@ compiled surface imports are served from memory under the reserved `/.agent-bundle-virtual/` namespace (`src/build/meta.ts`), which never exists on disk: the virtual paths are predictable, so the build refuses to compile while anything occupies that directory -(`assertGeneratedModulesRootAbsent`). That namespace hangs off the project root — the -bundler `context` — on purpose: Rspack writes module identifiers relative to +(`assertGeneratedModulesRootAbsent`). That namespace hangs off the project root, the +bundler `context`, on purpose: Rspack writes module identifiers relative to `context` into emitted bundles (the `// NAMESPACE OBJECT: ./…` comments of concatenated modules), so a namespace under the staging root would stamp the per-build token into the artifact. @@ -668,9 +668,9 @@ cannot (`retained-by-host` for Claude's ~14-day orphaned copy, missing receipt or an owned-content mismatch is refused (`AB7009`, `AB7007`) unless `--force`; a receipt or manifest naming another plugin is refused regardless; `--purge-data` without `--confirm-purge` is `AB7008`; a second run is -a `not-installed` no-op. `doctor --from` adds the lifecycle stage per host — +a `not-installed` no-op. `doctor --from` adds the lifecycle stage per host, placed → registered → enabled → active, each observed or typed `unavailable` -(`AB7330`) — and cross-checks every store receipt against the host (`AB7328`). +(`AB7330`), and cross-checks every store receipt against the host (`AB7328`). The host-install proofs snapshot an isolated home before install and after uninstall: byte-identical for Cursor and portable, zero Agent Bundle residue plus classified host-owned bookkeeping for Claude and Codex. Details: diff --git a/docs/local-ci.md b/docs/local-ci.md index 977edfe47..8f4182373 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -1,7 +1,7 @@ # Local CI gate `pnpm check:local-ci` proves what the hosted CI gate proves, on the -development machine, in one command — including the full Node matrix. It +development machine, in one command, including the full Node matrix. It exists because the hosted Verify work for one Node version takes ~13 minutes of runner time by current measurement (hosted CI splits it into a `fast` leg and two integration shards that run in parallel, see below). PR CI runs every @@ -13,7 +13,7 @@ machine can run all three legs plus the release gates concurrently. The local-merge workflow it enables: 1. Run `pnpm check:local-ci` on the branch's HEAD commit. -2. If the gate is green, the branch is mergeable — merge it. +2. If the gate is green, the branch is mergeable, merge it. 3. Hosted CI still runs on the merged commit (push to `main`) and stays the asynchronous post-merge safety net; if it disagrees with the local run, the hosted result wins and the merge gets a follow-up fix. @@ -33,7 +33,7 @@ and covered by `packages/agent-bundle/tests/classify-docs-only.test.ts`. ## What it runs Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted -changes are not covered — the runner warns), with its own `node_modules` and +changes are not covered, the runner warns), with its own `node_modules` and its own `TMPDIR` (`/abci--`, where `` is derived from the repo root path; recreated every run). The temp roots live under the short system temp directory rather than the repo worktree because @@ -43,7 +43,7 @@ from colliding. The private temp root keeps concurrent legs from observing each other's temp traffic: suites that assert temp-root hygiene (for example `cli.test.ts` scans `os.tmpdir()` for leaked `agent-bundle-artifact-*` directories) only ever see their own leg's directories, so a sibling leg's -in-flight work cannot fail them — while a directory the leg itself leaks +in-flight work cannot fail them, while a directory the leg itself leaks still fails its own scan. Rstest re-hashes that leg directory, worker ID, and invocation identity to `/tmp/ab-rstest-` before exposing its worker `TMPDIR`; this leaves headroom below Linux's 108-byte `sun_path` cap for nested @@ -51,18 +51,18 @@ socket fixtures without sacrificing per-leg, per-worker, or concurrent-run isolation. Because those hashed roots live beside the leg directory rather than inside it, each one carries an owner marker (`.ab-rstest-owner.json`) naming the leg `TMPDIR` and process it was derived from; the runner removes -the roots owned by a leg's `TMPDIR` — and only those, once their creating -process has exited — before the leg starts (leftovers of an interrupted run) +the roots owned by a leg's `TMPDIR`, and only those, once their creating +process has exited, before the leg starts (leftovers of an interrupted run) and after it finishes (`scripts/rstest-worker-roots.mjs`), so reruns cannot accumulate worker caches or interrupted-test fixtures under `/tmp`. Legs live under `.worktrees/local-ci/` (gitignored), are reused across runs for warm caches, and can be recreated with `--fresh`. The three Verify legs below mirror the hosted `main`-push matrix. Hosted CI -runs each Node version as three parallel jobs — `Verify (fast, Node N)` +runs each Node version as three parallel jobs, `Verify (fast, Node N)` (build, typecheck, lint, unit, route-unit, projection) and `Verify (integration-1|2, Node N)` (build, then one `--shard N/2` of the -integration pool) — fanned into the required `Verify gate` check; a local leg +integration pool), fanned into the required `Verify gate` check; a local leg runs the same pools serially in one worktree, which proves the same union. On PRs, hosted CI runs every leg on Node 24 and the `fast` leg on Node 26. @@ -100,7 +100,7 @@ That skip is what let #364 change the Codex `interface.logo` emission and break both proofs on `main` without CI noticing. Hosted CI therefore runs a dedicated `host-install-proofs` job (Node 22.19) on every PR and `main` push: -1. `node scripts/host-cli-pins.mjs print` reads the pins — the `hostCli` +1. `node scripts/host-cli-pins.mjs print` reads the pins, the `hostCli` block in `packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json` (`@anthropic-ai/claude-code`) and `.../schemas/codex/PROVENANCE.json` (`@openai/codex`). Each pin must equal that file's `observedCliVersion`, @@ -161,7 +161,7 @@ PR critical path. Hosted CI therefore adds one extra job, `host-filesystem`, on `ubuntu-latest`, `macos-latest`, and `windows-latest` (Node 22.19, engines -floor — three cells, not a Node × OS product). It builds once and runs +floor, three cells, not a Node × OS product). It builds once and runs `pnpm test:host-filesystem` (`rstest.host-filesystem.config.ts`, which does not build the Workbench e2e example payload): @@ -187,7 +187,7 @@ To run the same slice locally after `pnpm build`: pnpm test:host-filesystem ``` -The local gate does not fan this across OSes — one machine can prove one OS. +The local gate does not fan this across OSes, one machine can prove one OS. Hosted macOS and Windows results are the qualification for those runners. ## Node provisioning @@ -195,7 +195,7 @@ Hosted macOS and Windows results are the qualification for those runners. The runner introduces no new tooling. For each hosted runtime line (22.19.x, 24.x, 26.x) it resolves a Node binary from, in order: -1. `AGENT_BUNDLE_LOCAL_CI_NODE_22` / `_24` / `_26` — a Node binary or bin +1. `AGENT_BUNDLE_LOCAL_CI_NODE_22` / `_24` / `_26`, a Node binary or bin directory, for machines with bespoke layouts; 2. `mise where node@`; 3. `~/.nvm/versions/node/*`; @@ -205,7 +205,7 @@ Every resolved binary is version-checked against the hosted line before use. If a line is missing, the runner fails with the exact install command (e.g. `mise install node@22.19`). pnpm itself is pinned by reusing the entrypoint that launched the runner, executed on each leg's own Node, so `pnpm`, its -lifecycle children, and `pnpm exec node` all agree on the leg's runtime — +lifecycle children, and `pnpm exec node` all agree on the leg's runtime, `node_modules` trees (native modules such as the rspack bindings) are never shared across Node ABIs, while the content-addressed pnpm store is shared safely. @@ -217,8 +217,8 @@ The integration pool derives workers from cores runner instead slices the machine: with N concurrent Verify legs each leg gets `min(4, cores / (2 N))` integration workers (`AGENT_BUNDLE_INTEGRATION_MAX_WORKERS`) and `cores / N` unit workers -(`--pool.maxWorkers`), and full runs pin `AGENT_BUNDLE_TEST_TIME_SCALE=4` — -the same polling-budget scale hosted CI uses — because four legs sharing a +(`--pool.maxWorkers`), and full runs pin `AGENT_BUNDLE_TEST_TIME_SCALE=4`, +the same polling-budget scale hosted CI uses, because four legs sharing a machine is exactly the contention that scale exists for. Exporting `AGENT_BUNDLE_TEST_TIME_SCALE` yourself (e.g. when the machine is also running other heavy work) overrides the default; the integration config @@ -286,10 +286,10 @@ then treat a repeat as a real signal. pinned-CLI host-install proofs stay on `ubuntu-latest`. The `host-filesystem` job is the exception: `ubuntu-latest`, `macos-latest`, and `windows-latest`. Hosted Workbench browser suites launch Playwright's - bundled Chromium — pinned by the + bundled Chromium, pinned by the Playwright version in the lockfile and selected with `AGENT_BUNDLE_PLAYWRIGHT_CHANNEL=chromium` (read by - `packages/workbench/tests/support/workbench-e2e.ts`) — so the browser under + `packages/workbench/tests/support/workbench-e2e.ts`), so the browser under test only changes with a commit. The local gate keeps the developer default, branded Google Chrome (`playwright install chrome` in its `browsers` step; the OS dependencies, `--with-deps`, are one-time machine setup and may need @@ -299,7 +299,7 @@ then treat a repeat as a real signal. browser-app pool, because the shipped `agentBundleBrowserRstest` helper targets branded Chrome; the `examples-check` job records that Chrome version in its step summary. A green local run on a different distro, - glibc, or browser build is strong but not identical evidence — this is the + glibc, or browser build is strong but not identical evidence. This is the main reason hosted CI remains the post-merge safety net. - **Job isolation**: hosted gives every job a fresh VM; local legs reuse worktrees for speed. `--fresh` restores cold-start fidelity when staleness diff --git a/docs/mcp-conformance.md b/docs/mcp-conformance.md index 0b8c13d37..f335aa1cd 100644 --- a/docs/mcp-conformance.md +++ b/docs/mcp-conformance.md @@ -7,7 +7,7 @@ pnpm test:mcp-conformance ``` CI runs the same command as the `MCP conformance` job of the `Nightly` -workflow (`.github/workflows/nightly.yml`) — on the daily schedule and on +workflow (`.github/workflows/nightly.yml`), on the daily schedule and on manual dispatch from any ref. The lane uses one Node version and one existing fixture, and uploads the official runner's `checks.json` artifacts from `artifacts/mcp-conformance`. It is not part of pull request, push, or default @@ -59,8 +59,8 @@ What the audit found on `@modelcontextprotocol/server@2.0.0` / `@modelcontextprotocol/client@2.0.0` (unchanged since the deferral): - The task methods are outside the SDK's typed spec-method surface, but the - SDK's documented custom-method form — `setRequestHandler(method, { params }, - handler)` on the server and `request(request, resultSchema)` on the client — + SDK's documented custom-method form, `setRequestHandler(method, { params }, + handler)` on the server and `request(request, resultSchema)` on the client, routes them, and the SDK exports their result schemas publicly as `specTypeSchemas.CreateTaskResult`, `GetTaskResult`, `CancelTaskResult`, `ListTasksResult` (the deferral note's "not exported publicly" was wrong; they @@ -70,8 +70,8 @@ What the audit found on `@modelcontextprotocol/server@2.0.0` / `capabilities.tasks`, so a modern-revision client keeps the ordinary contract. - The SDK's `tools/call` result validation admits `CallToolResult` only and refuses a `task` body. The lifecycle therefore lives in a `Server` subclass - (`packages/agent-bundle/src/mcp-tasks.ts`) whose `_wrapHandler` — the SDK's - documented protected seam for role-specific request handling — answers a + (`packages/agent-bundle/src/mcp-tasks.ts`) whose `_wrapHandler`, the SDK's + documented protected seam for role-specific request handling, answers a task-augmented request with a `CreateTaskResult` and runs the SDK-validated handler behind the task. Nothing reaches past the SDK's public or protected surface. @@ -168,7 +168,7 @@ level): negotiated one, so no client is invited into a lifecycle the server cannot serve. - A `tools/call` carrying task augmentation is processed as an ordinary request - and returns one final `CallToolResult` with no task handle — the behaviour + and returns one final `CallToolResult` with no task handle, the behaviour the `2025-11-25` Tasks utility requires of a receiver that declared no task capability for that request type. `tasks/get`, `tasks/result`, `tasks/list`, and `tasks/cancel` answer with JSON-RPC `-32601`. @@ -177,9 +177,9 @@ level): Unblock condition: a published `@modelcontextprotocol/server` / `@modelcontextprotocol/client` release that routes task operations through its -typed surface — a server-side task store or `registerTool` task handler, a +typed surface, a server-side task store or `registerTool` task handler, a typed client path for `CreateTaskResult` and task polling, and a `tasks` -capability the SDK itself gates — for whichever revision the conformance lane +capability the SDK itself gates, for whichever revision the conformance lane then records. `packages/rsc-runtime/tests/mcp-tasks-deferral.test.ts` pins the audited SDK version and asserts each fact above against the installed packages; when one stops holding, that test (or `pnpm typecheck`, through its diff --git a/docs/preview-packages.md b/docs/preview-packages.md index 31ba75eb4..1e282a399 100644 --- a/docs/preview-packages.md +++ b/docs/preview-packages.md @@ -4,8 +4,8 @@ pkg.pr.new is the only package distribution channel. Every CI package-preview run publishes real, installable tarballs of all four publishable workspace packages (`agent-bundle`, `@agent-bundle/runtime`, `rsc-markdown-stream`, `create-agent-bundle`) to -[pkg.pr.new](https://pkg.pr.new) -— a free continuous-release registry keyed by commit SHA and pull request. +[pkg.pr.new](https://pkg.pr.new), +a free continuous-release registry keyed by commit SHA and pull request. Consumers pin these previews by commit SHA; no npm registry credential is needed or expected. @@ -38,7 +38,7 @@ A scaffolded project pins `agent-bundle` to the preview of the same commit the scaffolder came from, so both sides of the pairing rule below hold automatically. -`@1` resolves to the last preview published for PR #1 — commit `5685521` at the +`@1` resolves to the last preview published for PR #1, commit `5685521` at the time of its merge, which is the state that landed on `main`. ## Pin an exact commit @@ -64,7 +64,7 @@ the optional compiler/runtime release-pair record declared by peer). A regular `dependencies` entry that names a sibling workspace package is rewritten to that sibling's same-sha tarball URL: `@agent-bundle/runtime`'s `rsc-markdown-stream` dependency resolves to the renderer preview of the same -commit. Installing both packages from the same sha therefore works with stock npm — no +commit. Installing both packages from the same sha therefore works with stock npm, no `--legacy-peer-deps` needed. Mixing two different shas fails with `ERESOLVE` by design; use one sha (or one PR number) for both URLs. Previews published before the peer rewrite landed (PR #46, fixing #45) still carry the original diff --git a/examples/audiobook-curator/README.md b/examples/audiobook-curator/README.md index 901b14ebb..406fed978 100644 --- a/examples/audiobook-curator/README.md +++ b/examples/audiobook-curator/README.md @@ -65,10 +65,10 @@ server selector. The CLI is the same 16 tools. Each tool has a colocated `.cli.ts` projection module (#725) that declares its command name, option spellings, -positionals, and confirmation policy — `search_audible.cli.ts` spells the tool +positionals, and confirmation policy, `search_audible.cli.ts` spells the tool as `audible-search` with `--duration` and a comma-separated or repeatable `--regions`, `prepare_audiobook.cli.ts` spells `outputRoot` as `--output`, and -`inspect_sources.cli.ts` takes `` as a positional — so the compiled graph +`inspect_sources.cli.ts` takes `` as a positional, so the compiled graph carries exactly 16 CLI commands, each with the tool's identity (`tool:curator/`). There is no `src/cli/` directory, no second input schema or render body per command, and no `audiobook-curator curator ` @@ -77,8 +77,8 @@ the plan-first commands gate their mutation on `--apply`, never on `--yes`. ### `src/layout.tsx` is the shared document shell -The conventional layout module wraps every rendered route once — the 16 MCP -tools (on both surfaces), the catalog resource, and the curate prompt — so no +The conventional layout module wraps every rendered route once, the 16 MCP +tools (on both surfaces), the catalog resource, and the curate prompt, so no route imports a wrapper to get the server's standard document structure. The layout renders a container `Agent.Result` and the runtime merges each route's own @@ -146,7 +146,7 @@ The conventional state module defines the workspace-durable `mutationApplied`, and `shelfCleared`. `select_audible_edition` dispatches the selection event; `apply_audiobook_metadata` and `apply_audiobook_chapters` dispatch mutation records and render the updated shelf. The read-only -`review_curation_shelf` tool — `shelf` on the command line — exposes the +`review_curation_shelf` tool, `shelf` on the command line, exposes the mounted state. If state is not mounted, it returns an empty structured shelf and renders an explicit unavailable notice on both surfaces. @@ -155,7 +155,7 @@ and renders an explicit unavailable notice on both surfaces. `audit_library` places the asynchronous `LibraryAnalysis` component behind React `Suspense`. While that component re-stats duplicate candidates and calculates reclaimable bytes, its fallback is an `Agent.Progress` document -node — and that node is the whole progress story: the generated MCP projector +node, and that node is the whole progress story: the generated MCP projector turns the streamed fallback into `notifications/progress` for a client that sent a progress token, then replaces it with the completed analysis without changing the final structured `LibraryAuditReceipt`. No `progress.report()` @@ -165,8 +165,8 @@ call repeats the fallback's message. On an interactive terminal the ### Every command renders its tool's document Each command runs its tool's component: piped output is one final Markdown -document — the same headline and report the MCP client receives as text -content — and an interactive terminal draws the streamed `Agent.Progress` +document, the same headline and report the MCP client receives as text +content, and an interactive terminal draws the streamed `Agent.Progress` fallback in place. `--json` selects machine output and emits one result-schema-validated JSON value followed by a newline: the canonical final `Agent.Result` value, never the Markdown presentation or an intermediate @@ -176,8 +176,8 @@ projected as flags; `convert` carries its optional `receipt` inside the JSON `conversion` object. A command that gets one still writes the receipt file when it succeeds. This is a behavior change from the retired `src/cli/` tree, where every command that -took a report or receipt path — thirteen of the sixteen, `inventory --report` -and the convert receipt among them — required it: those commands now run +took a report or receipt path, thirteen of the sixteen, `inventory --report` +and the convert receipt among them, required it: those commands now run without the path and write no report or receipt file, and their exit codes, `--apply` gating, and error output are unchanged either way. `inspect`, `prepare`, and `shelf` never took one. diff --git a/examples/audiobook-curator/src/skills/curate-audiobooks/SKILL.md b/examples/audiobook-curator/src/skills/curate-audiobooks/SKILL.md index 99025ac01..d22c11605 100644 --- a/examples/audiobook-curator/src/skills/curate-audiobooks/SKILL.md +++ b/examples/audiobook-curator/src/skills/curate-audiobooks/SKILL.md @@ -1,6 +1,6 @@ --- name: curate-audiobooks -description: Inventory, match, convert, repair, or audit audiobooks. Use for a concrete curation step — not general media discussion. +description: Inventory, match, convert, repair, or audit audiobooks. Use for a concrete curation step, not general media discussion. --- # Curate audiobooks diff --git a/examples/hooks-and-scripts/README.md b/examples/hooks-and-scripts/README.md index 7149fe7a3..6ddd99277 100644 --- a/examples/hooks-and-scripts/README.md +++ b/examples/hooks-and-scripts/README.md @@ -11,11 +11,11 @@ session-start Hook, a manifest-backed packaging check, and a risk-register check so the Workbench can show canonical Hook simulation, successful and blocking script traces, and live Logs. Both scripts export `main` and return their exit codes; the build generates the process envelope that owns argv, -awaiting, and exit-code adoption. `verify-release` ships by convention — any -unclaimed plain script under `src/scripts/` is discovered — while +awaiting, and exit-code adoption. `verify-release` ships by convention, any +unclaimed plain script under `src/scripts/` is discovered, while `detect-risk` stays explicitly configured because it selects a host: it is emitted only when the build selects `portable`, into the shared `scripts/` of -the one plugin root every selected host installs — so the example keeps both +the one plugin root every selected host installs, so the example keeps both modes covered. The plain Hook imports the application-owned `releaseContext` function from diff --git a/examples/host-test/README.md b/examples/host-test/README.md index 4f52d455b..cf71ea1d5 100644 --- a/examples/host-test/README.md +++ b/examples/host-test/README.md @@ -2,7 +2,7 @@ A probing plugin. Install it into a Claude Code, Codex, or Cursor home, drive one agent session, and read back exactly what that host sent to every plugin -hook and MCP call — the raw envelope, the framework request context each +hook and MCP call, the raw envelope, the framework request context each handler saw, and the conversation lineage the runtime resolved for it. It is the acceptance vehicle for `request.lineage` and the evidence source for `docs/audits/*-host-lineage-matrix.md`. @@ -34,7 +34,7 @@ bounded summary into the durable state kernel (`src/state.ts`, | `event.canonical` | The framework's canonical identity (`event`, `idempotencyKey`, `observedAt`, `provenance`). | | `request` | `(await agent())` as the route saw it: `invocation`, `host`, `session`, `actor`, `workspace`, `capabilities`, `lineage`, provider keys, and whether state and notices were mounted. `lineage` is always present: `available` with the resolved tree position, or `unavailable` with the runtime's per-host reason. | | `ids` | Every identity-shaped native field (`conversation_id`, `generation_id`, `session_id`, `subagent_id`, `tool_call_id`, `agent_id`, `turn_id`, `user_email`, …) lifted out for filtering. | -| `process` | `pid`, `ppid`, `cwd`, `execPath`, entry file, uptime — of the process that ran the route. | +| `process` | `pid`, `ppid`, `cwd`, `execPath`, entry file, uptime, of the process that ran the route. | | `runtime` | `standalone-hook` for handler-only events, `shared-runtime` when the rendered session-start view reaches the warm MCP-hosted runtime, `mcp-server`, or `cli`. | | `env.names` | Environment variable **names** matching `CURSOR_*`, `CLAUDE_*`, `CODEX_*`, `AGENT_BUNDLE_*`, `PLUGIN_*`, `MCP_*`, `HOST_TEST_*`. Values are never written. | @@ -43,8 +43,8 @@ Two MCP servers ship in the plugin: - `host-test` (generated routes, `src/mcp/host-test/tools/`): `dump` (filter by any conversation/session/subagent id, `full` for raw lines), `reset`, and `slow`. Each `dump` call records the request context the generated server - mounted for it. A bare `dump` returns the newest 50 matching records — a - whole log of a few hundred records overflows the tool-result document — so + mounted for it. A bare `dump` returns the newest 50 matching records, a + whole log of a few hundred records overflows the tool-result document, so pass `limit` (up to 5000) for more; `matched` and `total` always count the whole log. `slow` holds a call open for `holdMs` (up to 30 s) and reports progress every `tickMs`; it declares `execution.taskSupport: "optional"`, so @@ -53,8 +53,8 @@ Two MCP servers ship in the plugin: while every other host receives the ordinary result. The recorded call and the `host-test-raw` envelope show which path the host took. - `host-test-raw` (hand-rolled stdio factory, `src/mcp/host-test-raw.ts`): - `probe` records the raw SDK request context — session id, JSON-RPC id, - `_meta`, lifted envelope, negotiated client info — so hook↔MCP correlation is + `probe` records the raw SDK request context, session id, JSON-RPC id, + `_meta`, lifted envelope, negotiated client info, so hook↔MCP correlation is judged against the wire. The rendered CLI `host-test dump [--conversation ] [--full] [--log ]` @@ -131,7 +131,7 @@ the printed command, open the Agents pane, and use the same scenario prompt. ## Workbench walkthrough 1. **Application** lists the twenty event routes, both MCP servers, the skill, - and the routed CLI with their per-target capability judgments — `workspace/open` + and the routed CLI with their per-target capability judgments, `workspace/open` is Cursor-only, `task/*` and `file/change` are Claude-only, and portable carries no hooks at all. 2. Under **Application → Events / Hooks**, select any family and run it with diff --git a/examples/rsc-agent-runtime/README.md b/examples/rsc-agent-runtime/README.md index d1ec1fec7..162c67f64 100644 --- a/examples/rsc-agent-runtime/README.md +++ b/examples/rsc-agent-runtime/README.md @@ -11,7 +11,7 @@ This private, opt-in example shows one React Server Components (RSC) runtime sha | RSC render | Agent Document component trees projected from Flight | One request | | MCP App UI | Mounted timeline, Refresh, and recoverable row selection | One UI instance | -Native hooks are fresh requests: the compiler-generated client validates one host event, invokes `src/events/tool/after.tsx` in its explicit standalone mode, projects the final Agent Document, and exits. The durable kernel—not a Node module cache or React state—connects later hook processes and MCP calls. +Native hooks are fresh requests: the compiler-generated client validates one host event, invokes `src/events/tool/after.tsx` in its explicit standalone mode, projects the final Agent Document, and exits. The durable kernel, not a Node module cache or React state, connects later hook processes and MCP calls. ```tsx // The semantic event route receives canonical identity — including the family's @@ -71,7 +71,7 @@ node packages/workbench/scripts/capture-runtime-playground.mjs \ The `--compile-error` capture shows the Workbench diagnostic the provider publishes when a source change fails to compile: code `AB8206`, phase -`source/build`, and a message that carries the Rspack errors themselves — one +`source/build`, and a message that carries the Rspack errors themselves, one `file:line:col: message` line per error, with the path relative to the example root, ANSI colour and the SWC code frame stripped. Breaking `src/rsc/worker.tsx` produces, for example: @@ -83,7 +83,7 @@ src/rsc/worker.tsx:177:6: Module build failed (from builtin:swc-loader): Syntax The active generation stays served while the diagnostic is shown, and the next successful compile clears it. The development session runs Rsbuild at -`logLevel: 'silent'`, so the diagnostic — not the provider's console — is the +`logLevel: 'silent'`, so the diagnostic, not the provider's console, is the one place the message lands; the errors are read with the `agent-bundle/api` helpers (`rspackStatsErrors`, `formatRspackStatsError`) the framework's own `AB4770` App diagnostics use. The production `rsbuild build` has no diagnostic @@ -118,7 +118,7 @@ Installing [the optional RSC Runtime topology](../../docs/architecture/rsc-runtime-workbench.md) for the full ownership boundary. -The build emits `dist/runtime` (including `dist/runtime/agent-runtime.manifest.json`), self-contained `dist/app` MCP App documents, and one self-contained composite plugin root at `dist/plugins` (the portable, Claude Code, and Codex projections over the shared payload). `dist/app` holds exactly one HTML file per App entry (`edit-timeline-v1.html`, `standalone.html`) with every script, style, asset, and licence comment inlined — the same invariants the framework's MCP App compiler enforces (`splitChunks: false`, unbounded `dataUriLimit`, no async chunks, `legalComments: 'inline'`); the build fails if the resolved configuration drifts from them or any sibling file would be emitted. The packaging step can also be rerun directly against the current Rsbuild output: +The build emits `dist/runtime` (including `dist/runtime/agent-runtime.manifest.json`), self-contained `dist/app` MCP App documents, and one self-contained composite plugin root at `dist/plugins` (the portable, Claude Code, and Codex projections over the shared payload). `dist/app` holds exactly one HTML file per App entry (`edit-timeline-v1.html`, `standalone.html`) with every script, style, asset, and licence comment inlined, the same invariants the framework's MCP App compiler enforces (`splitChunks: false`, unbounded `dataUriLimit`, no async chunks, `legalComments: 'inline'`); the build fails if the resolved configuration drifts from them or any sibling file would be emitted. The packaging step can also be rerun directly against the current Rsbuild output: ```bash pnpm --filter @agent-bundle/rsc-agent-runtime-demo exec agent-bundle build --json --output dist/plugins @@ -334,7 +334,7 @@ workspace on Node's built-in `node:sqlite`, with WAL journaling, transactional commits, idempotency-key replay, and exact-revision reads. The retired hand-rolled JSONL kernel is gone; this example now only declares its schema, events, and reducer. Loading the sqlite driver emits Node's one-time -`ExperimentalWarning: SQLite is an experimental feature` on stderr — expected +`ExperimentalWarning: SQLite is an experimental feature` on stderr, expected on the supported Node lines (`node:sqlite` needs no flag on Node >= 22.13), harmless for hooks and MCP servers (protocol output uses stdout), and absent for stateless consumers because the driver lives behind its own subpath. It is diff --git a/examples/worktree-proximity/README.md b/examples/worktree-proximity/README.md index ebac493b1..e494f7638 100644 --- a/examples/worktree-proximity/README.md +++ b/examples/worktree-proximity/README.md @@ -2,8 +2,8 @@ This advanced composition reference coordinates one root task and two child agents working in linked worktrees of the same Git repository. The runtime's -lineage registry supplies the agent tree — who the root is, which children -are alive, who is a sibling — through `(await agent()).lineage`; application +lineage registry supplies the agent tree, who the root is, which children +are alive, who is a sibling, through `(await agent()).lineage`; application code records only which worktree each agent works in and its current intent, detects path or dependency overlap, warns the actor handling the current event, and publishes a durable notice addressed to the other actor. The @@ -24,8 +24,8 @@ This example is intentionally not part of the newcomer path. 5. A conflict renders an `Agent.Context` warning with an `outcome: continue` result and publishes a notice addressed to the other actor's lineage conversation (`recipient.conversation`). -6. That actor's next event — and only that actor's, even when a sibling works - in the same worktree — admits the pending notice, changes its +6. That actor's next event, and only that actor's, even when a sibling works + in the same worktree, admits the pending notice, changes its evidence-backed state to `attempted`, and renders its content as context. 7. `tool/after` records an empty current intent; `agent/stop` and `stop` release the actor's binding and whatever intent it still held (the @@ -45,7 +45,7 @@ repository-relative slash-separated paths by the domain module. The application has four planes: -- **Lineage** — the agent tree is the runtime's. `(await agent()).lineage` +- **Lineage**: the agent tree is the runtime's. `(await agent()).lineage` answers who this request is (`conversation`, `parent`, `root`, `depth`, `resolution`) and, through `lineage.value.tree`, who else is alive: `siblings` (every other live conversation under the same root, the root @@ -54,7 +54,7 @@ The application has four planes: turns that into the coordinator's report (the `agent-topology` provider calls it over the `lineage` the framework hands it); `liveConversations()` turns it into the liveness the domain uses. -- **Providers** — `git-worktree` derives repository, branch, commit, common +- **Providers**: `git-worktree` derives repository, branch, commit, common Git directory, and linked-worktree identity without throwing for expected degradation. `agent-topology` assembles the coordinator's snapshot once per request from the request view every provider receives: the agent tree @@ -62,15 +62,15 @@ The application has four planes: mounted intent state through `context.state.read()`, and the counts of the notices this caller published through `context.notices.published()`; each part carries its own availability, and the provider can only read. -- **Events** — canonical shared-runtime routes bind actors to worktrees, +- **Events**: canonical shared-runtime routes bind actors to worktrees, record or clear intent, detect conflicts, render current-actor context, release stopped actors, and publish or admit notices. -- **State and notices** — one workspace-durable intent definition (worktree +- **State and notices**: one workspace-durable intent definition (worktree bindings, activities, refusals) and the framework notice definition share the generated runtime's SQLite driver. Routes use only the mounted `(await agent()).state` and `(await agent()).notices` handles; SQLite supplies cross-process durability and idempotency without a daemon. -- **Domain** — `src/domain/proximity.ts` contains all collision decisions and +- **Domain**: `src/domain/proximity.ts` contains all collision decisions and performs no I/O. The generated runtime owns the durable root. It mounts SQLite at @@ -81,9 +81,9 @@ store from Git identity data; `gitWorktree.commonDir` remains identity evidence only. `providers.agentTopology` is that snapshot: a provider factory receives the -request's `host`, `session`, `workspace`, `plugin`, and `lineage` — with the -live tree ([#457](https://github.com/scriptedalchemy/agent-bundle/issues/457)) -— plus read-only `state` (`read()`) and `notices` (`inbox()`, `published()`) +request's `host`, `session`, `workspace`, `plugin`, and `lineage`, with the +live tree ([#457](https://github.com/scriptedalchemy/agent-bundle/issues/457)), +plus read-only `state` (`read()`) and `notices` (`inbox()`, `published()`) handles ([#459](https://github.com/scriptedalchemy/agent-bundle/issues/459)), so the coordinator `status` tool reads `providers.agentTopology` and performs no read of its own. Event routes still use the mounted `(await agent()).state` @@ -102,13 +102,13 @@ registry's own resolutions (`confirmed` once the host has named every edge up to the root; `transcript` is read from the host's own rollout file), and `derived` is this application's fallback: -- `session/start` binds the root conversation — `(await agent()).lineage.root` - when the runtime resolved a lineage and the native `session_id` otherwise — +- `session/start` binds the root conversation: `(await agent()).lineage.root` + when the runtime resolved a lineage and the native `session_id` otherwise, to its worktree, under the same id `request.lineage.tree` lists it by. - `agent/start` binds the child named by `request.lineage` (`conversation`, with `resolution` as the binding's provenance) when the runtime placed the - start below the root — which needs the spawning `Agent`/`Task` - `tool/before` to have passed through the same shared runtime — and the + start below the root, which needs the spawning `Agent`/`Task` + `tool/before` to have passed through the same shared runtime, and the native `agent_id` otherwise. Either way the child's actor id is its lineage conversation (Claude and Codex spell it `agent_id`), which is what a directed notice targets. The edge itself (parent, depth, root) is not @@ -137,7 +137,7 @@ the tree no longer lists under our root is stale and warns nobody; a derived way; and a lineage with no tree (a payload that proved only its own chain, a standalone hook, or none at all) presumes nothing about who stopped. The coordinator `status` tool reports the tree the runtime resolved for *its* -call — a client no pre-tool hook window names gets an honest +call, a client no pre-tool hook window names gets an honest `agents: unavailable`, never a tree from another caller's point of view. Unsupported worktree, actor, parent, state, and delivery conditions are @@ -154,9 +154,9 @@ reason as `Agent.Context`; there is no fallback write path. Notice admission runs once per event invocation in the render scope. Generated event principals mount host, session, workspace, and lineage identity, but not actor identity (#391/#444). A proximity notice is therefore -addressed to the other actor's lineage conversation — +addressed to the other actor's lineage conversation, `recipient: { conversation }`, matched against the admitting request's -`request.lineage.conversation` — so only that agent thread admits it, even +`request.lineage.conversation`, so only that agent thread admits it, even when a sibling shares its worktree and every subagent shares the root `session_id`. An event whose lineage the runtime could not resolve (no shared runtime, an unplaced `agent_id`) is never the addressed agent; the notice @@ -168,7 +168,7 @@ message to one peer, not to the tree. `(await agent()).notices.read()` exposes only deliveries attempted for the current invocation, and `inbox()` only what is pending for the current recipient. The coordinator status reports, beside the agent tree, bindings, -and intents, what became of the notices *the calling agent* published — `pending`, +and intents, what became of the notices *the calling agent* published, `pending`, `attempted`, `acknowledged`, and the other ledger states, counted by the `agent-topology` provider from the request's own `notices.published()` ([#460](https://github.com/scriptedalchemy/agent-bundle/issues/460)). That @@ -192,8 +192,8 @@ spawning `Agent` `PreToolUse` opens the registry's spawn window and the child's hook payloads carry its `agent_id`, as Claude's do; an event the runtime cannot place under that child is not delivered to), replay idempotency, exact-revision restart durability, and the registry-fed agent -tree — spawned children visible to the root's `status` call, still visible -after a server restart, gone after `agent/stop` — through the generated MCP +tree, spawned children visible to the root's `status` call, still visible +after a server restart, gone after `agent/stop`, through the generated MCP server. Journey 8 has two honesty layers: the generated wrapper fails closed on an identity-less `SubagentStart` for host contracts that require `agent_id`, while the route-unit suite proves the route records a refusal for @@ -205,5 +205,5 @@ or displays projected context in production. Version 1 connects no external driver adapter and claims none. A future external adapter must pass the framework state-driver conformance suite -before any “integrated” claim. The generated runtime's SQLite driver is the +before any "integrated" claim. The generated runtime's SQLite driver is the only durable driver used by this example. diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index b686f57be..b1e709e9f 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -18,7 +18,7 @@ agent-bundle dev --root . `inspect` reads source configuration; use `validate --artifact artifact` for source-free artifact validation. `dev.runtime.provider` is an advanced optional extension: a normal project starts the dev server and Workbench without loading an RSC provider. -The artifact root defaults to `artifact` for the `agent-bundle build` and `agent-bundle prepack` commands (they also emit the npm package build into `dist/`) and to `dist` for the programmatic `build()` API; set `output: { distPath: '' }` in `agent-bundle.config.ts` to relocate it (Rsbuild/Rslib naming, string shorthand only) — `--output` still wins per invocation. See [Framework mode](../../docs/framework-mode.md#output). +The artifact root defaults to `artifact` for the `agent-bundle build` and `agent-bundle prepack` commands (they also emit the npm package build into `dist/`) and to `dist` for the programmatic `build()` API; set `output: { distPath: '' }` in `agent-bundle.config.ts` to relocate it (Rsbuild/Rslib naming, string shorthand only), `--output` still wins per invocation. See [Framework mode](../../docs/framework-mode.md#output). Generated executables target Node.js 22.12 or newer by default. `runtime: { node: '24.0' }` raises that floor (it can never be lowered), and the selected floor is recorded as `runtime.node` in the @@ -105,13 +105,13 @@ merged last into every synthesized config and bounded by the artifact invariant MCP App views (`src/mcp//apps/*` or `mcp.servers..apps`) compile to one self-contained HTML resource each and talk to their host through `createAppClient()` from the browser-safe -`agent-bundle/app` — the MCP Apps handshake, request ids, timeouts and cancellation, JSON-RPC and +`agent-bundle/app`, the MCP Apps handshake, request ids, timeouts and cancellation, JSON-RPC and result decoding, and exact parent source/origin pinning are framework-owned, so a view never hand-writes `postMessage` frames. `call('tool:/', input)` resolves the tool's structured result object directly (an object-rooted `resultSchema` is what makes a tool callable -from an App), `onToolInput` / `onToolResult` / `onToolError` observe the opening call — dispatched +from an App), `onToolInput` / `onToolResult` / `onToolError` observe the opening call, dispatched to the listeners registered for the tool the host's initialize result names, with a failed or -malformed opening result arriving on `onToolError` — `onToolCancelled` reports its cancellation, +malformed opening result arriving on `onToolError`, `onToolCancelled` reports its cancellation, `request()` covers `resources/read` and supported `ui/*` methods, and every wire or lifecycle failure is an `AppClientError` with a discriminated `code` (option misuse is a plain `TypeError` or `RangeError`). A request that times out or is aborted sends `notifications/cancelled`, which the @@ -235,8 +235,8 @@ itself stays spec-conformant for other Agent Plugins clients. Cursor installation is user-scoped. Amp accepts project scope and user scope (its system root). Claude also accepts `--scope project` and `--scope local`; -Codex is user-scoped. `--from` names the composite root itself — -the directory that holds `agent-bundle.manifest.json` — and a source-free copy +Codex is user-scoped. `--from` names the composite root itself, +the directory that holds `agent-bundle.manifest.json`, and a source-free copy of that root is accepted. Identity (name, version, marketplace) and the host plugin document come from that manifest's `application` and `projections[]` rows, never from probing host files; a root whose manifest is missing, not @@ -332,7 +332,7 @@ receipt and remove exactly what it owns: - Claude / Codex: `claude plugin uninstall --scope --keep-data` / `codex plugin remove `, then `plugin marketplace remove ` unless another installed plugin still uses it (live row, another store - receipt, or — Claude — an install at another scope or in another project + receipt, or, Claude, an install at another scope or in another project recorded in `plugins/installed_plugins.json`), then the store receipt. An unusable `plugin list --json` fails closed (`AB7004`); a registration the host no longer holds is `already-absent`, so an orphaned receipt is consumed @@ -419,7 +419,7 @@ disk, while the stable proxy command fails closed until that project dev server `script.run` is a production-mounted, trusted-local Playground operation. It runs only the selected manifest-owned emitted script for the selected target, in a managed workspace, and preserves bounded stdout/stderr, exit, cancellation, and raw event references. Native prompts choose a server catalog -selection — case, fixture, host, and pinned model — for the selected epoch rather than accepting a +selection, case, fixture, host, and pinned model, for the selected epoch rather than accepting a browser-supplied command or model. Only actions started in Playground join its ordered durable trace. Hook and MCP page operations remain @@ -466,7 +466,7 @@ longer prepares at all, the `dev.contracts` declaration cannot be read and the r adopted directly, exactly as an undeclared project would be. The Workbench project stream emits `dev.contract.status`, and `status()` (and `/api/project/status`) -carries a `hostAdoption` snapshot — `mode` (`gated` or `direct`), the `adoptedEpochId` hosts serve, +carries a `hostAdoption` snapshot, `mode` (`gated` or `direct`), the `adoptedEpochId` hosts serve, and the latest `contracts` evaluation. The Overview page renders it as **Host adoption**: a failed gate names the published build, the build hosts kept, and the failed check names grouped by route, and folds the gate diagnostics into the Diagnostics table; the Logs page carries the same records. @@ -556,10 +556,10 @@ bundler configuration. Two subpaths ship that harness, and both are opt-in: `@rstest/core` and `react` are optional peer dependencies, so a project that never tests routes installs neither. Rendering also needs `@agent-bundle/runtime`, which the project already owns whenever it has route -modules — the generated entries import it the same way. +modules, the generated entries import it the same way. -`agent-bundle/rstest` is the configuration helper. It compiles the project once -— the same route-graph compilation the build performs, with no artifact build — +`agent-bundle/rstest` is the configuration helper. It compiles the project once, +the same route-graph compilation the build performs, with no artifact build, and returns a plain Rstest configuration object carrying the test manifest, the route loaders, React's `react-server` resolution, and the automatic JSX runtime: @@ -583,11 +583,11 @@ carrying the project identity the compiler pass reported (`name`, source that imports it loads under the pool without a build. A pool that is not built from `agentBundleRstest()` (or `agentBundleBrowserRstest()`) has no such alias, and importing that source raises `AB4760`; build the pool that -reaches it from the preset too — `agentBundleRstest({ include: ['tests/unit/**/*.test.ts'] })` -— or add the alias the diagnostic names. +reaches it from the preset too, `agentBundleRstest({ include: ['tests/unit/**/*.test.ts'] })`, +or add the alias the diagnostic names. -`agent-bundle/test` holds the helpers. `renderRoute` executes a route — by -compiled route id, or by importing the module directly — through the real +`agent-bundle/test` holds the helpers. `renderRoute` executes a route, by +compiled route id, or by importing the module directly, through the real renderer and the real request store, and resolves to the final Agent Document: ```ts @@ -605,18 +605,18 @@ expectDocument(document).toHaveStatus('success').toContainMarkdown('Dune').toHav ``` `renderRoute` accepts `input`, `args` (CLI routes), request-`context` -overrides — including a `context.progress` reporter — render `limits`, and a +overrides, including a `context.progress` reporter, render `limits`, and a `signal`; it returns the document, the request-scoped progress the route reported, the resolved provenance, and the route's own `resultSchema`-parsed value. Progress is recorded whether or not the caller supplies a reporter of its own. `testManifest()` exposes the compiled route inventory, so a suite can iterate every route in process rather -than paying for a build per route. Every failure — an unknown route, a refused -route kind, a rejected input, a render error — names the route id, the target +than paying for a build per route. Every failure, an unknown route, a refused +route kind, a rejected input, a render error, names the route id, the target kind, and the module provenance. `loadRouteModule(id)` returns the evaluated module behind one compiled id through the same registered loader `renderRoute` -uses — the module object itself, so `inputSchema`, `resultSchema`, `config`, -and `default` are the route's own exports by reference — which replaces a +uses, the module object itself, so `inputSchema`, `resultSchema`, `config`, +and `default` are the route's own exports by reference, which replaces a hand-maintained list of static route imports in a schema-identity suite; it fails closed with `manifest-unavailable` outside an `agentBundleRstest()` pool or against a manifest the registered loaders did not come from. @@ -632,13 +632,13 @@ is its `{ canonical, native }` payload and its `result` `undefined`); `RegisteredRouteId`, `RegisteredRouteInput`, and `RegisteredRouteResult` from `@agent-bundle/runtime` name that surface for wrappers. A value typed `string`, a module target, or a -program without the generated file sees the previous types — any id, `unknown` +program without the generated file sees the previous types, any id, `unknown` input and result. The registration flows to every harness surface that takes a route id or payload, not only `renderRoute`: `invokeMcpTool('find', { input })` and `getMcpPrompt` check the wire name against the registered tool/prompt names and -type `input` from that route — of the literal `server`, when passed +type `input` from that route, of the literal `server`, when passed (`RegisteredMcpServerName`, `RegisteredMcpRouteName`, and `RegisteredMcpRouteId` name what a `tool:/` id encodes); the contract matrices type each registered key of `fixtures` while an MCP App key @@ -651,9 +651,9 @@ only for object-valued documents) deliberately stay untyped. Conventional request context providers (`src/providers/*`, see [entry conventions](../../docs/entry-conventions.md#request-context-providers-power-tier)) -are resolved on demand for every manifest-backed helper — `renderRoute`, +are resolved on demand for every manifest-backed helper, `renderRoute`, `renderRouteEvents`, `invokeCli`, `runScript` (rendered scripts), and the -in-memory MCP helpers — exactly as the +in-memory MCP helpers, exactly as the generated request scopes mount them: discovered from the compiled manifest, loaded once per requested key per request, handed the same surface-specific `invocation` (`tool`, `event`, `cli`, `script`), and failing the @@ -708,8 +708,8 @@ is never a receipt for another. | `route-unit` | `renderRoute`, `renderRouteEvents` | a route module renders to the document (and render-event stream) it claims | | `mcp-in-memory` | `openInMemoryMcpServer`, `invokeMcpTool`, `readMcpResource`, `getMcpPrompt`, `listMcpSurface`, `runContractMatrix` | the real generated MCP server's protocol contract, over the SDK's in-memory transport | | `cli-dispatch` | `invokeCli`, `cliJson`, `cliNdjson` | a plain or rendered argv vector resolved and run through the routed CLI's own shell, including rendered Markdown, explicit TTY, JSON, and NDJSON modes, in-process | -| `script-dispatch` | `runScript`, `scriptJson`, `scriptNdjson` | a conventional `src/scripts/*` module run through its generated executable's contract: a rendered `.tsx` script through the rendered-script shell in-process (piped Markdown, explicit TTY, `--json`, `--ndjson`), a plain `.ts` script through the `main` process envelope as a Node process of its own over the source — fresh module state, real `process.exit`, its own argv, exit code, and streams — without bundling | -| `workbench-surface` | `inspectWorkbenchSurface`, `workbenchSurfaceFromRouteGraph` | what the dev server would hand the Workbench for this project — the route manifest, the grouped route catalog, the state declaration, lifecycle-replay fixtures per host, and page availability — from the same compiler pass and projection functions, with no browser and no dev server | +| `script-dispatch` | `runScript`, `scriptJson`, `scriptNdjson` | a conventional `src/scripts/*` module run through its generated executable's contract: a rendered `.tsx` script through the rendered-script shell in-process (piped Markdown, explicit TTY, `--json`, `--ndjson`), a plain `.ts` script through the `main` process envelope as a Node process of its own over the source, fresh module state, real `process.exit`, its own argv, exit code, and streams, without bundling | +| `workbench-surface` | `inspectWorkbenchSurface`, `workbenchSurfaceFromRouteGraph` | what the dev server would hand the Workbench for this project, the route manifest, the grouped route catalog, the state declaration, lifecycle-replay fixtures per host, and page availability, from the same compiler pass and projection functions, with no browser and no dev server | | `packed-stdio` | `openPackedMcpServer`, `runPackedContractMatrix` | a built artifact's generated entry running as a real process over stdio | | `packed-deleted-source` | `removeProjectSource`, `openPackedMcpServer({ deletedSource })`, `runPackedContractMatrix` | the packed stdio process still runs after project source and configuration are removed and verified absent | | `host-install` | `openInstalledHostMcpServer`, `runInstalledHostContractMatrix` | a built bundle staged into an isolated host root, discovered in the emitted host format, and spawned from the installed layout | @@ -738,7 +738,7 @@ expect(tty.stdout).toContain('\r\u001B[2K'); `runScript` is the same idea for the `src/scripts/*` convention. The manifest carries every conventional script with its extension contract (`testManifest().scripts`), and the helper runs the module through what its -generated `scripts/.mjs` would do — never by bundling it: a rendered +generated `scripts/.mjs` would do, never by bundling it: a rendered `.tsx` script runs in-process through the same shell the executable uses, and a plain `.ts` script runs as a Node process of its own, as the executable does (see below): @@ -764,8 +764,8 @@ A plain script runs as a Node process of its own over the source module, so the process contract is Node's rather than a simulation of it: every run evaluates the module afresh (module-level state never survives between runs, as it never survives between processes), `process.argv` is -`[node, , ...argv]`, `process.exit` ends the script for real — work -queued after it never runs, whether or not the script caught the call — +`[node, , ...argv]`, `process.exit` ends the script for real, work +queued after it never runs, whether or not the script caught the call, process-level APIs such as `process.chdir` work and affect only the script, a numeric `main` return goes through the real `process.exitCode` setter (`300` reports `44`; `1.5` exits 1 with the setter's `RangeError`), a signal that @@ -776,8 +776,8 @@ between the `main` envelope and a self-executing module, and a non-callable `main` fails the way the generated executable fails. The process resolves relative `.js` specifiers to their TypeScript sources, transforms `.ts` with Node's own type transform, lowers the `.tsx` and `.jsx` helpers a plain -script imports with the bundler's SWC — the same lowering the generated -executable was built with — and serves `agent-bundle/meta` as the identity the build stamps from the +script imports with the bundler's SWC, the same lowering the generated +executable was built with, and serves `agent-bundle/meta` as the identity the build stamps from the manifest's `plugin`. Explicit `scripts:` configuration entries are bundled entries rather than routes and stay with the packed level. A rendered script composes the project's root layout (a script belongs to no server, so no @@ -795,8 +795,8 @@ it reads end-of-file at once); `process.execArgv` is empty as under plain `node`; an aborted `signal` sends SIGTERM and, should the script trap it, kills the process after a one-second grace before the run rejects. A rendered script's own `console` and stream writes during the run land on the -invocation's `stderr` — the generated executable forwards its render worker's -stdout and stderr there — so `stdout` holds machine output only and nothing +invocation's `stderr`, the generated executable forwards its render worker's +stdout and stderr there, so `stdout` holds machine output only and nothing escapes into the test runner. `process.exit` from rendered code is that worker's exit, never the test process's: the run fails as the executable's shell reports it (`Generated render worker exited with code N.` on `stderr`, @@ -809,11 +809,11 @@ module load that has not begun is started on its behalf. Every `ScriptInvocation `inspectWorkbenchSurface` answers "what would the Workbench show for this project?" without a browser. It runs the dev server's own preparation as the -Workbench server constructs it — `development` mode for a configuration +Workbench server constructs it, `development` mode for a configuration factory that branches on `context.mode`, the selected `configPath` for both -the compiler pass and eval-suite discovery — and the same projection functions -the dev server serves — `GET /api/routes/manifest` and `GET /api/lifecycles` -byte for byte — then applies the Workbench's own grouping and navigation +the compiler pass and eval-suite discovery, and the same projection functions +the dev server serves, `GET /api/routes/manifest` and `GET /api/lifecycles` +byte for byte, then applies the Workbench's own grouping and navigation rules: ```ts @@ -836,8 +836,8 @@ and the RSC runtime page are artifact- or process-bound and are not projected here. `expectEvents` asserts over a render-event stream. `toContainSequence` is -sequence-tolerant — an extra `progress` or `replace` frame is legal and cannot -turn a passing render red — while a missing frame, a reordering, or a regressed +sequence-tolerant, an extra `progress` or `replace` frame is legal and cannot +turn a passing render red, while a missing frame, a reordering, or a regressed ordinal still fails; `toHaveMonotonicSequence`, `toCompleteOnce`, `toHaveProgress`, and `toHaveNoErrors` cover the rest of the contract. @@ -861,7 +861,7 @@ remain login-gated. The contract matrix is the framework-owned generated-plugin wire-contract suite. Three entry points share one implementation; boundary differences are explicit -capability flags, not forked check logic. The project supplies only fixtures — +capability flags, not forked check logic. The project supplies only fixtures, valid inputs, a declared `resultCompat` policy for every in-memory tool route, optional `previousResults` payloads, optional `cancellation` cases, and an optional deterministic lifecycle transition driver with declarative @@ -898,8 +898,8 @@ against an already-open packed session (the single packed journey owns session open/close). It proves process stdio evidence for surface completeness (including compiled MCP App resource URIs in `listResources`), fixture coverage, successful-path sweeps, advertised input-schema rejection, and client-side -cancellation hygiene. It cannot load project route modules — source may be -deleted and verified absent — so serialized-round-trip, compat-probe, and +cancellation hygiene. It cannot load project route modules, source may be +deleted and verified absent, so serialized-round-trip, compat-probe, and version-skew (including their per-lifecycle-phase variants) are reported `not-applicable` with an honest reason. The packed server validates every tool result through its bundled `resultSchema` before @@ -907,8 +907,8 @@ returning; a successful sweep invocation is that evidence. **MCP App coverage per level.** Fixtures must cover every compiled tool, prompt, and resource route on the server. App routes are covered at every boundary that -registers app resources — `packed-stdio`, `packed-deleted-source`, -`host-install`, and `dev-epoch` — where `surface-completeness` requires the +registers app resources, `packed-stdio`, `packed-deleted-source`, +`host-install`, and `dev-epoch`, where `surface-completeness` requires the compiled `ui://` URI in `listResources` and `sweep` reads that resource. With the default `apps: 'auto'` an app route needs no fixture entry: `coverage` passes with a reason naming the auto-covered sweep. An explicit @@ -1054,7 +1054,7 @@ Top-level `scripts` is a record of stable output names to an entry path or `{ en A project with routed `src/cli/**` commands also ships that CLI inside the plugin root as `bin/.mjs` (plus `bin/-flight.mjs` when a command renders), a -self-contained module run as `node /bin/.mjs ` — so a script +self-contained module run as `node /bin/.mjs `, so a script route can spawn its `../bin/.mjs` sibling and a Claude skill can point at `${CLAUDE_PLUGIN_ROOT}/bin/.mjs` without a separate npm install. Every built-in host publishes the `cli` capability that admits it; `inspect` accounts for it as a `cli` component, and @@ -1133,7 +1133,7 @@ harness. failure. Their live smoke tests are opt-in and are not part of an ordinary test run. - Codex exposes no authoritative Skill-activation event, so Codex activation evidence is `inferred` and is never reported as `observed`. -- Comparison facets that a run did not record — semantic grader identity, host CLI version, invocation — are +- Comparison facets that a run did not record, semantic grader identity, host CLI version, invocation, are labeled unverified rather than assumed aligned. - Semantic grading requires a native Claude harness and a signed-in Claude Code session; deterministic and Codex selections are refused when it is configured. diff --git a/packages/create-agent-bundle/README.md b/packages/create-agent-bundle/README.md index 27fc56038..53059f25d 100644 --- a/packages/create-agent-bundle/README.md +++ b/packages/create-agent-bundle/README.md @@ -13,7 +13,7 @@ npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@` | Project directory (also the first positional argument). `foo/bar` scaffolds into `foo/bar` and names the package `bar`; `@scope/name` keeps the scoped package name. | | `-t, --template ` | `minimal`, `mcp-server`, or `cli-tool`. | -| `--targets ` | Comma-separated host targets: `amp`, `portable`, `claude`, `codex`, `cursor` — every target `agent-bundle` compiles. Default: `portable,codex,claude`. `amp` is refused with the `mcp-server` template, whose compiler-owned local server Amp's skill-scoped MCP contract rejects. | +| `--targets ` | Comma-separated host targets: `amp`, `portable`, `claude`, `codex`, `cursor`, every target `agent-bundle` compiles. Default: `portable,codex,claude`. `amp` is refused with the `mcp-server` template, whose compiler-owned local server Amp's skill-scoped MCP contract rejects. | | `--package-manager ` | `npm`, `pnpm`, `yarn`, or `bun`. Default: detected from the invoking client. | | `--no-install` | Skip installing dependencies after scaffolding. | | `--framework-version ` | Pin the project's `agent-bundle` dependency to this spec (a version, a tarball path, or a URL). Runtime templates require the compiler version recorded by this scaffolder release. | @@ -36,7 +36,7 @@ scripted and asks nothing — the remaining values fall back to their defaults. | `cli-tool` | An installable routed CLI from one `src/cli/.ts` route module (generated executable, help, argv grammar, validation), a conventional `src/scripts/.ts` artifact script, and a `src/index.ts` library export with declarations, with the framework test harness wired up. | Every template ships a `check` script (validate + build + typecheck + tests) -and validates with zero diagnostics — including the `AB473x` migration +and validates with zero diagnostics, including the `AB473x` migration nudges, because the templates are written against the entry conventions from the start. @@ -48,7 +48,7 @@ ships one projection pool at the `cli-dispatch` (`invokeCli`, `cliJson`) and `script-dispatch` (`runScript`) levels. In both, `npm test` runs every pool the template ships, each as its own labeled run, so a broken route or projection fails the ordinary test command; the focused scripts remain for a -tight loop — `test:unit`, `test:routes`, and `test:projection` in +tight loop, `test:unit`, `test:routes`, and `test:projection` in `mcp-server`, `test:unit` and `test:projection` in `cli-tool`. The `minimal` template compiles no route modules, so it ships no harness pool that would pass without addressing anything; its README documents the wiring to add with the first route. diff --git a/packages/create-agent-bundle/templates/cli-tool/README.md b/packages/create-agent-bundle/templates/cli-tool/README.md index ea8d619b8..d81a06704 100644 --- a/packages/create-agent-bundle/templates/cli-tool/README.md +++ b/packages/create-agent-bundle/templates/cli-tool/README.md @@ -39,44 +39,44 @@ Validate and publish the generated npm root with ## Layout -- `agent-bundle.config.ts` — the one typed config: plugin identity and +- `agent-bundle.config.ts`: the one typed config: plugin identity and targets. Commands, the script, and the library are discovered by convention. -- `src/cli/greet.ts` — the `greet` command: static `config` (including +- `src/cli/greet.ts`: the `greet` command: static `config` (including `inputJsonSchema` and `positionals`), `inputSchema`, `resultSchema`, and an async default function. The file path is the command name; nesting (`src/cli/library/audit.ts`) becomes `library audit`. A `.tsx` command renders through the Agent renderer with Markdown, TTY, `--json`, and `--ndjson` output modes. -- `src/scripts/hello.ts` — a conventional plain script exporting `main(argv)`; +- `src/scripts/hello.ts`: a conventional plain script exporting `main(argv)`; the framework generates the process envelope and `scripts/hello.mjs`. -- `src/index.ts` — the library export (`dist/index.js` + `dist/index.d.ts`). -- `rstest.projection.config.ts` — the framework-generated projection pool. -- `tests/` — see below. +- `src/index.ts`: the library export (`dist/index.js` + `dist/index.d.ts`). +- `rstest.projection.config.ts`: the framework-generated projection pool. +- `tests/`: see below. ## Tests `npm test` tests the plugin: it runs both pools below in turn, so a command that stops dispatching fails the ordinary test command even while the plain module tests stay green. Each pool names the proof level it carries and is -reported as its own run — a pass at one level is never a receipt for another. +reported as its own run, a pass at one level is never a receipt for another. The focused scripts run one pool for a tight loop, and take Rstest's own flags after `--` (`npm run test:projection -- --watch`). | pool | command | files | what a pass proves | | --- | --- | --- | --- | | unit | `npm run test:unit` | `tests/*.test.ts` | ordinary module tests over `src/index.ts`; no framework involved | -| projection (`cli-dispatch`) | `npm run test:projection` | `tests/projection/cli-dispatch.test.ts` | argv resolved and executed through the routed CLI's own shell over the compiled command graph — help, grammar, validation, exit codes — in-process; not the spawned executable | -| projection (`script-dispatch`) | `npm run test:projection` | `tests/projection/script-dispatch.test.ts` | `src/scripts/hello.ts` run through its generated executable's `main` envelope contract with captured stdout/stderr and exit code — as a Node process of its own over the source, not the bundled `scripts/hello.mjs` | +| projection (`cli-dispatch`) | `npm run test:projection` | `tests/projection/cli-dispatch.test.ts` | argv resolved and executed through the routed CLI's own shell over the compiled command graph, help, grammar, validation, exit codes, in-process; not the spawned executable | +| projection (`script-dispatch`) | `npm run test:projection` | `tests/projection/script-dispatch.test.ts` | `src/scripts/hello.ts` run through its generated executable's `main` envelope contract with captured stdout/stderr and exit code, as a Node process of its own over the source, not the bundled `scripts/hello.mjs` | The projection pool is generated by `agentBundleRstest()` from -`agent-bundle/rstest`: one compiler pass — the same route compilation the build -performs, with no artifact built — supplies the command graph, the script +`agent-bundle/rstest`: one compiler pass, the same route compilation the build +performs, with no artifact built, supplies the command graph, the script inventory, and the route loaders. The helpers (`invokeCli`, `cliJson`, `runScript`, `testManifest`, …) come from `agent-bundle/test`, and every one of them stamps its proof level into the result and into any failure message. -Process-level evidence — the executable and the artifact script as real -processes — comes from `npm run build`; the scaffolder's own release matrix +Process-level evidence, the executable and the artifact script as real +processes, comes from `npm run build`; the scaffolder's own release matrix runs both. The scaffold pins matching `agent-bundle` and `@agent-bundle/runtime` builds. diff --git a/packages/create-agent-bundle/templates/mcp-server/README.md b/packages/create-agent-bundle/templates/mcp-server/README.md index eccbc05f9..d4438cd07 100644 --- a/packages/create-agent-bundle/templates/mcp-server/README.md +++ b/packages/create-agent-bundle/templates/mcp-server/README.md @@ -29,12 +29,12 @@ Validate and publish the generated npm root with ## Layout -- `agent-bundle.config.ts` — plugin identity, targets, and project policy. -- `src/mcp/status/tools/report-status.tsx` — the complete MCP tool route. -- `src/scripts/check-status.ts` — an artifact script with a generated process envelope. -- `src/status.ts` — shared domain logic covered by `tests/` and published as +- `agent-bundle.config.ts`: plugin identity, targets, and project policy. +- `src/mcp/status/tools/report-status.tsx`: the complete MCP tool route. +- `src/scripts/check-status.ts`: an artifact script with a generated process envelope. +- `src/status.ts`: shared domain logic covered by `tests/` and published as the package library export. -- `rstest.route-unit.config.ts`, `rstest.projection.config.ts` — the two +- `rstest.route-unit.config.ts`, `rstest.projection.config.ts`: the two framework-generated test pools (see below). ## Tests @@ -42,25 +42,25 @@ Validate and publish the generated npm root with `npm test` tests the plugin: it runs the three pools below in turn, so a route that stops rendering or a server that stops registering it fails the ordinary test command even while the plain module tests stay green. Each pool names the -proof level it carries and is reported as its own run — a pass at one level is +proof level it carries and is reported as its own run, a pass at one level is never a receipt for another. The focused scripts run one pool for a tight loop, and take Rstest's own flags after `--` (`npm run test:routes -- --watch`). | pool | command | files | what a pass proves | | --- | --- | --- | --- | | unit | `npm run test:unit` | `tests/*.test.ts` | ordinary module tests over `src/status.ts`; no framework involved | -| route-unit | `npm run test:routes` | `tests/route-unit/**` | the route module renders to the Agent Document it claims, through the real renderer — no artifact, no transport | -| projection | `npm run test:projection` | `tests/projection/**` | the real generated MCP server registers the route and projects its document to protocol content, over the SDK's in-memory transport — not a process, not the packed artifact | +| route-unit | `npm run test:routes` | `tests/route-unit/**` | the route module renders to the Agent Document it claims, through the real renderer, no artifact, no transport | +| projection | `npm run test:projection` | `tests/projection/**` | the real generated MCP server registers the route and projects its document to protocol content, over the SDK's in-memory transport, not a process, not the packed artifact | The two framework pools are generated by `agentBundleRstest()` from -`agent-bundle/rstest`: one compiler pass — the same route compilation the build -performs, with no artifact built — supplies the route manifest, the route +`agent-bundle/rstest`: one compiler pass, the same route compilation the build +performs, with no artifact built, supplies the route manifest, the route loaders, React's `react-server` conditions, and the JSX transform. The helpers (`renderRoute`, `expectDocument`, `invokeMcpTool`, `listMcpSurface`, …) come from `agent-bundle/test`, and every one of them stamps its proof level into the result and into any failure message. -Process-level evidence — a spawned stdio server from a built artifact — is a +Process-level evidence, a spawned stdio server from a built artifact, is a further level the framework offers (`openPackedMcpServer`); this template does not ship it, because it costs a pack and an install per run. diff --git a/packages/create-agent-bundle/templates/minimal/README.md b/packages/create-agent-bundle/templates/minimal/README.md index 2a398c11d..44509d77d 100644 --- a/packages/create-agent-bundle/templates/minimal/README.md +++ b/packages/create-agent-bundle/templates/minimal/README.md @@ -14,16 +14,16 @@ npm run check # validate + build + typecheck + test ## Layout -- `agent-bundle.config.ts` — the one typed config. -- `src/skills/getting-started/` — a Skill: `SKILL.md` frontmatter plus optional +- `agent-bundle.config.ts`: the one typed config. +- `src/skills/getting-started/`: a Skill: `SKILL.md` frontmatter plus optional `references/` and `assets/`. Every `src/skills//SKILL.md` directory is discovered automatically; add a folder and it ships. -- `tests/` — run with `npm test`. +- `tests/`: run with `npm test`. ## Tests -`npm test` runs ordinary module tests — here, one test that keeps the Skill's -frontmatter and host-layout guidance aligned with the template — and +`npm test` runs ordinary module tests, here, one test that keeps the Skill's +frontmatter and host-layout guidance aligned with the template, and `npm run check` runs them after validate, build, and typecheck. A skills-only project compiles no route modules, so the framework's consumer @@ -31,7 +31,7 @@ harness (`agent-bundle/rstest` + `agent-bundle/test`) has nothing to render: its route-unit pool would contain zero routes and pass unconditionally. This template ships no such pool on purpose; a green run has to mean something. -Add one with the first route module — `src/mcp//tools/.tsx` for +Add one with the first route module, `src/mcp//tools/.tsx` for an MCP tool, `src/cli/**` for a routed CLI command: ```ts @@ -63,6 +63,6 @@ pool at the `mcp-in-memory` level, as a working example. agent-bundle is distributed through [pkg.pr.new](https://pkg.pr.new); this project pins one commit-addressed preview tarball. To move to a newer preview, change the `agent-bundle` entry -in `devDependencies` — see +in `devDependencies`, see [Preview packages](https://github.com/ScriptedAlchemy/agent-bundle/blob/main/docs/preview-packages.md) for the URL forms. diff --git a/packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md b/packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md index 3a954dd2c..0ab972af2 100644 --- a/packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md +++ b/packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md @@ -1,6 +1,6 @@ --- name: getting-started -description: Explain this plugin’s current Skills or help add a new Skill under src/skills. +description: Explain this plugin's current Skills or help add a new Skill under src/skills. --- # Getting started diff --git a/packages/rsc-markdown-stream/README.md b/packages/rsc-markdown-stream/README.md index 4d1f50aae..db57afcb1 100644 --- a/packages/rsc-markdown-stream/README.md +++ b/packages/rsc-markdown-stream/README.md @@ -2,7 +2,7 @@ Render React / RSC trees to **Markdown**, never HTML. -This is a custom renderer in the spirit of [rsc-html-stream](https://github.com/devongovett/rsc-html-stream), but for the other half of the job: where you would normally hand your tree to `renderToReadableStream` from `react-dom/server`, hand it to `renderToMarkdownStream` instead and get a stream of GitHub Flavored Markdown. No HTML is ever produced, no RSC payload is injected, nothing hydrates — markdown *is* the output. +This is a custom renderer in the spirit of [rsc-html-stream](https://github.com/devongovett/rsc-html-stream), but for the other half of the job: where you would normally hand your tree to `renderToReadableStream` from `react-dom/server`, hand it to `renderToMarkdownStream` instead and get a stream of GitHub Flavored Markdown. No HTML is ever produced, no RSC payload is injected, nothing hydrates, markdown *is* the output. Zero dependencies. `react` is the only peer (v19+). `react-dom` is not in the dependency graph at all. @@ -15,7 +15,7 @@ let md = await renderToMarkdown(
    ); // "# Hello\n\nSome **bold** text...\n" ``` -Streaming, parallel to the SSR setup you already know — consume an RSC stream and render it to markdown instead of HTML: +Streaming, parallel to the SSR setup you already know, consume an RSC stream and render it to markdown instead of HTML: ```js import {renderToReadableStream} from 'react-server-dom-BUNDLER/server.edge'; @@ -41,31 +41,31 @@ This package is the renderer behind `MarkdownContent` in as JSX, and the runtime lowers the rendered Markdown into an `Agent.Markdown` node inside a real React Flight request compiled under the `react-server` condition. The upstream repository, [ScriptedAlchemy/rsc-markdown-stream](https://github.com/ScriptedAlchemy/rsc-markdown-stream), -keeps a standalone Rsbuild example of the full pipeline — an RSC server writing raw Flight bytes +keeps a standalone Rsbuild example of the full pipeline, an RSC server writing raw Flight bytes to stdout, a consumer decoding them with `react-server-dom-webpack/client` and handing the tree to -`renderToMarkdownStream` — plus a browser demo; neither ships with this package. +`renderToMarkdownStream`, plus a browser demo; neither ships with this package. Markdown blocks stream out progressively as each server component's data resolves: the header -arrives first, a table next, the slower subtrees last — the same progressive behavior you'd get +arrives first, a table next, the slower subtrees last, the same progressive behavior you'd get from streaming HTML SSR, but the output is markdown. ## shadcn/ui to markdown -Component-library trees — Radix primitives, cva variants, lucide icons and all — render to markdown surprisingly well (the upstream example renders a shadcn/ui dashboard this way): +Component-library trees, Radix primitives, cva variants, lucide icons and all, render to markdown surprisingly well (the upstream example renders a shadcn/ui dashboard this way): - shadcn's `Table` components are real `` elements underneath, so they come out as GFM tables. -- Radix `Checkbox` renders a hidden `` for form interop — inside `
  • ` that becomes a GFM task list (`- [x]`). +- Radix `Checkbox` renders a hidden `` for form interop: inside `
  • ` that becomes a GFM task list (`- [x]`). - Radix `AccordionTrigger` lives inside an `

    ` header, so triggers become real markdown headings; collapsed content and inactive `TabsContent` are unmounted by Radix and produce nothing, while the `defaultValue` panel renders. - Radix state/hooks (`useState`, `useId`, context) run on the renderer's built-in dispatcher in their initial, uncontrolled state. Portal-based components (Dialog, Popover, Tooltip) are the ones that won't work. -- Styled containers (Card, Alert, Button) flatten to plain text blocks — map `button` to a custom serializer via `options.components` to keep adjacent button labels from running together. +- Styled containers (Card, Alert, Button) flatten to plain text blocks: map `button` to a custom serializer via `options.components` to keep adjacent button labels from running together. ### How this relates to rsc-html-stream -[rsc-html-stream](https://github.com/devongovett/rsc-html-stream) does not render anything: it is a ~130-line transport that interleaves Flight bytes into an HTML stream as `