diff --git a/.changeset/590-diagnostics-contract-rows.md b/.changeset/590-diagnostics-contract-rows.md new file mode 100644 index 000000000..b8ac6b6f8 --- /dev/null +++ b/.changeset/590-diagnostics-contract-rows.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Document every emitted diagnostic code explicitly in `docs/diagnostics.md` (the `reference/diagnostics` page). Codes that were covered only by a family row now each have a row with severity, trigger message, and — where the diagnostic carries one — recovery, read from the emitting site: `AB3000`–`AB3006`, `AB3008`–`AB3010`, `AB4000`, `AB4002`–`AB4007`, `AB4012`, `AB4100`–`AB4102`, `AB4200`–`AB4212` (including the previously undocumented `AB4204`), `AB4300`–`AB4339`, `AB4400`–`AB4408`, `AB4600`–`AB4602`, `AB4700`–`AB4716`, `AB6000`–`AB6001`, `AB6002`–`AB6003` (reserved, never emitted), `AB6004`–`AB6018` (`AB6005` restates #588), `AB6023`–`AB6025`, `AB6200`–`AB6202`, `AB7000`–`AB7004`, `AB7103`, `AB8000`–`AB8023` (including `AB8003` and `AB8004`), `AB8030`–`AB8034`, `AB8040`–`AB8057`, `AB8060`–`AB8068`, `AB8070`–`AB8083`, `AB8085`–`AB8088`, `AB8090`–`AB8093`, `AB8120`–`AB8123`, `AB9001`–`AB9005`, and `AB9007`–`AB9011`. The `AB4834` row is rewritten to three cells so the table renders. `pnpm docs:site:build` now fails when a code cited in the docs or a code literal in `packages/agent-bundle/src` has no explicit row (`website/scripts/check-diagnostics-coverage.mjs`). (#599) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5a32bd8f4..668d69855 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,10 @@ updates: schedule: interval: weekly - package-ecosystem: github-actions - directory: / + # `/` covers .github/workflows/*; the composite actions under + # .github/actions/* (where pnpm/setup lives) are separate directories. + directories: + - / + - /.github/actions/* schedule: interval: weekly diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e26abad28..a929abb48 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -44,21 +44,26 @@ jobs: name: Docs site (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 25 + # Third-party actions are pinned to full commit SHAs (a moving `vN` tag + # can be repointed at unreviewed code); the trailing comment names the + # release Dependabot keeps in step. steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup-workspace with: node-version: 22.19.0 # Website-only PRs skip every ci.yml job that lints, so lint here too. - run: pnpm lint - # Typecheck the site config and build it. Rspress's own dead-link, - # dead-anchor, dead-image, and language-parity checks fail the build if - # TypeDoc or the generated reference pages are missing, since the - # authored pages link to them. + # Typecheck the site config, run the locale-drift and diagnostics- + # coverage checks (website/scripts), build with the persistent Rspack + # cache off, then walk the emitted HTML for dead links and anchors. + # Rspress's own dead-link, dead-anchor, dead-image, and language-parity + # checks fail the build if TypeDoc or the generated reference pages are + # missing, since the authored pages link to them. - run: pnpm docs:site:build - name: Upload Pages artifact if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: website/doc_build @@ -96,7 +101,7 @@ jobs: echo "Skipped deploy: main moved to ${head}; this run built ${GITHUB_SHA}." >> "$GITHUB_STEP_SUMMARY" fi - if: steps.head.outputs.deploy == 'true' - uses: actions/configure-pages@v6 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - if: steps.head.outputs.deploy == 'true' id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 0908edd23..657733625 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -13,16 +13,16 @@ even when no error diagnostic was reported. | Family | Area | | --- | --- | -| `AB30xx` | Skill documents: Markdown parsing (`AB3000`–`AB3002`: unreadable, missing or malformed frontmatter) and rendered-skill compilation (`AB3003`: module failed to load, `AB3004`: missing/invalid default component or `frontmatter` export, `AB3005`: content outside the supported Markdown element subset). | -| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`/`AB4001`: name/version; `AB4002`–`AB4007`: Skill fields; `AB4008`–`AB4011` and `AB4013`: release identity, see below; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project). | -| `AB41xx` | Normalized model invariants (unknown targets, duplicate IDs and outputs). | -| `AB42xx` | Hook configuration and native hook sources. | -| `AB43xx` | MCP server and MCP App configuration (`AB4340`: a declaration for a route-generated server redeclares `entry`/`command`/`url`; see below). | -| `AB44xx` | Script configuration. | -| `AB4500` | Registered config extensions (strict finite JSON). | -| `AB46xx` | Assets and the generated-runtime floor. | -| `AB470x` | Package build `bin` configuration (`AB4706`: artifact output overlaps `dist`; `AB4707`–`AB4709`: `output.distPath` shape, root escape, reserved namespace). | -| `AB471x` | Package build `lib` configuration (`AB4710`–`AB4715`) and declaration generation (`AB4716`; see below). | +| `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); see below. | +| `AB41xx` | Normalized model invariants (`AB4100`–`AB4102`: unknown targets, duplicate IDs and outputs; 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`; see below). | +| `AB44xx` | Script configuration (`AB4400`–`AB4408`; see below). | +| `AB4500` | Registered config extensions (strict finite JSON; see below). | +| `AB46xx` | Assets and the generated-runtime floor (`AB4600`–`AB4602`; see below). | +| `AB470x` | Package build `bin` configuration (`AB4700`–`AB4705`; `AB4706`: artifact output overlaps `dist`; `AB4707`–`AB4709`: `output.distPath` shape, root escape, reserved namespace); see below. | +| `AB471x` | Package build `lib` configuration (`AB4710`–`AB4715`) and declaration generation (`AB4716`); see below. | | `AB472x` | The `tools.rsbuild` / `tools.rspack` escape hatch (`AB4720`–`AB4723`: shape; `AB4724`: a framework-owned Rsbuild plugin re-added through `tools.rsbuild.plugins`; see below). | | `AB473x` | Migration nudges (informational; see below). | | `AB474x`/`AB4750` | Prebuilt payloads and prebuilt entries (see below). | @@ -31,23 +31,262 @@ even when no error diagnostic was reported. | `AB477x` | MCP App view compilation (`AB4770`: compile error with file, line, column and the bundler message; `AB4771`: compile warning; `AB4772`: emitted-size advisory; see below). | | `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`), generated route declarations outside the TypeScript program (`AB4834`), route render budgets (`AB4835`), tool task support (`AB4836`), a route module that value-imports a compiler-carrying framework entry (`AB4837`), and provider conventions (see below). | -| `AB5000` | General CLI and adapter failures. | +| `AB5000` | General CLI and adapter failures (see below). | | `AB60xx` | Built-artifact validation, including schema documents and referenced files (`AB6005`: an emitted JavaScript module — a host-pack module or a package build `dist` bundle (`dist/bin/*.js`, the Flight workers, the `lib` entry), prebuilt payloads excepted — has an import that is neither a Node built-in nor a relative or `file:` specifier resolving to a listed regular file inside its tree, or a non-literal dynamic import; 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). | -| `AB700x` | Host installation and uninstallation: bundle identity, host availability, scope, command failure, and collision checks (`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). | +| `AB6200`–`AB6202` | Workbench artifact inspection over published epochs: `AB6200` the epoch does not validate or its provenance is inconsistent, `AB6201` an epoch reference could not be released, `AB6202` unsafe runtime metadata (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; `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 packed file references; `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; `AB7103`: the development package build; see below). | | `AB7300`–`AB7331` | Read-only install Doctor: host probes, installed inventory, bundle comparison and registration proof, runtime endpoint health and identity, durable-state inventory, static bytes-at-rest validation, foreign-install detection (`AB7321`; see below), Cursor plugin hook registration / marketplace staging (`AB7322`–`AB7324`; see below), host load refusal (`AB7325`; see below), the Cursor Agent Plugins launch proof (`AB7326`; see below), a disabled Claude install (`AB7327`; see below), lifecycle receipts and activation states (`AB7328`–`AB7330`; see below), and the operator `.env` layer of an installed pack (`AB7331`; see below). `AB7311` and `AB7325` are also emitted by `build` and `validate --artifact` from the Claude load check (see "Claude Code host validation"). | | `AB8200`–`AB8209` | Workbench development runtime routes (`/api/runtime/**`): `AB8200` development runtime provider configuration, load, or lifecycle failure, `AB8201` runtime/session/run not available, `AB8202` invalid route path, `AB8203` invalid request shape, `AB8204` stale runtime generation or MCP session revision (409), `AB8205` runtime request could not be completed, `AB8206` Workbench runtime client failure, `AB8207` Agent Document decoding needs the optional `@agent-bundle/runtime` peer (503), `AB8208` stored Flight could not be decoded as an Agent Document (409), `AB8209` decoded Agent Document over the 16 MiB budget (413) or an invalid document response. | | `AB8210`–`AB8214` | Workbench semantic lifecycle replay routes (`/api/lifecycles`, `/api/lifecycles/replays`): `AB8210` invalid path, `AB8211` malformed replay request or native envelope (400, carries the shared validator message), `AB8212` replay unavailable or could not be completed, `AB8213` stale manifest binding (409; the page repairs it with refresh → explicit re-run), `AB8214` replay over the 16 MiB budget (413). | -| `AB8215`–`AB8218` | Workbench read-only host discovery route. | +| `AB8215`–`AB8218` | Workbench read-only host discovery route (`/api/discovery`): `AB8215` invalid path, `AB8216` query string or non-`GET` method (400/405), `AB8217` report over the 16 MiB response limit (413), `AB8218` discovery not available (503). | | `AB8219`–`AB8223` | Workbench live MCP probe route (user-initiated, read-only initialize + tools/list): `AB8219` invalid path, `AB8220` invalid request/method, `AB8221` probe target not found, `AB8222` response over the 16 MiB budget, `AB8223` probe unavailable. | | `AB8233`–`AB8235` | Workbench browser-side strict decoders rejecting a dev-server response: `AB8233` lifecycle replay, `AB8234` host discovery, `AB8235` MCP probe report. | | `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). | -| `AB8xxx` | Development server configuration. | +| `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`) + +A skill directory (`src/skills//`, or a directory an explicit `skills` +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 — `claude`, `codex`, `cursor`, `portable`, plus the +unified `plugin` target's shared document — 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 +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`. | +| `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. | +| `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`. | +| `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`; shared: `disable-model-invocation`, `paths`), and not the authoring key `targets`; or `targets` is not an object, carries a key other than `claude`, `codex`, `cursor`, or holds an unknown key inside `targets.` (including `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: Codex, Cursor, portable, and the unified `plugin` target's shared portable document. 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 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 Codex and portable: `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`) + +`plugin.name` is the host-native slug every manifest carries; `plugin.logo` +is an optional project-relative image. Each discovered skill is checked +against the pinned Agent Skills frontmatter schema +(`schemas/agent-skills/frontmatter.schema.json`: `name` and `description` +required, closed portable field shapes), its directory, its resources, and +the other skills. `AB4001`, `AB4008`–`AB4011`, and `AB4013` are the release +identity codes (see "Release identity" below). Every row is reported on the +config file (`AB4000`, `AB4012`) or the skill source. + +| Code | Severity | Trigger | Recovery | +| --- | --- | --- | --- | +| `AB4000` | error | `plugin.name` is missing or is not a nonempty string. | Declare `plugin: { name: '' }`. | +| `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. | +| `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. | +| `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`) + +`validateModel` runs over the normalized plugin after source validation and +normalization, on every command that prepares a project. These codes judge +the model rather than the config text, so they also hold for a model handed +to the API directly. Source-level duplicates (`AB4006`, `AB4408`, `AB4802`) +are usually reported first; `AB4101` and `AB4102` are the model-level +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 `claude`, `codex`, `cursor`, `portable`, and `plugin`. Reported with `target`. | Select a registered target name. | +| `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. | + +## Hook configuration and native hook sources (`AB4200`–`AB4212`) + +`hooks.` accepts a handler path, an entry object (`handler`, +`targets`, `tools`, `timeout`, `args`), or an array of those; see +`website/docs/en/guide/authoring/hooks.mdx`. `AB4200`–`AB4205` and +`AB4210`–`AB4212` are source checks reported on the config file; +`AB4203`/`AB4204` are repeated on the normalized model with `target`; +`AB4206`–`AB4209` judge the target-native hook documents an adapter's +`nativeHooks` key names (`claude.nativeHooks`, `codex.nativeHooks`). A hook +without `targets` inherits the selected targets that support hooks, so a host +without a hook surface (`portable`) is skipped there without a diagnostic; +naming it explicitly is `AB4204`. Prebuilt handlers (`{ prebuilt }`) and +`args` are judged by `AB4744`–`AB4746`. + +| Code | Severity | Trigger | Recovery | +| --- | --- | --- | --- | +| `AB4200` | error | A hook entry's `handler` is neither a nonempty path nor a `{ prebuilt }` marker: `Hook requires a nonempty handler path.` | Point `handler` at the handler module. | +| `AB4201` | error | A hook on an event other than `beforeTool` or `afterTool` declares `tools`; only the tool events take selectors. | Remove `tools`, or move the hook to a tool event. | +| `AB4202` | error | A `tools` selector is neither a canonical selector (`shell`, `file.read`, `file.write`, `mcp`, `agent`) nor a `:` host selector. | Use a canonical selector or a `:` spelling. | +| `AB4203` | error | A hook `targets` entry is not a nonempty string, or names a target that is not registered. Also reported on the normalized model. | Name registered targets only. | +| `AB4204` | error | A hook explicitly names a registered target whose adapter does not support hooks (`portable`): `Target "" cannot emit hook .` Also reported on the normalized model. | Drop that target from the hook's `targets`, or omit `targets` so the hook inherits only the selected hosts that support hooks. | +| `AB4205` | error | `timeout` is not a positive whole number of seconds. | Declare an integer number of seconds, or omit it for the host default. | +| `AB4206` | error | A native hook document is attributed to a target that is not registered (a model-level check on `nativeHooks`). | Keep native hook documents under a registered host key. | +| `AB4207` | error | A native hook document is attributed to a target whose adapter cannot emit hooks. | Remove the document from that host key. | +| `AB4208` | error | `.nativeHooks` (`claude.nativeHooks`, `codex.nativeHooks`) is declared as something other than a nonempty string path: `Native hook source for target "" must return a string or undefined.` | Set it to the path of the host's `hooks.json`, or omit it. | +| `AB4209` | error | Reading `.nativeHooks` from the config threw (an accessor that throws), so the adapter could not resolve the native hook source. | Declare the path as a plain string. | +| `AB4210` | error | A `:` selector names a target that is not registered. | Spell a registered target. | +| `AB4211` | error | A `:` selector names a target whose adapter cannot emit hooks. | Select a host with a hook surface. | +| `AB4212` | error | A `:` selector names a target outside the hook's selected targets (its `targets`, or the project's selected targets when omitted). | Add the target to the hook's `targets`, or drop the selector. | + +## MCP server and MCP App configuration (`AB4300`–`AB4339`) + +`mcp.servers.` declares one of `entry` (a local module compiled into a +stdio server; the conventional `src/mcp/.ts` supplies it when all three +are absent), `command` (an external stdio command), or `url` (a remote +`streamable-http` server); `apps` declares MCP App views for a server with a +local entry. `AB4300`–`AB4319`, `AB4322`–`AB4335`, and `AB4338` are source +checks reported on the config file and name the server or App; +`AB4320`, `AB4321`, `AB4336`, `AB4337`, and `AB4339` are judged on the +normalized model. A server the route graph compiles in `generated` mode is +judged by `AB4340` (see below) plus the shared local-entry field rules +(`AB4305`, `AB4308`–`AB4312`) and the App rules. + +| Code | Severity | Trigger | +| --- | --- | --- | +| `AB4300` | error | `mcp` is not an object. | +| `AB4301` | error | `mcp.servers` is not an object. | +| `AB4302` | error | A server key is empty or whitespace. | +| `AB4303` | error | A server value is not an object. | +| `AB4304` | error | A server declares none, or more than one, of `entry`, `command`, and `url`, and no conventional `src/mcp/.ts` stdio entry exists to fill in. Declare exactly one, or add the conventional module. | +| `AB4305` | error | `targets` is not an array of nonempty strings. | +| `AB4306` | error | `entry` is declared but is not a nonempty path. | +| `AB4307` | error | `entry` does not name an existing file (resolved from the project root). | +| `AB4308` | error | A local-entry server (explicit, conventional, or route-generated) declares a `transport` other than `stdio`. | +| `AB4309` | error | A local-entry server declares `cwd`; the compiled server runs from the plugin root. | +| `AB4310` | error | A stdio server (`entry` or `command`) declares `headers`, which belong to remote servers. | +| `AB4311` | error | `args` is not an array of nonempty strings. | +| `AB4312` | error | `env` is not an object mapping nonempty keys to string values. | +| `AB4313` | error | `command` is declared but empty. | +| `AB4314` | error | A `command` server declares a `transport` other than `stdio`. | +| `AB4315` | error | A `command` server declares a `cwd` that is not a nonempty path. | +| `AB4316` | error | `url` is not a valid `http:` or `https:` URL. | +| `AB4317` | error | A `url` server does not declare `transport: 'streamable-http'`, the only remote transport. | +| `AB4318` | error | A `url` server declares `args`, `env`, or `cwd`, which are stdio options. | +| `AB4319` | error | `headers` is not an object mapping nonempty keys to string values. | +| `AB4320` | error | A normalized server selects a target that is not registered (reported with `target`). | +| `AB4321` | error | A normalized local-entry server's first `args` entry is not the compiler's content-hashed output alias `mcp/mcp--<8 hex>.mjs`: `MCP server "" has an unsafe local output alias.` The normalizer always writes that alias, so this guards a model built or altered outside it. | +| `AB4322` | error | A `command` or `url` server declares `apps`; Apps need a local entry (explicit `entry`, the conventional module, or route-generated) to host them. | +| `AB4323` | error | `apps` is not an object. | +| `AB4324` | error | An App name is not lowercase kebab-case starting with a letter (`^[a-z][a-z0-9-]*$`). | +| `AB4325` | error | One App name is declared by more than one server with different declarations, or by a config App and a `src/mcp//apps/` route module; servers may share a name only when `entry`, `resourceUri`, `template`, and `_meta` are identical. | +| `AB4326` | error | An App value is not an object. | +| `AB4327` | error | An App's `entry` is not a nonempty path. | +| `AB4328` | error | An App's `entry` does not name an existing file. | +| `AB4329` | error | An App's `resourceUri` is not a `ui://` URI with a nonempty host. | +| `AB4330` | error | One `resourceUri` is declared under two different App names (config Apps and route-declared Apps together). | +| `AB4331` | error | `template` is declared but is not a nonempty path. | +| `AB4332` | error | `template` does not end in `.html` or `.htm`, or does not name an existing file (resolved from the project root; a route App's template resolves from the route module instead, `AB4827`). | +| `AB4333` | error | An App's `targets` is not an array of nonempty strings. | +| `AB4334` | error | An App's `targets` names a target outside its server's declared `targets`. | +| `AB4335` | error | `_meta` is declared but is not an object. | +| `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. | + +## Script configuration (`AB4400`–`AB4408`) + +`scripts.` is an entry path or `{ entry, targets }`. A bundled entry +(`.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, `.cts`) compiles to +`scripts/.mjs`; `.sh`, `.bash`, and `.py` entries are copied and keep +their extension. Conventional `src/scripts/` routes are judged by +`AB4808`/`AB4809` and `AB4737`/`AB4738` instead. Every row is reported on the +config file; `AB4406` is repeated on the normalized model. + +| Code | Severity | Trigger | +| --- | --- | --- | +| `AB4400` | error | `scripts` is not an object. | +| `AB4401` | error | A script name is not a safe stable output name (`^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$`). | +| `AB4402` | error | A script value is neither an entry path nor an object with an entry path, or its `entry` is empty. | +| `AB4403` | error | The entry's extension is not one of `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, `.cts`, `.sh`, `.bash`, `.py`. | +| `AB4404` | error | The entry does not name an existing regular file. | +| `AB4405` | error | The entry resolves outside the project root, lexically or after resolving symlinks. | +| `AB4406` | error | `targets` names a target that is not registered. | +| `AB4407` | error | `targets` is not an array of nonempty strings. | +| `AB4408` | error | Two scripts resolve to the same canonical artifact output (`scripts/.mjs` for bundled entries, `scripts/` otherwise): `Scripts and share canonical output "".` | + +## Registered config extensions (`AB4500`) + +The host-scoped config keys (`claude`, `codex`, `cursor`, `portable`) are +extensions the target adapters register. Normalization deep-clones each +declared extension as strict finite JSON before any adapter reads it; a value +that cannot be cloned aborts normalization, and `validate`, `build`, +`inspect`, and `dev` report the failed preparation under this code on the +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. | + +## Assets and the generated-runtime floor (`AB4600`–`AB4602`) + +`assets` lists project files or globs copied into the artifact's `assets/`; +`runtime.node` raises the minimum Node.js version of the generated +executables above the framework default (`22.12.0`). The two validators share +three codes; the message tells them apart. + +| Code | Severity | Trigger | +| --- | --- | --- | +| `AB4600` | error | `assets` is not an array of nonempty paths or globs; or `runtime` is not a plain object whose only key is `node`. | +| `AB4601` | error | An asset entry resolves outside the project root; or `runtime.node` is not a `major.minor[.patch]` version string such as `"22.16"` or `"24.0.0"`. | +| `AB4602` | error | A literal asset entry (one without the glob characters `*`, `?`, `{`, `[`, `]`, `(`, `)`, `!`) names nothing on disk; or `runtime.node` is lower than the default floor, Node.js `22.12.0`, which `runtime` may raise but never lower. | + +## Package build configuration: `bin`, `output`, and `lib` (`AB4700`–`AB4716`) + +`bin` (`false`, or `{ : '' }` / `{ : { entry } }`) and +`lib` (`false`, an entry path, or `{ entry, dts }`) declare the framework-owned +npm package build under `dist/` (`docs/framework-mode.md`, "Package +entries"); `output.distPath` moves the artifact output. Bundled entries must be +JavaScript or TypeScript modules (`.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, +`.mts`, `.cts`). Every row but `AB4706` is reported on the config file. +`AB4716`, the declaration-emit failure of a `lib` entry with `dts`, has its own +section, "Declaration generation", below. + +| Code | Severity | Trigger | Recovery | +| --- | --- | --- | --- | +| `AB4700` | error | `bin` is neither `false` nor an object of bin entries. | Declare `bin: false` or `bin: { : '' }`. | +| `AB4701` | error | A bin name is not a safe stable output name (`^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$`); it becomes `dist/bin/.js`. | Rename the entry. | +| `AB4702` | error | A bin value is neither an entry path nor `{ entry }`, or its entry is empty. | Point the entry at a module. | +| `AB4703` | error | A bin entry resolves outside the project root. | Move the module inside the project. | +| `AB4704` | error | A bin entry's extension is not a bundled JavaScript or TypeScript extension. | Point the entry at a `.ts`/`.js`-family module. | +| `AB4705` | error | A bin entry does not name an existing regular file. | Create the module or fix the path. | +| `AB4706` | error (build) | The project has package entries, and the artifact output (`output.distPath`, or `--output`) and the package build output (`dist/`) overlap in either direction; `agent-bundle build` refuses before writing anything. Thrown without a `sourcePath`. | Configure a different `output.distPath` or pass a different `--output`. | +| `AB4707` | error | `output` is not an object with an optional `distPath` string, or `distPath` is declared but is not a nonempty string. | Declare `output.distPath` as a non-empty project-root-relative path string, or remove the `output` block. | +| `AB4708` | error | `output.distPath` is not a project-root-contained relative POSIX path: it is absolute, contains a backslash, has a `.`, `..`, or empty segment, or resolves to the project root itself. | Use a project-root-contained relative POSIX path; pass the CLI `--output` flag for per-invocation absolute locations. | +| `AB4709` | error | The first segment of `output.distPath` is a reserved namespace, compared case-insensitively: `.agent-bundle`, `.git`, `node_modules`, or `src`. | Choose a directory outside the framework, VCS, dependency, and source namespaces. | +| `AB4710` | error | `lib` is neither `false`, an entry path, nor an object with an entry path. | Declare `lib: false`, `lib: ''`, or `lib: { entry, dts? }`. | +| `AB4711` | error | The lib entry is empty. | Point `entry` at a module. | +| `AB4712` | error | The lib entry resolves outside the project root. | Move the module inside the project. | +| `AB4713` | error | The lib entry's extension is not a bundled JavaScript or TypeScript extension. | Point the entry at a `.ts`/`.js`-family module. | +| `AB4714` | error | The lib entry does not name an existing regular file. | Create the module or fix the path. | +| `AB4715` | error | `lib.dts` is declared but is not a boolean. | Declare `dts: true` or `dts: false`, or omit it (the default is `true`). | + +## General CLI and adapter failures (`AB5000`) + +`AB5000` is the catch-all for a failure that reached the CLI or the build +without a structured diagnostic of its own. Structured failures never fall +through to it: declaration-emit failures are `AB4716`, MCP App compile +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. | + ## Claude Code host validation (`AB6019`–`AB6022`, `AB7311`, `AB7325`) `agent-bundle validate --artifact ` and `agent-bundle build` run the @@ -267,6 +506,10 @@ its module does not export) are invisible to `tsc --noEmit`, so a green `tsc --declaration --emitDeclarationOnly` over the lib entry source 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. | + ## MCP App view compilation (`AB4770`–`AB4772`) MCP App views compile through Rsbuild with its logging silenced @@ -902,7 +1145,7 @@ schema constants), unions, nested objects, transforms, coercions — raises | `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` | warning | `agent-bundle validate` published `.agent-bundle/routes.d.ts` (the project compiles routes or providers) but the root `tsconfig.json` program — resolved like `tsc -p`, including `extends` and one level of project `references` — does not compile it, so `renderRoute` / `renderRouteEvents` type-check route ids as `string` and `input` / `result` as `unknown`. Reported on `tsconfig.json`; never for a project without one. | Add `".agent-bundle/routes.d.ts"` to `tsconfig.json` `include` (not `files`: an `include` entry is inert until the first build publishes the file, while a missing `files` entry is a `tsc` error); `build`, `dev`, and `validate` keep the file current and it stays gitignored. | +| `AB4834` | warning | `agent-bundle validate` published `.agent-bundle/routes.d.ts` (the project compiles routes or providers) but the root `tsconfig.json` program — resolved like `tsc -p`, including `extends` and one level of project `references` — does not compile it, so `renderRoute` / `renderRouteEvents` type-check route ids as `string` and `input` / `result` as `unknown`. Reported on `tsconfig.json`; never for a project without one. Add `".agent-bundle/routes.d.ts"` to `tsconfig.json` `include` (not `files`: an `include` entry is inert until the first build publishes the file, while a missing `files` entry is a `tsc` error); `build`, `dev`, and `validate` keep the file current and it stays gitignored. | | `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 route module of any kind except an App — a `src/cli/**` command, a `src/scripts/**` script, a tool, resource, or prompt route of a generated server, an event route — a layout, or a provider, or a module one of them reaches through relative value imports, imports `agent-bundle`, `agent-bundle/api`, `agent-bundle/config`, `agent-bundle/eval`, `agent-bundle/rstest`, `agent-bundle/test`, or `agent-bundle/test/browser` as a value (a static import whose binding is read at run time, `import 'agent-bundle/api'`, `import('agent-bundle/api')` with a literal specifier, or a non-type re-export). Those entries carry the compiler, and the generated executable is self-contained (#387): the bundler would inline the compiler and fail on the framework's runtime-relative module references (`Module not found: Can't resolve '../events'`), or the artifact validator would reject the inlined compiler's non-literal dynamic imports with `AB6005` — either way naming a generated file instead of the route (#558). Judged statically when the route graph compiles, so `inspect`, `validate`, `build`, and `dev` all report it, once per module, naming the route and the helper the import lives in. `import type`, `type`-qualified specifiers, and imports used only in type positions are elided by the bundler and never reported; routes of a server that is not generated (`custom`/`command`/`remote`, or an `AB4800` conflict) or of a CLI that is not generated (`conventional`, or an `AB4801` conflict) are never bundled, so they are not judged; likewise a layout that no bundled rendered route composes through (a worker imports only the layouts its routes reach: the tool, resource, and prompt routes of a generated server, the rendered `.tsx` commands of a generated CLI, and rendered `.tsx` scripts), and a provider in a project whose only executables are plain `.ts` scripts, which are bundled from their own source and mount none. Spawn the framework instead of importing it: serve an MCP App from a routed command with `spawnServeApp` from `agent-bundle/serve-app-command`, which runs `agent-bundle serve-app` as a child process; keep other framework calls in host processes (`package.json` scripts, a hand-written `.mjs` run from the checkout). The bundle-safe entries stay allowed: `agent-bundle/routes`, `agent-bundle/launch-env`, `agent-bundle/meta`, `agent-bundle/mcp-apps`, `agent-bundle/mcp-entry`, `agent-bundle/cli-entry`, `agent-bundle/terminal-capability`, and `agent-bundle/serve-app-command`. | @@ -960,7 +1203,7 @@ install receipt, `.agent-bundle-install.json`, beside the plugin manifest: ```json { - "contentHash": "", + "contentHash": "", "directories": [".cursor-plugin", "..."], "files": [".cursor-plugin/plugin.json", "INSTALL.md", "install.mjs", "..."], "format": "agent-bundle-install-receipt/2", @@ -1283,6 +1526,10 @@ that already committed; it surfaces as one `AB7103` **warning** on the succeeded build attempt, and the package build retries on the next invalidation. See `docs/entry-conventions.md` for the dev-watch contract. +| 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. | + ## Read-only Doctor Cursor hook registration and marketplace staging (`AB7322`–`AB7324`) Cursor delivers a plugin's hooks from its `.cursor-plugin/plugin.json` `hooks` @@ -1380,3 +1627,242 @@ 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. | + +## Built-artifact validation (`AB6000`–`AB6018`, `AB6023`–`AB6025`) + +`agent-bundle build` validates the staged tree before it writes the manifest +(`validateArtifactFiles`: filesystem entries, generated JSON documents, and +generated JavaScript modules), validates the finished artifact against its +manifest, and re-checks the validated snapshot after the staging tree is +renamed into place. `agent-bundle validate --artifact ` runs the same +validator over a built directory, `agent-bundle dev` runs it over every +rebuild before publishing the epoch, and the Workbench artifact inspection +runs it over each epoch it reads (`AB6200`). Every code here is an **error** +whose `recovery` is fixed per code in the artifact diagnostic registry +(`artifactDiagnosticRecoveries`); `generatedPath` names the offending artifact +file (`agent-bundle.manifest.json` for manifest-level findings) and `target` +names the host target namespace when the check is per target. + +| 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. `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 | `Generated JavaScript import from "" .` — an emitted JavaScript module — a host-pack module or a package build `dist` bundle (`dist/bin/*.js`, the Flight workers, the `lib` entry), prebuilt payloads excepted — has an import that is neither a Node built-in nor a relative or `file:` specifier resolving to a listed regular file inside its tree (`uses unsupported specifier`, `is missing`, `resolves outside the artifact root`, `is not listed in the artifact manifest`, `does not resolve to a regular file`, `references invalid JSON`, `uses unsupported target`), or the module cannot be read, has invalid syntax, or has a non-literal dynamic import; a `dist` finding names `dist/`. The walk covers `import` specifiers only — `require`, `createRequire`, and `import.meta.resolve` calls are the prepack inventory's business (`AB7014`). | 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 for target "" does not match its registered contract.` — the manifest's per-target metadata (contract version, kind, artifact layout) 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; or a compiler-emitted MCP entry is referenced by no server or by more than one. | Rebuild the artifact so every target MCP manifest references its exact compiler outputs. | +| `AB6018` | error | Hook coherence: `agent-bundle.hooks.json` is not strict canonical hook index data; an entry selects an undeclared or hook-incompatible target, or references a missing or invalid target wrapper; a target in the index is missing its native hook manifest, or that manifest is invalid for command enumeration; an indexed hook does not map to exactly one native command, or a native command is not indexed (or indexed more than once). | Rebuild the artifact so native hook commands and hook metadata agree. | +| `AB6023` | error | `Target "" is missing required install surface "INSTALL.md".` — a built-in target (`claude`, `codex`, `cursor`, `plugin`, `portable`) has no `INSTALL.md` in its namespace. | Rebuild the artifact so every built-in target includes its generated `INSTALL.md`. | +| `AB6024` | error | `Target "" is missing required install surface "install.mjs".` — a `cursor`, `plugin`, or `portable` target has no `install.mjs` in its namespace (`claude` and `codex` do not require one). | Rebuild the Cursor-compatible artifact so it includes 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. | + +## Workbench artifact inspection (`AB6200`–`AB6202`) + +The Workbench artifact pages (`/api/artifacts/epochs/`, +`/api/artifacts/diff`) read published epochs through the artifact inspection +service. Each refusal is an `ArtifactInspectionServiceError` whose +`diagnostics` carry one of these **error** codes; the route reports the +service failure as `AB8064`, `AB8065`, or `AB8066` and forwards the +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 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`) + +The `AB7000`–`AB7004` codes are shared by two families of emitters with +distinct meanings. `agent-bundle install` and `agent-bundle uninstall` +(`install/install.ts`, `install/uninstall.ts`, `install/cursor-marketplace.ts`) +throw them as `DiagnosticError`s with `target` set to the host; the +`install-entry` CLI wraps any non-diagnostic failure as `AB7004`. The +development project service (`dev/project-service.ts`) and `inspectProject` +emit them as **error** diagnostics on a failed preparation with `sourcePath` +set to the config file and a fixed `recovery` ending in "then inspect again". +The install-time collision (`AB7005`) and load-refusal (`AB7006`) codes, and +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 `claude`, `codex`, or `cursor`. Project preparation: `Unable to load project source.` — evaluating the configuration module or discovering source threw before validation. | Install: pass `--host claude`, `codex`, or `cursor`. Preparation: fix the Agent Bundle configuration and source files, then inspect again. | +| `AB7001` | error | Install/uninstall: the bundle identity is unreadable — no host manifest in the `--from` directory, its `` directory, or its `plugin` directory; a manifest that is not a JSON object or lacks a nonempty required key; a Cursor plugin name that is not a safe local name; a Claude or Codex bundle with no marketplace identity. 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 `Unable to compose the bundler inspection.` | Install: point `--from` at a built bundle whose host manifest carries the fields the message names. Preparation: fix normalized project configuration and source references, then inspect again. | +| `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; `--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. | Install: use `--scope user`, 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 and remain inside the project root, 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 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; any non-diagnostic error thrown by a Cursor installer or reaching the `install-entry` CLI. `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). Inspection: choose a target selected by the project configuration, then inspect again. | + +## Development server (`AB80xx`) + +`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" } }` 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 +module emits with it. The Workbench browser clients reuse the owning route's +"could not be completed" code (`AB8019`, `AB8033`, `AB8043`, `AB8063`, +`AB8073`, `AB8083`, `AB8093`, `AB8123`) for a response they cannot decode or an +HTTP failure that carried no diagnostic body, `AB8015`–`AB8017` for a request +the browser refuses to send, and `AB8003` when the page origin is not one the +foreground server accepts. + +### Server configuration and shared transport + +| 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. | +| `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. | + +### 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. | + +### 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` | 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. | Read the dev-server log; shrink or split the App operation result if the bound was hit. | + +### 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. | + +### 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. | + +### 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. | + +### 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. | +| `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. | +| `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. | + +### 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`. | + +### 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. | + +## Eval refusals through the API and CLI (`AB9001`–`AB9005`, `AB9007`–`AB9011`) + +`runEvals` and `compareEvals` (and `agent-bundle eval` / `eval compare` on top +of them) map every `EvalServiceError` to one **error** diagnostic whose +`message` is the service's own and whose `recovery` is fixed per code, thrown +as a `DiagnosticError`. The dev-server eval routes report the same service +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. | diff --git a/package.json b/package.json index 0487e274e..4ca1182e7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test:watch": "rstest --config rstest.config.ts --watch", "lint": "rslint .", "bench:hook-cold-start": "node scripts/measure-hook-cold-start.mjs", - "typecheck": "node scripts/check-dist-fresh.mjs && tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json && tsc --project packages/rsc-markdown-stream/tsconfig.json", + "typecheck": "node scripts/check-dist-fresh.mjs && tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json && tsc --project packages/rsc-markdown-stream/tsconfig.json && pnpm --filter @agent-bundle/docs typecheck", "check": "pnpm build && pnpm test:unit && pnpm test:route-unit && pnpm test:projection && pnpm test:integration:run && pnpm lint && pnpm typecheck", "check:local-ci": "node scripts/local-ci.mjs", "check:host-cli": "node scripts/host-cli-pins.mjs verify", diff --git a/patches/@rspress__core@2.0.21.patch b/patches/@rspress__core@2.0.21.patch new file mode 100644 index 000000000..7219a9eac --- /dev/null +++ b/patches/@rspress__core@2.0.21.patch @@ -0,0 +1,20 @@ +diff --git a/dist/theme/components/Search/logic/providers/LocalProvider.js b/dist/theme/components/Search/logic/providers/LocalProvider.js +index 1f49672061c56144b8299aef61d6b25029b143c4..3bb16cddef4176194a939202c5a1ec6a581dd82a 100644 +--- a/dist/theme/components/Search/logic/providers/LocalProvider.js ++++ b/dist/theme/components/Search/logic/providers/LocalProvider.js +@@ -78,12 +78,12 @@ class LocalProvider { + finalize: (str)=>str.flatMap((s)=>tokenize(s, cyrillicRegex)) + } + }); ++ const pending = []; + for (const item of pagesForSearch){ + const flexSearchItem = item; +- this.#index.addAsync(item.routePath, flexSearchItem); +- this.#cjkIndex.addAsync(item.routePath, flexSearchItem); +- this.#cyrillicIndex.addAsync(item.routePath, flexSearchItem); ++ pending.push(this.#index.addAsync(item.routePath, flexSearchItem), this.#cjkIndex.addAsync(item.routePath, flexSearchItem), this.#cyrillicIndex.addAsync(item.routePath, flexSearchItem)); + } ++ await Promise.all(pending); + } + async search(query) { + const { keyword, limit } = query; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7158a67d..73fa25c8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ overrides: '@agent-bundle/runtime': workspace:* rsc-markdown-stream: workspace:* +patchedDependencies: + '@rspress/core@2.0.21': 555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c + importers: .: @@ -486,19 +489,19 @@ importers: version: 3.1.1(@types/react@19.2.18)(react@19.2.8) '@rspress/core': specifier: 2.0.21 - version: 2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) + version: 2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) '@rspress/plugin-llms': specifier: 2.0.21 - version: 2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(supports-color@7.2.0) + version: 2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(supports-color@7.2.0) '@rspress/plugin-sitemap': specifier: 2.0.21 - version: 2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)) + version: 2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)) '@rspress/plugin-twoslash': specifier: 2.0.21 - version: 2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react@19.2.8)(supports-color@7.2.0)(typescript@6.0.3) + version: 2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react@19.2.8)(supports-color@7.2.0)(typescript@6.0.3) '@rspress/plugin-typedoc': specifier: 2.0.21 - version: 2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(typescript@6.0.3) + version: 2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(typescript@6.0.3) '@shikijs/transformers': specifier: 4.4.3 version: 4.4.3 @@ -511,12 +514,6 @@ importers: '@types/react': specifier: 19.2.18 version: 19.2.18 - '@types/react-dom': - specifier: 19.2.5 - version: 19.2.5(@types/react@19.2.18) - agent-bundle: - specifier: workspace:* - version: link:../packages/agent-bundle react: specifier: 19.2.8 version: 19.2.8 @@ -3961,7 +3958,7 @@ snapshots: optionalDependencies: '@rspack/core': 2.2.2(@swc/helpers@0.5.23) - '@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)': + '@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@mdx-js/mdx': 3.1.1(supports-color@7.2.0) '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) @@ -4009,9 +4006,9 @@ snapshots: - micromark-util-types - supports-color - '@rspress/plugin-llms@2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(supports-color@7.2.0)': + '@rspress/plugin-llms@2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: - '@rspress/core': 2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) + '@rspress/core': 2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) remark-mdx: 3.1.1(supports-color@7.2.0) remark-parse: 11.0.0(supports-color@7.2.0) remark-stringify: 11.0.0 @@ -4020,14 +4017,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/plugin-sitemap@2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))': + '@rspress/plugin-sitemap@2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))': dependencies: - '@rspress/core': 2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) + '@rspress/core': 2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) - '@rspress/plugin-twoslash@2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react@19.2.8)(supports-color@7.2.0)(typescript@6.0.3)': + '@rspress/plugin-twoslash@2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(react@19.2.8)(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@floating-ui/dom': 1.8.0 - '@rspress/core': 2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) + '@rspress/core': 2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) '@shikijs/twoslash': 4.4.3(supports-color@7.2.0)(typescript@6.0.3) mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-gfm: 3.1.0(supports-color@7.2.0) @@ -4039,9 +4036,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/plugin-typedoc@2.0.21(@rspress/core@2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(typescript@6.0.3)': + '@rspress/plugin-typedoc@2.0.21(@rspress/core@2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))(typescript@6.0.3)': dependencies: - '@rspress/core': 2.0.21(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) + '@rspress/core': 2.0.21(patch_hash=555fe9cee718693c8c981e2ae0e282cb514a1808af1744dfab977ed7ca90fa2c)(@rspack/core@2.2.2(@swc/helpers@0.5.23))(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0) typedoc: 0.28.20(typescript@6.0.3) typedoc-plugin-markdown: 4.12.0(typedoc@0.28.20(typescript@6.0.3)) transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 38bfbd41a..69163b837 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,3 +26,8 @@ minimumReleaseAgeExclude: - '@effect/tsgo-win32-arm64@0.39.0' - '@effect/tsgo-win32-x64@0.39.0' - '@effect/tsgo@0.39.0' +# LocalProvider.init() awaits FlexSearch addAsync so the first search query +# cannot race the index build (#590; upstream web-infra-dev/rspress#3658). +# Drop the patch once a release awaits the adds. +patchedDependencies: + '@rspress/core@2.0.21': patches/@rspress__core@2.0.21.patch diff --git a/website/docs/en/_nav.json b/website/docs/en/_nav.json index 068035516..c9434da7f 100644 --- a/website/docs/en/_nav.json +++ b/website/docs/en/_nav.json @@ -7,7 +7,7 @@ { "text": "Reference", "link": "/reference/", - "activeMatch": "/reference/" + "activeMatch": "/reference/(?!api$)" }, { "text": "Examples", @@ -21,7 +21,7 @@ }, { "text": "Type API", - "link": "/api/", - "activeMatch": "/api/" + "link": "/reference/api", + "activeMatch": "/api/|/reference/api" } ] diff --git a/website/docs/en/api/_meta.json b/website/docs/en/api/_meta.json index 3bb657871..7e39760cd 100644 --- a/website/docs/en/api/_meta.json +++ b/website/docs/en/api/_meta.json @@ -3,31 +3,37 @@ { "type": "dir", "name": "modules", - "label": "Modules" + "label": "Modules", + "collapsed": true }, { "type": "dir", "name": "functions", - "label": "Functions" + "label": "Functions", + "collapsed": true }, { "type": "dir", "name": "classes", - "label": "Classes" + "label": "Classes", + "collapsed": true }, { "type": "dir", "name": "interfaces", - "label": "Interfaces" + "label": "Interfaces", + "collapsed": true }, { "type": "dir", "name": "types", - "label": "Type aliases" + "label": "Type aliases", + "collapsed": true }, { "type": "dir", "name": "variables", - "label": "Variables" + "label": "Variables", + "collapsed": true } ] diff --git a/website/docs/en/examples/audiobook-curator.mdx b/website/docs/en/examples/audiobook-curator.mdx index 852e29c72..ea1809c9b 100644 --- a/website/docs/en/examples/audiobook-curator.mdx +++ b/website/docs/en/examples/audiobook-curator.mdx @@ -1,5 +1,5 @@ --- -description: 'The Audiobook Curator example: a complete agent-bundle application built from route modules, request-context providers, durable state, MCP-to-CLI projection, and the framework-owned package build.' +description: 'Audiobook Curator: a complete agent-bundle app from route modules, request-context providers, durable state, MCP-to-CLI projection, and the package build.' --- # Audiobook Curator diff --git a/website/docs/en/examples/hooks-and-scripts.mdx b/website/docs/en/examples/hooks-and-scripts.mdx index fca406688..2f0de850d 100644 --- a/website/docs/en/examples/hooks-and-scripts.mdx +++ b/website/docs/en/examples/hooks-and-scripts.mdx @@ -1,5 +1,5 @@ --- -description: 'The Hooks and Scripts example: a session-start hook, two emitted scripts including a blocking exit code, durable Playground traces, and a reversible diagnostic walkthrough.' +description: 'The Hooks and Scripts example: a session-start hook, two scripts (one blocking exit code), durable Playground traces, and a reversible diagnostic walkthrough.' --- # Hooks and Scripts diff --git a/website/docs/en/examples/mcp-app.mdx b/website/docs/en/examples/mcp-app.mdx index 1cb5db9f8..7b098c43b 100644 --- a/website/docs/en/examples/mcp-app.mdx +++ b/website/docs/en/examples/mcp-app.mdx @@ -1,5 +1,5 @@ --- -description: 'The MCP App example: one service-readiness workflow expressed as a generated stdio MCP server, a typed tool, an interactive MCP App resource, a Skill, a hook, a script, and a deterministic eval.' +description: 'The MCP App example: a service-readiness workflow as a stdio MCP server, a typed tool, an MCP App resource, a Skill, a hook, a script, and a deterministic eval.' --- # MCP App diff --git a/website/docs/en/guide/authoring/hooks.mdx b/website/docs/en/guide/authoring/hooks.mdx index 6132ab1b0..5ba1f6f3d 100644 --- a/website/docs/en/guide/authoring/hooks.mdx +++ b/website/docs/en/guide/authoring/hooks.mdx @@ -98,7 +98,8 @@ canonical event, so a `satisfies` clause is the whole contract: import type { HookHandler } from 'agent-bundle'; export default ((event) => ({ - additionalContext: `Run verify-release from ${event.cwd ?? process.cwd()} before publishing.`, + additionalContext: + `Run verify-release from ${event.cwd ?? process.cwd()} before publishing.`, outcome: 'continue', })) satisfies HookHandler<'sessionStart'>; ``` @@ -129,21 +130,25 @@ are accepted, and each one is checked: | Key | Contract | | --- | --- | -| `outcome` | `continue`, `deny`, or `stop`. | -| `reason` | A non-empty string, valid only when denying a `beforeTool`, `stop`, or `agentStop` hook. Denying one of those without a reason fails. | +| `outcome` | `continue` or `deny`. `'stop'` is not a legal result on any event: `HookHandler` excludes it and the generated wrapper rejects it at run time. | +| `reason` | A non-empty string, valid only when denying a `beforeTool`, `stop`, or `agentStop` hook (Cursor's wrapper also takes one on the `agentStart` denial it alone admits). Denying one of those without a reason fails. | | `additionalContext` | A string appended to the agent's context. | | `updatedInput` | Replacement input for the pending call. | Per-event restrictions are enforced, not documented-and-hoped: -- `sessionStart`, `afterTool`, and `agentStart` cannot deny, stop, or replace input. +- `sessionStart`, `afterTool`, and `agentStart` cannot deny, stop, or replace input. The typed + contract rejects all three, and so do the Claude and Codex wrappers; Cursor's wrapper alone + tolerates a denying `agentStart` with a reason, projected as `permission: 'deny'`. - `beforeTool` cannot stop, and cannot replace input while denying. A continuing `beforeTool` handler makes **no** permission decision: nothing is written to the host's decision field, so its normal permission prompt still applies, and a `updatedInput` rewrite is evaluated by that flow against the rewritten input. Only `deny` is projected as a decision. To approve a call explicitly, use an [event route](#event-routes) and return `outcome: 'allow'`. - `stop` accepts only `continue`, or `deny` with a reason. -- `agentStop` cannot stop the parent flow or replace input, and on Codex it cannot add context. +- `agentStop` cannot stop the parent flow or replace input, and only Claude Code carries + `additionalContext` from it — Codex and Cursor reject it, and the typed contract excludes it + everywhere. An unknown key, a wrong type, or a violation of any rule above fails the hook with a clear message instead of being projected into a host document that would misbehave silently. @@ -195,13 +200,17 @@ export const config = { tools: ['file.write'], // canonical selector -> per-host native matcher } satisfies AgentEventRouteConfig; -export default async function AfterFileEdit({ canonical, native, signal }: AgentEventRouteProps<'tool/after'>) { +export default async function AfterFileEdit( + { canonical, native, signal }: AgentEventRouteProps<'tool/after'>, +) { // canonical.provenance = { host, hostContractRevision, nativeEvent, source: 'native' } // canonical.payload.toolName = { value: 'Write', nativeKey: 'tool_name' } on every host const tool = canonical.payload.toolName?.value ?? 'a tool'; return ( - {`Recorded an edit by ${tool}, reported by ${canonical.provenance.host}.`} + + {`Recorded an edit by ${tool}, reported by ${canonical.provenance.host}.`} + ); } @@ -243,7 +252,9 @@ Route-unit tests build the same props from a host envelope with `createEventRout import { createEventRouteInput, renderRoute } from 'agent-bundle/test'; const rendered = await renderRoute('event:tool/after', { - input: createEventRouteInput('tool/after', claudePostToolUseEnvelope, { host: 'claude' }), + input: createEventRouteInput('tool/after', claudePostToolUseEnvelope, { + host: 'claude', + }), }); ``` @@ -371,7 +382,8 @@ npx agent-bundle inspect --root . --hooks npx agent-bundle hooks list --artifact artifact --target claude npx agent-bundle hooks simulate --artifact artifact --target claude \ --hook session-start-session-start-7ab7e8a5 \ - --input '{"sessionId":"local","transcriptPath":"/tmp/transcript.jsonl","cwd":"/tmp/project","source":"startup"}' + --input '{"sessionId":"local","transcriptPath":"/tmp/transcript.jsonl", + "cwd":"/tmp/project","source":"startup"}' ``` `--hook` must be the exact `name` (or `id`) that `hooks list` printed — normalization derives it diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index 79645850b..1cb6fa823 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -41,7 +41,9 @@ export const config = { export const inputSchema = z.object({ verbose: z.boolean().optional() }).strict(); export const resultSchema = z.object({ status: z.literal('ready') }).strict(); -export default async function Status({ input, signal }: ToolRouteProps) { +export default async function Status( + { input, signal }: ToolRouteProps, +) { if (signal.aborted) throw new DOMException('aborted', 'AbortError'); if (input.verbose) await agent(); const result = { status: 'ready' as const }; @@ -144,10 +146,12 @@ from the same registry and never invented: ```ts const { lineage } = await agent(); if (lineage.state === 'available' && lineage.value.tree !== undefined) { - lineage.value.tree.siblings; // every other live conversation under the same root (any depth, - // the root itself included for a subagent), oldest first + lineage.value.tree.siblings; // every other live conversation under the same root + // (any depth, the root itself included for a subagent), + // oldest first lineage.value.tree.children; // live conversations whose parent is this one - lineage.value.tree.roots; // other live root conversations (Cursor: same workspace_roots) + lineage.value.tree.roots; // other live root conversations + // (Cursor: same workspace_roots) } ``` @@ -171,8 +175,18 @@ output agree. It is information only — never a writer — and it changes nothi ```ts interface AgentTerminal { hostSurface: 'cli' | 'mcp' | 'hook' | 'script' | 'workbench'; - stdout: { kind: 'tty' | 'pipe' | 'none'; color: 'none' | 'basic' | '256' | 'truecolor'; columns?: number; rows?: number }; - stderr: { kind: 'tty' | 'pipe' | 'none'; color: 'none' | 'basic' | '256' | 'truecolor'; columns?: number; rows?: number }; + stdout: { + kind: 'tty' | 'pipe' | 'none'; + color: 'none' | 'basic' | '256' | 'truecolor'; + columns?: number; + rows?: number; + }; + stderr: { + kind: 'tty' | 'pipe' | 'none'; + color: 'none' | 'basic' | '256' | 'truecolor'; + columns?: number; + rows?: number; + }; sharesTarget: boolean; // fd 1 and fd 2 name one open file (`2>&1`, one shared terminal) } ``` @@ -295,9 +309,15 @@ framework's streaming progress surface on MCP, in the rendered CLI, and in the W import React, { Suspense } from 'react'; import { Agent } from '@agent-bundle/runtime'; -export default async function Search({ input, signal }: ToolRouteProps) { +export default async function Search( + { input, signal }: ToolRouteProps, +) { return ( - }> + + } + > ); @@ -651,8 +671,10 @@ value on every request it opens as `(await agent()).plugin`: ```ts const { plugin } = await agent(); if (plugin.state === 'available') { - plugin.value.root; // the install root: AGENT_BUNDLE_PLUGIN_ROOT, or the shell's fallback - plugin.value.stateRoot; // `/state`, where defineState / notices / lineage already live + plugin.value.root; // the install root: AGENT_BUNDLE_PLUGIN_ROOT, + // or the shell's fallback + plugin.value.stateRoot; // `/state`, where defineState / notices / lineage + // already live plugin.source; // 'native' from the variable, 'derived' from the fallback } ``` @@ -818,7 +840,8 @@ tab, from a terminal — `agent-bundle serve-app` hosts one built App standalone ```sh npx agent-bundle build -npx agent-bundle serve-app status/status --artifact artifact --input '{"service":"compiler"}' --open +npx agent-bundle serve-app status/status --artifact artifact \ + --input '{"service":"compiler"}' --open ``` The command launches the plugin's packed MCP server exactly as `mcp run` does (same artifact @@ -859,7 +882,7 @@ host packs stay self-contained. This is what cargo-hauler's `hauler dashboard` d resolution of [#558](https://github.com/ScriptedAlchemy/agent-bundle/issues/558): ```ts -// src/cli/dashboard.ts — `hauler dashboard`: open the App against the plugin's own server. +// src/cli/dashboard.ts — `hauler dashboard`: open the App against the plugin's server. import type { CliRouteConfig, CliRouteProps } from 'agent-bundle'; import { ServeAppCommandError, spawnServeApp } from 'agent-bundle/serve-app-command'; import { z } from 'zod'; @@ -880,7 +903,9 @@ export const resultSchema = z.object({ url: z.string().nullable(), }).strict(); -export default async function dashboard({ input, signal }: CliRouteProps) { +export default async function dashboard( + { input, signal }: CliRouteProps, +) { let served; try { served = await spawnServeApp({ diff --git a/website/docs/en/guide/authoring/package-entries.mdx b/website/docs/en/guide/authoring/package-entries.mdx index 1e0e3f8d8..02649e2f8 100644 --- a/website/docs/en/guide/authoring/package-entries.mdx +++ b/website/docs/en/guide/authoring/package-entries.mdx @@ -60,7 +60,10 @@ function, receives the generated process envelope: // src/cli.ts — the whole CLI entry a consumer writes import type { ExecutableMainContext } from 'agent-bundle'; -export const main = async (argv: readonly string[], { terminal }: ExecutableMainContext): Promise => { +export const main = async ( + argv: readonly string[], + { terminal }: ExecutableMainContext, +): Promise => { if (terminal.stderr.color !== 'none') { /* paint progress on stderr */ } return 0; }; @@ -100,7 +103,9 @@ export const inputSchema = z.object({ export const resultSchema = z.object({ scanned: z.number().int() }).strict(); -export default async function inspect({ input, signal }: CliRouteProps) { +export default async function inspect( + { input, signal }: CliRouteProps, +) { signal.throwIfAborted(); return { scanned: input.maxFiles ?? 0 }; } diff --git a/website/docs/en/guide/development/evaluations.mdx b/website/docs/en/guide/development/evaluations.mdx index 84c960e61..a366631df 100644 --- a/website/docs/en/guide/development/evaluations.mdx +++ b/website/docs/en/guide/development/evaluations.mdx @@ -1,5 +1,5 @@ --- -description: 'Run agent-bundle eval suites: pass/fail/inconclusive semantics, minimum evidence, smoke versus reliability, comparison alignment, and the authenticated native harnesses.' +description: 'Run agent-bundle eval suites: pass/fail/inconclusive semantics, minimum evidence, smoke versus reliability, comparison alignment, and the native harnesses.' --- # Evaluations @@ -70,10 +70,17 @@ interface ReleaseResult { } const grade: EvalGraderFunction = async ({ fixturePath }) => { - const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as ReleaseResult; - return result.verdict === 'ready' && Array.isArray(result.blockers) && result.blockers.length === 0 + const result = JSON.parse( + await readFile(join(fixturePath, 'result.json'), 'utf8'), + ) as ReleaseResult; + return result.verdict === 'ready' + && Array.isArray(result.blockers) + && result.blockers.length === 0 ? { detail: 'The release artifact is ready with no blockers.', outcome: 'pass' } - : { detail: 'The release artifact is not ready or has unresolved blockers.', outcome: 'fail' }; + : { + detail: 'The release artifact is not ready or has unresolved blockers.', + outcome: 'fail', + }; }; export default grade; diff --git a/website/docs/en/guide/development/testing.mdx b/website/docs/en/guide/development/testing.mdx index 86ee423e4..ae3fba1ff 100644 --- a/website/docs/en/guide/development/testing.mdx +++ b/website/docs/en/guide/development/testing.mdx @@ -1,5 +1,5 @@ --- -description: 'Test agent-bundle routes through the framework harness: the rstest configuration helper, renderRoute, the eleven proof levels, and the generated-plugin contract matrix.' +description: 'Test agent-bundle routes through the framework harness: the rstest config helper, renderRoute, eleven proof levels, and the generated-plugin contract matrix.' --- # Testing @@ -69,7 +69,9 @@ alternatives — while `input` and `result` come from that route's own `inputSch `resultSchema`: ```ts -const { result } = await renderRoute('tool:library/summarize', { input: { title: 'Dune' } }); +const { result } = await renderRoute('tool:library/summarize', { + input: { title: 'Dune' }, +}); const chapters: number | undefined = result?.chapters; // no cast ``` @@ -125,7 +127,8 @@ import { loadRouteModule, testManifest } from 'agent-bundle/test'; // Schemas are the MCP route contract; event routes and scripts export none. const mcpRoutes = Object.values(testManifest().routes) - .filter((route) => route.kind === 'tool' || route.kind === 'prompt' || route.kind === 'resource'); + .filter((route) => + route.kind === 'tool' || route.kind === 'prompt' || route.kind === 'resource'); for (const route of mcpRoutes) { const module = await loadRouteModule(route.id); @@ -162,8 +165,14 @@ import type { TopologyEvents, TopologyState } from '../../src/state.js'; const topology = await mountTestState(); try { - await renderRoute('event:session/start', { context: { ...topology.context(), host, workspace }, input: start }); - await renderRoute('event:tool/before', { context: { ...topology.context(), host, workspace }, input: intent }); + await renderRoute('event:session/start', { + context: { ...topology.context(), host, workspace }, + input: start, + }); + await renderRoute('event:tool/before', { + context: { ...topology.context(), host, workspace }, + input: intent, + }); const snapshot = await topology.read(); // AgentStateSnapshot expect(snapshot.state.activities).toHaveLength(1); diff --git a/website/docs/en/guide/development/workbench.mdx b/website/docs/en/guide/development/workbench.mdx index e60529bf9..72b604314 100644 --- a/website/docs/en/guide/development/workbench.mdx +++ b/website/docs/en/guide/development/workbench.mdx @@ -1,5 +1,5 @@ --- -description: 'The agent-bundle developer Workbench: its loopback boundaries, epoch-pinned MCP sessions, Playground traces, development host installs, and the optional Agent API.' +description: 'The agent-bundle developer Workbench: loopback boundaries, epoch-pinned MCP sessions, Playground traces, development host installs, and the optional Agent API.' --- # Developer Workbench diff --git a/website/docs/en/guide/distribution/installation.mdx b/website/docs/en/guide/distribution/installation.mdx index 9c34fa9c2..388de02bd 100644 --- a/website/docs/en/guide/distribution/installation.mdx +++ b/website/docs/en/guide/distribution/installation.mdx @@ -1,5 +1,5 @@ --- -description: 'Install a built agent-bundle into Claude Code, Codex, or Cursor: the install command, host scopes, the standalone install.mjs, the package-relative installer bin, and Doctor.' +description: 'Install a built agent-bundle into Claude Code, Codex, or Cursor: the install command, host scopes, the standalone install.mjs, the installer bin, and Doctor.' --- # Host installation @@ -163,9 +163,12 @@ variables each declares (`AB7331`, informational) — never a name or a value. ## Uninstalling by receipt ```sh -npx agent-bundle uninstall cursor --from artifact/cursor --plan # exact paths, nothing changes -npx agent-bundle uninstall cursor --from artifact/cursor # receipt-owned files; state/ kept -npx agent-bundle uninstall claude --from artifact/claude # claude plugin uninstall --keep-data + marketplace remove +# exact paths, nothing changes +npx agent-bundle uninstall cursor --from artifact/cursor --plan +# receipt-owned files; state/ kept +npx agent-bundle uninstall cursor --from artifact/cursor +# claude plugin uninstall --keep-data + marketplace remove +npx agent-bundle uninstall claude --from artifact/claude npx agent-bundle uninstall codex --from artifact/codex --purge-data --confirm-purge node artifact/cursor/install.mjs --uninstall [--mode marketplace] ``` diff --git a/website/docs/en/guide/distribution/preview-packages.mdx b/website/docs/en/guide/distribution/preview-packages.mdx index 12e8873c9..932643182 100644 --- a/website/docs/en/guide/distribution/preview-packages.mdx +++ b/website/docs/en/guide/distribution/preview-packages.mdx @@ -52,7 +52,8 @@ with npm. The scaffolder ships on the same channel and is meant to be run rather than installed: ```sh -npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ my-plugin +npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ \ + my-plugin ``` A scaffolded project pins `agent-bundle` to the preview of the same commit the scaffolder came diff --git a/website/docs/en/guide/start/project-structure.mdx b/website/docs/en/guide/start/project-structure.mdx index e39ad813a..7f33644af 100644 --- a/website/docs/en/guide/start/project-structure.mdx +++ b/website/docs/en/guide/start/project-structure.mdx @@ -27,7 +27,7 @@ my-plugin/ │ ├── prompts/*.tsx │ ├── apps/*.tsx # browser MCP Apps compiled to self-contained HTML │ └── layout.tsx # optional per-server layout around this server's routes - ├── scripts/.ts # artifact scripts (.tsx renders through the Agent renderer) + ├── scripts/.ts # artifact scripts (.tsx renders via the Agent renderer) ├── cli.ts # a single package bin ├── cli/**/*.ts # or a routed CLI, where nesting is the command path ├── index.ts # the library entry diff --git a/website/docs/en/guide/start/quick-start.mdx b/website/docs/en/guide/start/quick-start.mdx index 679b86bd1..1cf94ba34 100644 --- a/website/docs/en/guide/start/quick-start.mdx +++ b/website/docs/en/guide/start/quick-start.mdx @@ -13,7 +13,8 @@ The fastest start is `create-agent-bundle`. It prompts for a name, a template, a targets: ```sh -npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ my-plugin +npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ \ + my-plugin ``` Once npm releases exist, this becomes `npm create agent-bundle`. Until then, use a commit SHA or @@ -23,7 +24,8 @@ A run that names both a directory and a template is treated as scripted and asks remaining values fall back to their defaults: ```sh -npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ my-plugin \ +npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ \ + my-plugin \ --template mcp-server \ --targets portable,codex,claude ``` diff --git a/website/docs/en/reference/cli.mdx b/website/docs/en/reference/cli.mdx index 165a56088..4c644ddaa 100644 --- a/website/docs/en/reference/cli.mdx +++ b/website/docs/en/reference/cli.mdx @@ -1,5 +1,5 @@ --- -description: 'The complete agent-bundle command-line surface: dev, serve-app, build, prepack, install, doctor, validate, eval, inspect, mcp, and hooks, with every option and exit code.' +description: 'The agent-bundle CLI surface: dev, serve-app, build, prepack, install, doctor, validate, eval, inspect, mcp, and hooks, with every option and exit code.' --- # CLI @@ -73,7 +73,8 @@ The proxy inherits `--root` from the parent `dev` command and writes diagnostics ## serve-app ```sh -agent-bundle serve-app / [--artifact ] [--tool ] [--input ] [--port ] [--open] +agent-bundle serve-app / [--artifact ] [--tool ] \ + [--input ] [--port ] [--open] ``` Serves one built MCP App in a plain browser tab, outside any MCP host and without the @@ -136,7 +137,8 @@ Programmatic `build()` calls never run host validation unless `hostValidation: t ## install ```sh -agent-bundle install [--from ] [--scope ] [--mode ] [--replace] [--json] +agent-bundle install [--from ] [--scope ] [--mode ] \ + [--replace] [--json] ``` | Option | Default | Meaning | @@ -159,7 +161,8 @@ marketplace mode — that `uninstall` and `doctor` consume. ## uninstall ```sh -agent-bundle uninstall [--from ] [--scope ] [--mode ] [--keep-data | --purge-data --confirm-purge] [--force] [--plan] [--json] +agent-bundle uninstall [--from ] [--scope ] [--mode ] \ + [--keep-data | --purge-data --confirm-purge] [--force] [--plan] [--json] ``` | Option | Default | Meaning | @@ -292,8 +295,8 @@ a name) and the same `--input` / `--input-file` pair as `mcp invoke`. | Code | Meaning | | --- | --- | | `0` | Success, including `--help` and `--version`. | -| `1` | A reported failure: an error diagnostic, an invalid model from `inspect`, a failing or inconclusive eval run, or an uncaught error written to stderr as one `AB5000` diagnostic. | -| `2` | An argument-parsing failure from the command-line parser. | +| `1` | A reported failure: an error diagnostic, an invalid model from `inspect`, a failing or inconclusive eval run, or an uncaught error written to stderr as one `AB5000` diagnostic. Option values checked outside the parser land here: an invalid `install` / `uninstall` ``, `--scope`, or `--mode`, an invalid `--port`, `--trials`, or `dev --install-host`, and a malformed `--input` / `--input-file` document. | +| `2` | A parser-level failure from the command-line parser: an unknown command or option, a missing required argument or option, or an invalid `serve-app --profile`, `serve-app --allow`, or `doctor --host` value. | An eval run exits `1` when any trial **fails or is inconclusive** — an inconclusive trial produced no evidence, so it cannot report success either. diff --git a/website/docs/en/reference/configuration.mdx b/website/docs/en/reference/configuration.mdx index 7c4cde548..2bec05454 100644 --- a/website/docs/en/reference/configuration.mdx +++ b/website/docs/en/reference/configuration.mdx @@ -1,5 +1,5 @@ --- -description: 'Field reference for agent-bundle.config.ts: every top-level key, its accepted shapes, defaults, and validation rules, including the deprecated plugin.version field.' +description: 'Field reference for agent-bundle.config.ts: every top-level key, its accepted shapes, defaults, and validation rules, including the deprecated plugin.version.' --- # Configuration @@ -21,7 +21,7 @@ export default defineConfig({ | Field | Type | Default | | --- | --- | --- | | `plugin` | `{ name, description?, logo?, ... }` | **Required.** | -| `targets` | `string[]` | Adapter-selected. | +| `targets` | `string[]` | `['portable']` — the only adapter registered as a default; a `--target` flag overrides both the default and a declared list. | | `skills` | `string[]` | The `src/skills/*` convention. | | `hooks` | `Partial>` | The `src/hooks/*` convention. | | `mcp` | `{ servers: Record }` | The `src/mcp/*` convention. | @@ -35,7 +35,7 @@ export default defineConfig({ | `payload` | `Record` | None. | | `state` | `false` | The `src/state.ts` convention. | | `notices` | `{ retention?: { terminalTtl?, maxTerminal?, maxJournalBytes? } }` | Runtime defaults (`7d`, `500`, `16777216`). | -| `marketplace` | `boolean` | Adapter-selected. | +| `marketplace` | `boolean` | `false`. Only `marketplace: true` is carried into the normalized model; it is what makes the Cursor marketplace plan emit `.cursor-plugin/marketplace.json`. | | `dev` | `{ agentApi?, contracts?, runtime? }` | None. | | `evals` | `{ include?, runsDir?, semanticGrader? }` | See below. | | `tools` | `{ rsbuild?, rspack? }` | None. | @@ -52,6 +52,13 @@ The config is a TypeScript contract, not a runtime schema: `defineConfig` accept `AgentBundleConfig` (or a factory returning one), and `validateSource` enforces the rules on this page with structured diagnostics. The exact shapes below are generated from the package source by TypeDoc on every documentation build, so they cannot drift from the published types. +Two keys in the table above are not members of `AgentBundleConfig`: `evals` and `routes` are read +through its `[key: string]: unknown` index signature, so `tsc` does not check their shape. The +route compiler still parses `routes` during discovery, so `agent-bundle validate` reports a +malformed override through the route graph's diagnostics; `validateSource` never reads `evals` — +the [`evals` rules below](#evals) fire when `agent-bundle eval` or the Workbench loads the config, +as `EVAL_CONFIG_INVALID`, `EVAL_INCLUDE_INVALID`, or `EVAL_RUNS_DIR_INVALID` errors rather than +`AB` diagnostics. | Field | Type definition | | --- | --- | diff --git a/website/docs/en/reference/index.mdx b/website/docs/en/reference/index.mdx index 588d38ba9..182300e94 100644 --- a/website/docs/en/reference/index.mdx +++ b/website/docs/en/reference/index.mdx @@ -1,5 +1,5 @@ --- -description: 'Reference material for agent-bundle: the CLI surface, configuration fields, target artifacts, runtime environment, security boundaries, limitations, and the generated type API.' +description: 'agent-bundle reference: the CLI, configuration fields, target artifacts, runtime environment, security boundaries, limitations, and the generated type API.' --- # Reference diff --git a/website/docs/en/reference/limitations.mdx b/website/docs/en/reference/limitations.mdx index fb737fb2c..1a223d6a9 100644 --- a/website/docs/en/reference/limitations.mdx +++ b/website/docs/en/reference/limitations.mdx @@ -1,5 +1,5 @@ --- -description: 'Known agent-bundle limitations: unmigrated pre-0.1 durable records, loopback-only development, native harness requirements, inferred Codex activation evidence, and unverified comparison facets.' +description: 'agent-bundle limitations: unmigrated pre-0.1 records, loopback-only development, native CLI sign-in, inferred Codex activation, unverified comparison facets.' --- # Limitations diff --git a/website/docs/en/reference/runtime-environment.mdx b/website/docs/en/reference/runtime-environment.mdx index b134fa1ef..bcc06e7fa 100644 --- a/website/docs/en/reference/runtime-environment.mdx +++ b/website/docs/en/reference/runtime-environment.mdx @@ -1,5 +1,5 @@ --- -description: 'The runtime contract for generated agent-bundle executables: the Node.js floor, host path tokens, AGENT_BUNDLE environment variables, .env precedence, and durable state locations.' +description: 'Runtime contract for generated agent-bundle executables: Node.js floor, host path tokens, AGENT_BUNDLE variables, .env precedence, and durable state locations.' --- # Runtime environment diff --git a/website/docs/en/reference/security.mdx b/website/docs/en/reference/security.mdx index 36d570c52..b11cd76c7 100644 --- a/website/docs/en/reference/security.mdx +++ b/website/docs/en/reference/security.mdx @@ -1,5 +1,5 @@ --- -description: 'The agent-bundle security boundaries: no provider credentials, loopback-only development surfaces, the Agent API bearer token, installer restraint, and inert Skill Markdown.' +description: 'agent-bundle security boundaries: no provider credentials, loopback-only development, Agent API bearer token, installer restraint, and inert Skill Markdown.' --- # Security diff --git a/website/docs/public/robots.txt b/website/docs/public/robots.txt new file mode 100644 index 000000000..5975e9d53 --- /dev/null +++ b/website/docs/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://scriptedalchemy.github.io/agent-bundle/sitemap.xml diff --git a/website/docs/zh/_nav.json b/website/docs/zh/_nav.json index 5fe1c9eda..c076c6e82 100644 --- a/website/docs/zh/_nav.json +++ b/website/docs/zh/_nav.json @@ -7,7 +7,7 @@ { "text": "参考", "link": "/reference/", - "activeMatch": "/zh/reference/" + "activeMatch": "/zh/reference/(?!api$)" }, { "text": "示例", @@ -21,7 +21,7 @@ }, { "text": "类型 API", - "link": "/api/", - "activeMatch": "/zh/api/" + "link": "/reference/api", + "activeMatch": "/zh/api/|/zh/reference/api" } ] diff --git a/website/docs/zh/api/_meta.json b/website/docs/zh/api/_meta.json index dccbc0d78..f8739133e 100644 --- a/website/docs/zh/api/_meta.json +++ b/website/docs/zh/api/_meta.json @@ -3,31 +3,37 @@ { "type": "dir", "name": "modules", - "label": "模块" + "label": "模块", + "collapsed": true }, { "type": "dir", "name": "functions", - "label": "函数" + "label": "函数", + "collapsed": true }, { "type": "dir", "name": "classes", - "label": "类" + "label": "类", + "collapsed": true }, { "type": "dir", "name": "interfaces", - "label": "接口" + "label": "接口", + "collapsed": true }, { "type": "dir", "name": "types", - "label": "类型别名" + "label": "类型别名", + "collapsed": true }, { "type": "dir", "name": "variables", - "label": "变量" + "label": "变量", + "collapsed": true } ] diff --git a/website/docs/zh/guide/authoring/hooks.mdx b/website/docs/zh/guide/authoring/hooks.mdx index d7f276dc6..13ed01bfc 100644 --- a/website/docs/zh/guide/authoring/hooks.mdx +++ b/website/docs/zh/guide/authoring/hooks.mdx @@ -91,7 +91,8 @@ export default defineConfig({ import type { HookHandler } from 'agent-bundle'; export default ((event) => ({ - additionalContext: `Run verify-release from ${event.cwd ?? process.cwd()} before publishing.`, + additionalContext: + `Run verify-release from ${event.cwd ?? process.cwd()} before publishing.`, outcome: 'continue', })) satisfies HookHandler<'sessionStart'>; ``` @@ -117,20 +118,23 @@ export default ((event) => ({ | 键 | 契约 | | --- | --- | -| `outcome` | `continue`、`deny` 或 `stop`。 | +| `outcome` | `continue` 或 `deny`。没有任何事件接受 `stop`:`HookHandler` 的类型把它排除在外,生成的包装层在运行时也会拒绝它。 | | `reason` | 非空字符串,仅在拒绝 `beforeTool`、`stop` 或 `agentStop` 钩子时有效。拒绝其中之一却不给出原因会失败。 | | `additionalContext` | 追加到智能体上下文的字符串。 | | `updatedInput` | 用于替换待执行调用的输入。 | 逐事件的限制是被强制执行的,而不是「写在文档里、希望有人遵守」: -- `sessionStart`、`afterTool` 与 `agentStart` 不能拒绝、停止或替换输入。 +- `sessionStart`、`afterTool` 与 `agentStart` 不能拒绝、停止或替换输入。类型契约(以及 Claude 与 Codex 的 + 包装层)拒绝 `agentStart` 的拒绝结果;只有 Cursor 的运行时包装层容忍带原因的 `agentStart` 拒绝,并把它 + 投影为 `permission: 'deny'`——因此它只能从未带类型的结果触及。 - `beforeTool` 不能停止,也不能在拒绝的同时替换输入。继续(`continue`)的 `beforeTool` 处理器 **不做**任何权限决定:宿主的决定字段不会被写入,因此其正常的权限提示仍然适用;`updatedInput` 改写会由该流程针对改写后的输入进行评估。只有 `deny` 会被投影为决定。若要显式批准一次调用,请使用 [事件路由](#事件路由)并返回 `outcome: 'allow'`。 - `stop` 只接受 `continue`,或带原因的 `deny`。 -- `agentStop` 不能停止父流程或替换输入;在 Codex 上也不能追加上下文。 +- `agentStop` 不能停止父流程或替换输入;在 Codex 与 Cursor 上也不能追加上下文——只有 Claude Code 会投递 + `agentStop` 的 `additionalContext`。 未知的键、错误的类型,或违反上述任一规则,都会让钩子以清晰的错误消息失败,而不是被投影成一份行为 异常却毫无声响的宿主文档。 @@ -179,13 +183,17 @@ export const config = { tools: ['file.write'], // 规范选择器 -> 各宿主的原生匹配器 } satisfies AgentEventRouteConfig; -export default async function AfterFileEdit({ canonical, native, signal }: AgentEventRouteProps<'tool/after'>) { +export default async function AfterFileEdit( + { canonical, native, signal }: AgentEventRouteProps<'tool/after'>, +) { // canonical.provenance = { host, hostContractRevision, nativeEvent, source: 'native' } // 在每个宿主上,canonical.payload.toolName = { value: 'Write', nativeKey: 'tool_name' } const tool = canonical.payload.toolName?.value ?? 'a tool'; return ( - {`Recorded an edit by ${tool}, reported by ${canonical.provenance.host}.`} + + {`Recorded an edit by ${tool}, reported by ${canonical.provenance.host}.`} + ); } @@ -219,7 +227,9 @@ Claude 的 `background_tasks`、只有一个宿主才有的 `duration`——仍 import { createEventRouteInput, renderRoute } from 'agent-bundle/test'; const rendered = await renderRoute('event:tool/after', { - input: createEventRouteInput('tool/after', claudePostToolUseEnvelope, { host: 'claude' }), + input: createEventRouteInput('tool/after', claudePostToolUseEnvelope, { + host: 'claude', + }), }); ``` @@ -319,7 +329,8 @@ npx agent-bundle inspect --root . --hooks npx agent-bundle hooks list --artifact artifact --target claude npx agent-bundle hooks simulate --artifact artifact --target claude \ --hook session-start-session-start-7ab7e8a5 \ - --input '{"sessionId":"local","transcriptPath":"/tmp/transcript.jsonl","cwd":"/tmp/project","source":"startup"}' + --input '{"sessionId":"local","transcriptPath":"/tmp/transcript.jsonl", + "cwd":"/tmp/project","source":"startup"}' ``` `--hook` 必须是 `hooks list` 打印出的精确 `name`(或 `id`)——规范化会把它推导为 diff --git a/website/docs/zh/guide/authoring/mcp.mdx b/website/docs/zh/guide/authoring/mcp.mdx index 576b881d3..83f50b5bd 100644 --- a/website/docs/zh/guide/authoring/mcp.mdx +++ b/website/docs/zh/guide/authoring/mcp.mdx @@ -38,7 +38,9 @@ export const config = { export const inputSchema = z.object({ verbose: z.boolean().optional() }).strict(); export const resultSchema = z.object({ status: z.literal('ready') }).strict(); -export default async function Status({ input, signal }: ToolRouteProps) { +export default async function Status( + { input, signal }: ToolRouteProps, +) { if (signal.aborted) throw new DOMException('aborted', 'AbortError'); if (input.verbose) await agent(); const result = { status: 'ready' as const }; @@ -152,8 +154,18 @@ stderr 上色或给表格定宽的路由,再也不必自行探测 `process.std ```ts interface AgentTerminal { hostSurface: 'cli' | 'mcp' | 'hook' | 'script' | 'workbench'; - stdout: { kind: 'tty' | 'pipe' | 'none'; color: 'none' | 'basic' | '256' | 'truecolor'; columns?: number; rows?: number }; - stderr: { kind: 'tty' | 'pipe' | 'none'; color: 'none' | 'basic' | '256' | 'truecolor'; columns?: number; rows?: number }; + stdout: { + kind: 'tty' | 'pipe' | 'none'; + color: 'none' | 'basic' | '256' | 'truecolor'; + columns?: number; + rows?: number; + }; + stderr: { + kind: 'tty' | 'pipe' | 'none'; + color: 'none' | 'basic' | '256' | 'truecolor'; + columns?: number; + rows?: number; + }; sharesTarget: boolean; // fd 1 与 fd 2 指向同一个打开的文件(`2>&1`,或同一个终端) } ``` @@ -265,9 +277,15 @@ boundary 替换掉对应的回退。回退内容应使用 `Agent.Progress`—— import React, { Suspense } from 'react'; import { Agent } from '@agent-bundle/runtime'; -export default async function Search({ input, signal }: ToolRouteProps) { +export default async function Search( + { input, signal }: ToolRouteProps, +) { return ( - }> + + } + > ); @@ -727,7 +745,8 @@ App: ```sh npx agent-bundle build -npx agent-bundle serve-app status/status --artifact artifact --input '{"service":"compiler"}' --open +npx agent-bundle serve-app status/status --artifact artifact \ + --input '{"service":"compiler"}' --open ``` 该命令以与 `mcp run` 完全相同的方式启动插件打包好的 MCP 服务器(同样的产物解析、同样的 `.env` 分层、 @@ -781,7 +800,9 @@ export const resultSchema = z.object({ url: z.string().nullable(), }).strict(); -export default async function dashboard({ input, signal }: CliRouteProps) { +export default async function dashboard( + { input, signal }: CliRouteProps, +) { let served; try { served = await spawnServeApp({ diff --git a/website/docs/zh/guide/authoring/package-entries.mdx b/website/docs/zh/guide/authoring/package-entries.mdx index 7f75ff8fb..ffc4a2df4 100644 --- a/website/docs/zh/guide/authoring/package-entries.mdx +++ b/website/docs/zh/guide/authoring/package-entries.mdx @@ -56,7 +56,10 @@ export default defineConfig({ // src/cli.ts — the whole CLI entry a consumer writes import type { ExecutableMainContext } from 'agent-bundle'; -export const main = async (argv: readonly string[], { terminal }: ExecutableMainContext): Promise => { +export const main = async ( + argv: readonly string[], + { terminal }: ExecutableMainContext, +): Promise => { if (terminal.stderr.color !== 'none') { /* 在 stderr 上绘制进度 */ } return 0; }; @@ -92,7 +95,9 @@ export const inputSchema = z.object({ export const resultSchema = z.object({ scanned: z.number().int() }).strict(); -export default async function inspect({ input, signal }: CliRouteProps) { +export default async function inspect( + { input, signal }: CliRouteProps, +) { signal.throwIfAborted(); return { scanned: input.maxFiles ?? 0 }; } diff --git a/website/docs/zh/guide/development/evaluations.mdx b/website/docs/zh/guide/development/evaluations.mdx index 405ff65ab..03a2242cd 100644 --- a/website/docs/zh/guide/development/evaluations.mdx +++ b/website/docs/zh/guide/development/evaluations.mdx @@ -64,10 +64,17 @@ interface ReleaseResult { } const grade: EvalGraderFunction = async ({ fixturePath }) => { - const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as ReleaseResult; - return result.verdict === 'ready' && Array.isArray(result.blockers) && result.blockers.length === 0 + const result = JSON.parse( + await readFile(join(fixturePath, 'result.json'), 'utf8'), + ) as ReleaseResult; + return result.verdict === 'ready' + && Array.isArray(result.blockers) + && result.blockers.length === 0 ? { detail: 'The release artifact is ready with no blockers.', outcome: 'pass' } - : { detail: 'The release artifact is not ready or has unresolved blockers.', outcome: 'fail' }; + : { + detail: 'The release artifact is not ready or has unresolved blockers.', + outcome: 'fail', + }; }; export default grade; diff --git a/website/docs/zh/guide/development/testing.mdx b/website/docs/zh/guide/development/testing.mdx index 62d8c92b8..b5c5bd539 100644 --- a/website/docs/zh/guide/development/testing.mdx +++ b/website/docs/zh/guide/development/testing.mdx @@ -60,7 +60,9 @@ export const summarizes = async (): Promise => { 备选项——而 `input` 与 `result` 来自该路由自己的 `inputSchema` 与 `resultSchema`: ```ts -const { result } = await renderRoute('tool:library/summarize', { input: { title: 'Dune' } }); +const { result } = await renderRoute('tool:library/summarize', { + input: { title: 'Dune' }, +}); const chapters: number | undefined = result?.chapters; // 无需强制类型转换 ``` @@ -107,7 +109,8 @@ import { loadRouteModule, testManifest } from 'agent-bundle/test'; // schema 是 MCP 路由的契约;事件路由与脚本不导出 schema。 const mcpRoutes = Object.values(testManifest().routes) - .filter((route) => route.kind === 'tool' || route.kind === 'prompt' || route.kind === 'resource'); + .filter((route) => + route.kind === 'tool' || route.kind === 'prompt' || route.kind === 'resource'); for (const route of mcpRoutes) { const module = await loadRouteModule(route.id); @@ -139,8 +142,14 @@ import type { TopologyEvents, TopologyState } from '../../src/state.js'; const topology = await mountTestState(); try { - await renderRoute('event:session/start', { context: { ...topology.context(), host, workspace }, input: start }); - await renderRoute('event:tool/before', { context: { ...topology.context(), host, workspace }, input: intent }); + await renderRoute('event:session/start', { + context: { ...topology.context(), host, workspace }, + input: start, + }); + await renderRoute('event:tool/before', { + context: { ...topology.context(), host, workspace }, + input: intent, + }); const snapshot = await topology.read(); // AgentStateSnapshot expect(snapshot.state.activities).toHaveLength(1); diff --git a/website/docs/zh/guide/distribution/installation.mdx b/website/docs/zh/guide/distribution/installation.mdx index 463cda04d..fd61e5a80 100644 --- a/website/docs/zh/guide/distribution/installation.mdx +++ b/website/docs/zh/guide/distribution/installation.mdx @@ -135,9 +135,12 @@ RTORRENT_SSH_HOST=nas.local ## 按回执卸载 ```sh -npx agent-bundle uninstall cursor --from artifact/cursor --plan # 确切路径,不做改动 -npx agent-bundle uninstall cursor --from artifact/cursor # 回执归属的文件;保留 state/ -npx agent-bundle uninstall claude --from artifact/claude # claude plugin uninstall --keep-data + marketplace remove +# 确切路径,不做改动 +npx agent-bundle uninstall cursor --from artifact/cursor --plan +# 回执归属的文件;保留 state/ +npx agent-bundle uninstall cursor --from artifact/cursor +# claude plugin uninstall --keep-data + marketplace remove +npx agent-bundle uninstall claude --from artifact/claude npx agent-bundle uninstall codex --from artifact/codex --purge-data --confirm-purge node artifact/cursor/install.mjs --uninstall [--mode marketplace] ``` diff --git a/website/docs/zh/guide/distribution/preview-packages.mdx b/website/docs/zh/guide/distribution/preview-packages.mdx index ffc266eeb..68f9f5e94 100644 --- a/website/docs/zh/guide/distribution/preview-packages.mdx +++ b/website/docs/zh/guide/distribution/preview-packages.mdx @@ -42,7 +42,8 @@ SHA 的预览 tarball,而 pnpm 11 的 `blockExoticSubdeps`(默认 `true`) 脚手架发布在同一条通道上,其设计意图是直接运行而不是安装: ```sh -npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ my-plugin +npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ \ + my-plugin ``` 脚手架生成的项目会把 `agent-bundle` 固定到脚手架自身所来自的那个提交的预览版本,因此上述配对规则的 diff --git a/website/docs/zh/guide/start/quick-start.mdx b/website/docs/zh/guide/start/quick-start.mdx index 07fd8cfbc..7c1015de0 100644 --- a/website/docs/zh/guide/start/quick-start.mdx +++ b/website/docs/zh/guide/start/quick-start.mdx @@ -11,7 +11,8 @@ description: '用脚手架创建 agent-bundle 项目,或手写 agent-bundle.co 最快的方式是 `create-agent-bundle`。它会依次询问名称、模板与宿主 targets: ```sh -npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ my-plugin +npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ \ + my-plugin ``` 等到 npm 正式发布之后,这条命令会变成 `npm create agent-bundle`。在此之前,请使用 @@ -20,7 +21,8 @@ npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ my-plugin \ +npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ \ + my-plugin \ --template mcp-server \ --targets portable,codex,claude ``` diff --git a/website/docs/zh/reference/cli.mdx b/website/docs/zh/reference/cli.mdx index c8dafa1b0..dcd10ee17 100644 --- a/website/docs/zh/reference/cli.mdx +++ b/website/docs/zh/reference/cli.mdx @@ -72,7 +72,8 @@ proxy 从父命令 `dev` 继承 `--root`,并把诊断写到 stderr。 ## serve-app ```sh -agent-bundle serve-app / [--artifact ] [--tool ] [--input ] [--port ] [--open] +agent-bundle serve-app / [--artifact ] [--tool ] \ + [--input ] [--port ] [--open] ``` 在一个普通浏览器标签页里独立提供一个已构建的 MCP App——不在任何 MCP 宿主之内,也不需要 @@ -130,7 +131,8 @@ JSON 文档在 `hostValidation` 下携带这些报告,每个被校验的 targe ## install ```sh -agent-bundle install [--from ] [--scope ] [--mode ] [--replace] [--json] +agent-bundle install [--from ] [--scope ] [--mode ] \ + [--replace] [--json] ``` | 选项 | 默认值 | 含义 | @@ -151,7 +153,8 @@ agent-bundle install [--from ] [--scope ] [--mode [--from ] [--scope ] [--mode ] [--keep-data | --purge-data --confirm-purge] [--force] [--plan] [--json] +agent-bundle uninstall [--from ] [--scope ] [--mode ] \ + [--keep-data | --purge-data --confirm-purge] [--force] [--plan] [--json] ``` | 选项 | 默认值 | 含义 | @@ -271,7 +274,7 @@ keep-data 选项)。相对包的安装器 bin 接受带同样标志的 `uninst | --- | --- | | `0` | 成功,包括 `--help` 与 `--version`。 | | `1` | 一次被报告的失败:一条 error 级诊断、`inspect` 得到的无效模型、失败或结论不明的 eval 运行,或者被作为一条 `AB5000` 诊断写到 stderr 的未捕获错误。 | -| `2` | 来自命令行解析器的参数解析失败。 | +| `2` | 仅限命令行解析器(Commander)自身的错误:未知选项、缺少参数,或者 `serve-app` 的 `--profile` 与 `--allow`、`doctor --host` 的无效取值——这三个校验器抛出 `InvalidArgumentError`。`install`/`uninstall` 的 `` 参数、`--scope` 与 `--mode`,以及 `dev --install-host`、`--port` 与 `eval --trials` 的校验器抛出的是普通 `TypeError`,`runCli` 会把它作为一条 `AB5000` 诊断写到 stderr 并以 `1` 退出。 | 当任何一次试验**失败或结论不明**时,eval 运行以 `1` 退出——结论不明的试验没有产生证据,因此它同样不能 报告成功。 diff --git a/website/docs/zh/reference/configuration.mdx b/website/docs/zh/reference/configuration.mdx index b548af1ed..d68899e14 100644 --- a/website/docs/zh/reference/configuration.mdx +++ b/website/docs/zh/reference/configuration.mdx @@ -21,7 +21,7 @@ export default defineConfig({ | 字段 | 类型 | 默认值 | | --- | --- | --- | | `plugin` | `{ name, description?, logo?, ... }` | **必填。** | -| `targets` | `string[]` | 由适配器选择。 | +| `targets` | `string[]` | `['portable']`——唯一以 `default: true` 注册的适配器;`--target` 会同时覆盖配置值与默认值。 | | `skills` | `string[]` | `src/skills/*` 约定。 | | `hooks` | `Partial>` | `src/hooks/*` 约定。 | | `mcp` | `{ servers: Record }` | `src/mcp/*` 约定。 | @@ -35,7 +35,7 @@ export default defineConfig({ | `payload` | `Record` | 无。 | | `state` | `false` | `src/state.ts` 约定。 | | `notices` | `{ retention?: { terminalTtl?, maxTerminal?, maxJournalBytes? } }` | 运行时默认值(`7d`、`500`、`16777216`)。 | -| `marketplace` | `boolean` | 由适配器选择。 | +| `marketplace` | `boolean` | `false`。 | | `dev` | `{ agentApi?, contracts?, runtime? }` | 无。 | | `evals` | `{ include?, runsDir?, semanticGrader? }` | 见下文。 | | `tools` | `{ rsbuild?, rspack? }` | 无。 | @@ -48,8 +48,11 @@ export default defineConfig({ ### 生成的类型定义 配置是一份 TypeScript 契约,而不是运行时 schema:`defineConfig` 接受一个 `AgentBundleConfig`(或返回 -它的工厂函数),`validateSource` 则用结构化诊断强制执行本页的规则。下面这些精确形态在每次文档构建时由 -TypeDoc 从包源码生成,因此不会与已发布的类型产生偏差。 +它的工厂函数),`validateSource` 则用结构化诊断强制执行本页的规则。`evals` 与 `routes` 是例外:二者都不是 +`AgentBundleConfig` 的成员——它们经由 `[key: string]: unknown` 索引签名传入——因此 `tsc` 不会检查它们的 +形状;`evals` 的规则要到 eval 运行时才生效(`EVAL_CONFIG_INVALID`),`agent-bundle validate` 不会报告它们, +而 `routes` 覆盖块由路由图在发现阶段校验,其诊断随 `validateSource` 一并报告。下面这些精确形态在每次文档 +构建时由 TypeDoc 从包源码生成,因此不会与已发布的类型产生偏差。 | 字段 | 类型定义 | | --- | --- | diff --git a/website/package.json b/website/package.json index 8af8f63c0..a75d25be8 100644 --- a/website/package.json +++ b/website/package.json @@ -8,7 +8,10 @@ }, "scripts": { "build": "rspress build", - "check": "pnpm typecheck && pnpm build", + "check": "pnpm typecheck && pnpm check:drift && pnpm check:diagnostics && RSPRESS_PERSISTENT_CACHE=false pnpm build && pnpm check:links", + "check:diagnostics": "node scripts/check-diagnostics-coverage.mjs", + "check:drift": "node scripts/check-locale-drift.mjs", + "check:links": "node scripts/check-built-links.mjs", "dev": "rspress dev", "preview": "rspress preview", "typecheck": "tsc --project tsconfig.json" @@ -24,8 +27,6 @@ "@types/hast": "3.0.5", "@types/node": "26.4.0", "@types/react": "19.2.18", - "@types/react-dom": "19.2.5", - "agent-bundle": "workspace:*", "react": "19.2.8", "react-dom": "19.2.8", "typescript": "6.0.3", diff --git a/website/plugins/mirror-api-locale.ts b/website/plugins/mirror-api-locale.ts index de6f94d71..406adb1fa 100644 --- a/website/plugins/mirror-api-locale.ts +++ b/website/plugins/mirror-api-locale.ts @@ -1,8 +1,9 @@ -import { copyFile, mkdir, readdir, rm } from 'node:fs/promises'; +import { copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { RspressPlugin } from '@rspress/core'; const MARKDOWN_EXTENSION = '.md'; +const FRONTMATTER_FENCE = '---'; async function readDirectory(directory: string) { try { @@ -66,11 +67,50 @@ export async function cleanGeneratedApiMarkdown(directory: string): Promise index >= searchFrom && line.startsWith('# ')); + if (titleIndex === -1) { + throw new Error(`${filePath}: no "# " title to place the locale notice under`); + } + + lines.splice(titleIndex + 1, 0, '', ':::info', notice, ':::'); + return lines.join('\n'); +} + +export interface MirrorApiLocaleTarget { + /** Docs-root-relative directory that receives the mirrored Markdown. */ + dir: string; + /** + * Markdown placed in an `:::info` container under every mirrored page's + * title, written in the target locale's language: the body stays English, + * and this is where the reader is told so. + */ + notice?: string; +} + export interface MirrorApiLocaleOptions { /** Docs-root-relative directory that TypeDoc generates, such as `en/api`. */ sourceDir: string; - /** Docs-root-relative directories that receive the mirrored Markdown. */ - targetDirs: string[]; + /** Locale directories that receive the mirrored Markdown. */ + targets: MirrorApiLocaleTarget[]; } /** @@ -85,7 +125,7 @@ export interface MirrorApiLocaleOptions { * runs once the English reference has been written and before route scanning. */ export function mirrorApiLocale(options: MirrorApiLocaleOptions): RspressPlugin { - const { sourceDir, targetDirs } = options; + const { sourceDir, targets } = options; return { name: 'agent-bundle/mirror-api-locale', @@ -98,14 +138,22 @@ export function mirrorApiLocale(options: MirrorApiLocaleOptions): RspressPlugin const source = path.join(docsRoot, sourceDir); const generatedFiles = await collectMarkdownFiles(source); - for (const targetDir of targetDirs) { - const target = path.join(docsRoot, targetDir); + for (const { dir, notice } of targets) { + const target = path.join(docsRoot, dir); await cleanGeneratedApiMarkdown(target); for (const relativePath of generatedFiles) { + const sourcePath = path.join(source, relativePath); const destination = path.join(target, relativePath); await mkdir(path.dirname(destination), { recursive: true }); - await copyFile(path.join(source, relativePath), destination); + + if (notice === undefined) { + await copyFile(sourcePath, destination); + continue; + } + + const markdown = await readFile(sourcePath, 'utf8'); + await writeFile(destination, insertNoticeAfterTitle(markdown, notice, sourcePath)); } } diff --git a/website/rspress.config.ts b/website/rspress.config.ts index 48014aa69..d836696fc 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -4,11 +4,8 @@ import { pluginLlms } from '@rspress/plugin-llms'; import { pluginSitemap } from '@rspress/plugin-sitemap'; import { pluginTwoslash } from '@rspress/plugin-twoslash'; import { pluginTypeDoc } from '@rspress/plugin-typedoc'; -import { - transformerNotationDiff, - transformerNotationFocus, - transformerNotationHighlight, -} from '@shikijs/transformers'; +import { transformerNotationHighlight } from '@shikijs/transformers'; +import ts from 'typescript'; import { generatedReference } from './plugins/generated-reference.ts'; import { cleanGeneratedApiMarkdown, mirrorApiLocale } from './plugins/mirror-api-locale.ts'; import { rehypeTableCellBreaks } from './plugins/rehype-table-cell-breaks.ts'; @@ -18,6 +15,54 @@ const docsDir = path.join(websiteDir, 'docs'); const repoRoot = path.join(websiteDir, '..'); const packageSource = path.join(repoRoot, 'packages', 'agent-bundle', 'src'); +const typedocTsconfigPath = path.join(websiteDir, 'tsconfig.typedoc.json'); + +/** + * The `paths` that resolve the workspace packages `packages/agent-bundle/src` + * imports (`@agent-bundle/runtime`, `rsc-markdown-stream`) to their sources. + * Their published declarations only exist after `pnpm build`, which the docs + * build never runs, so a compiler that resolves them through `package.json` + * types every one of those imports as `any`. TypeDoc reads the tsconfig + * itself; twoslash takes its compiler options programmatically, so the same + * map is read here rather than copied. + * + * The file is JSONC and the values are relative to the tsconfig, which is how + * TypeScript resolves them; twoslash gets no `baseUrl` or config directory, + * so each target is made absolute before it is handed over. + */ +function readSourceMappedPaths(tsconfigPath: string): Record { + const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + if (error) { + throw new Error( + `${tsconfigPath}: ${ts.flattenDiagnosticMessageText(error.messageText, '\n')}`, + ); + } + const paths: unknown = config?.compilerOptions?.paths; + if (!paths || typeof paths !== 'object' || Object.keys(paths).length === 0) { + throw new Error(`${tsconfigPath}: expected a non-empty compilerOptions.paths`); + } + const tsconfigDir = path.dirname(tsconfigPath); + return Object.fromEntries( + Object.entries(paths).map(([specifier, targets]) => { + if (!Array.isArray(targets) || !targets.every(target => typeof target === 'string')) { + throw new Error(`${tsconfigPath}: compilerOptions.paths["${specifier}"] must be string[]`); + } + return [specifier, targets.map(target => path.resolve(tsconfigDir, target))]; + }), + ); +} + +/** + * Twoslash samples import the package by its published name, which nothing in + * `packages/agent-bundle/src` does, so those four entries live only here. + */ +const twoslashPaths: Record = { + 'agent-bundle': [path.join(packageSource, 'index.ts')], + 'agent-bundle/config': [path.join(packageSource, 'config/index.ts')], + 'agent-bundle/test': [path.join(packageSource, 'test/index.ts')], + 'agent-bundle/eval': [path.join(packageSource, 'eval/index.ts')], + ...readSourceMappedPaths(typedocTsconfigPath), +}; const publicApiEntryPoints = [ 'index.ts', @@ -37,7 +82,12 @@ const publicApiEntryPoints = [ ].map(entry => path.join(packageSource, entry)); const generatedApiDir = 'en/api'; -const mirroredApiDirs = ['zh/api']; +const mirroredApiTargets = [ + { + dir: 'zh/api', + notice: 'API 参考仅提供英文版本;正文与英文站点相同。', + }, +]; const repositoryUrl = 'https://github.com/ScriptedAlchemy/agent-bundle'; const siteTitle = 'agent-bundle'; @@ -45,6 +95,8 @@ const siteDescription = 'Compile skills, hooks, MCP servers, and scripts from one typed config into installable Claude Code, Codex, and Cursor artifacts.'; const siteDescriptionZh = '用一份带类型的配置描述 Skill、钩子、MCP 服务器与脚本,编译为可直接安装到 Claude Code、Codex 与 Cursor 的产物。'; +/** `--rp-c-brand` in `styles/index.css`. */ +const brandColor = '#0d8f80'; /** * `llms.txt` and `llms-full.txt` are emitted as build assets rather than @@ -63,6 +115,13 @@ export default defineConfig({ icon: '/logo.svg', logo: '/logo.svg', logoText: siteTitle, + // Only rendered at SSG (`renderHtmlTemplate`); `rspress dev` leaves the head + // marker untouched, so the tag is absent from the dev server. Per-route + // entries (`route => ['link', { rel: 'canonical', ... }]`) are typed but + // cannot be combined with `ssg.experimentalWorker`: `renderPages` ships + // `config.head` to the worker threads through `workerData`, and a function + // fails structured cloning (`DataCloneError`) before the first page renders. + head: [['meta', { name: 'theme-color', content: brandColor }]], locales: [ { lang: 'en', @@ -77,24 +136,31 @@ export default defineConfig({ description: siteDescriptionZh, }, ], - search: { - codeBlocks: true, - }, route: { cleanUrls: true, localeRedirect: 'never', }, + // Renders the ~900 routes across a tinypool of worker threads instead of one + // process; output is identical. + ssg: { + experimentalWorker: true, + }, + builderConfig: { + performance: { + // The per-asset table is one line per route (plus chunks) and buries the + // dead-link and parity results; the total is still printed. + printFileSize: { detail: false }, + }, + }, markdown: { shiki: { // Twoslash hovers render JSDoc code fences from dependency types, so the // grammar set cannot be inferred from page sources alone (same fix as // the upstream rspress.rs site). langs: ['markdown', 'mdx', 'ts', 'tsx', 'js', 'jsx', 'json', 'bash', 'yaml', 'css', 'html'], - transformers: [ - transformerNotationDiff(), - transformerNotationHighlight(), - transformerNotationFocus(), - ], + // Only `[!code highlight]` is used in the docs; add the diff or focus + // transformer back alongside the first page that needs its notation. + transformers: [transformerNotationHighlight()], }, link: { checkDeadLinks: { excludes: isGeneratedLlmsTarget }, @@ -118,6 +184,11 @@ export default defineConfig({ docRepoBaseUrl: `${repositoryUrl}/tree/main/website/docs`, }, socialLinks: [{ icon: 'github', mode: 'link', content: repositoryUrl }], + // Rendered by `HomeFooter` on the home layout only, and read from the + // site-level theme config, so one message serves both locales. + footer: { + message: `Released under the Apache-2.0 License.`, + }, }, plugins: [ // TypeDoc and twoslash compile packages/agent-bundle/src with the @@ -134,8 +205,10 @@ export default defineConfig({ // so typedoc-plugin-markdown's escaped underscores (`FOO\_BAR`) would // surface verbatim. Intraword underscores are not emphasis in // CommonMark, so the member title is safe to emit unescaped. + // `includeVersion` is unset, so `{version}` would only leave a + // trailing space in the title. app.options.setValue('pageTitleTemplates', { - index: '{projectName} {version}', + index: '{projectName}', module: '{kind}: {name}', member: ({ kind, name }: { kind: string; name: string }) => `${kind}: ${name.replace(/\\_/g, '_')}`, @@ -145,7 +218,7 @@ export default defineConfig({ }), mirrorApiLocale({ sourceDir: generatedApiDir, - targetDirs: mirroredApiDirs, + targets: mirroredApiTargets, }), generatedReference({ repoRoot, @@ -157,12 +230,7 @@ export default defineConfig({ pluginTwoslash({ twoslashOptions: { compilerOptions: { - paths: { - 'agent-bundle': [path.join(packageSource, 'index.ts')], - 'agent-bundle/config': [path.join(packageSource, 'config/index.ts')], - 'agent-bundle/test': [path.join(packageSource, 'test/index.ts')], - 'agent-bundle/eval': [path.join(packageSource, 'eval/index.ts')], - }, + paths: twoslashPaths, }, }, }), diff --git a/website/scripts/check-built-links.mjs b/website/scripts/check-built-links.mjs new file mode 100644 index 000000000..a72041fab --- /dev/null +++ b/website/scripts/check-built-links.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node +// Post-build link and anchor check over website/doc_build (issue #590, P2). +// +// Rspress's dead-link and dead-anchor checks inspect only mdast link/definition/ +// image nodes, so frontmatter `hero.actions[].link`/`features[].link`, `_nav.json` +// links, raw `` in Markdown, `` JSX, and every generated page +// (TypeDoc, the reference copies) are unchecked — and with the persistent Rspack +// cache a dead anchor into an unchanged page passes on a warm build. This walks +// the emitted HTML one file at a time, resolves every site-internal `href`/`src` +// (plus `og:url`/`og:image` content, canonical/alternate links, and sitemap +// ``s) to a file under doc_build honouring `cleanUrls`, and requires every +// `#fragment` on an HTML target to name an `id` in that file. The llms.txt and +// `.md` copies are build assets that resolve like any other file. No dependencies. +// +// Usage: node scripts/check-built-links.mjs [--dir ] [--help] + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const BASE = '/agent-bundle/'; +const ORIGIN = 'https://scriptedalchemy.github.io'; +const TAG = /<(a|link|script|img|source|iframe|meta)\b([^>]*)>/gi; +const ATTRIBUTE = /([a-zA-Z:-]+)\s*=\s*"([^"]*)"/g; +const ID = /\s(?:id|name)="([^"]*)"/g; +const SKIPPED_SCHEMES = /^(?:mailto:|javascript:|tel:|data:|https?:\/\/|\/\/)/i; + +const usage = `Usage: node scripts/check-built-links.mjs [--dir ] + --dir built site to walk (default: website/doc_build) + --help print this message`; + +const parseArgs = argv => { + const options = { dir: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'doc_build') }; + for (let index = 0; index < argv.length; index += 1) { + if (argv[index] === '--help' || argv[index] === '-h') return { help: true }; + if (argv[index] === '--dir' && argv[index + 1]) options.dir = path.resolve(argv[(index += 1)]); + else if (argv[index].startsWith('--dir=')) options.dir = path.resolve(argv[index].slice(6)); + else throw new Error(`unknown argument: ${argv[index]}`); + } + return options; +}; + +const walk = dir => + fs + .readdirSync(dir, { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name, 'en')) + .flatMap(entry => (entry.isDirectory() ? walk(path.join(dir, entry.name)) : [path.join(dir, entry.name)])); + +const decode = value => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; +const unescapeHtml = value => value.replace(/&/g, '&').replace(/'|'/g, "'").replace(/"/g, '"'); + +/** URLs named by a page: `href`/`src` of the interesting tags, `og:url`/`og:image` meta content, sitemap ``s. */ +const collectUrls = (file, html) => { + const urls = []; + if (file.endsWith('.xml')) for (const [, loc] of html.matchAll(/([^<]*)<\/loc>/g)) urls.push(loc); + else + for (const [, tag, rawAttributes] of html.matchAll(TAG)) { + const attributes = Object.fromEntries([...rawAttributes.matchAll(ATTRIBUTE)].map(([, name, value]) => [name.toLowerCase(), value])); + if (tag.toLowerCase() === 'meta') { + if (/^og:(?:url|image)$/.test(attributes.property ?? '') && attributes.content) urls.push(attributes.content); + } else { + for (const key of ['href', 'src']) if (attributes[key]) urls.push(attributes[key]); + } + } + return urls.map(unescapeHtml); +}; + +const main = () => { + const options = parseArgs(process.argv.slice(2)); + if (options.help) return void console.log(usage); + if (!fs.existsSync(path.join(options.dir, 'index.html'))) throw new Error(`${options.dir} has no index.html; build the site first`); + const files = walk(options.dir).filter(file => file.endsWith('.html') || file.endsWith('sitemap.xml')); + const idsByFile = new Map(); + const idsOf = file => { + if (!idsByFile.has(file)) idsByFile.set(file, new Set([...fs.readFileSync(file, 'utf8').matchAll(ID)].map(([, id]) => decode(id)))); + return idsByFile.get(file); + }; + const existsCache = new Map(); + const isFile = file => { + if (!existsCache.has(file)) existsCache.set(file, fs.existsSync(file) && fs.statSync(file).isFile()); + return existsCache.get(file); + }; + /** + * cleanUrls resolution: verbatim file, then `x.html`, then `x/index.html`. A + * candidate that escapes doc_build (`/agent-bundle/../package.json`, or an + * encoded `..`) is not a page even when the file exists on disk. + */ + const insideBuild = file => file.startsWith(`${options.dir}${path.sep}`); + const resolveTarget = pathname => { + const relative = decode(pathname.slice(BASE.length)).replace(/^\/+/, ''); + const candidates = relative === '' || relative.endsWith('/') ? [`${relative}index.html`] : [relative, `${relative}.html`, `${relative}/index.html`]; + return candidates.map(candidate => path.resolve(options.dir, candidate)).find(file => insideBuild(file) && isFile(file)) ?? null; + }; + const broken = new Map(); + let links = 0; + let anchors = 0; + for (const file of files) { + const html = fs.readFileSync(file, 'utf8'); + const pagePath = `/${path.relative(options.dir, file).split(path.sep).join('/')}`; + if (file.endsWith('.html')) idsByFile.set(file, new Set([...html.matchAll(ID)].map(([, id]) => decode(id)))); + for (const raw of collectUrls(file, html)) { + let url = raw.trim(); + if (url === '' || url === '#') continue; + if (url.startsWith(ORIGIN)) url = url.slice(ORIGIN.length) || '/'; + if (SKIPPED_SCHEMES.test(url)) continue; + links += 1; + const hash = url.indexOf('#'); + const fragment = hash === -1 ? '' : decode(url.slice(hash + 1)); + let pathname = (hash === -1 ? url : url.slice(0, hash)).replace(/\?.*$/, ''); + if (pathname === '' && fragment === '') continue; + if (pathname !== '' && !pathname.startsWith('/')) pathname = path.posix.resolve(path.posix.dirname(`${BASE}${pagePath.slice(1)}`), pathname); + const target = pathname === '' ? file : pathname.startsWith(BASE) ? resolveTarget(pathname) : null; + const key = pathname === '' ? `${pagePath}${url}` : url; + const report = reason => { + if (!broken.has(key)) broken.set(key, `${key} — ${reason} (e.g. in ${pagePath})`); + }; + if (target === null) { + report(pathname.startsWith(BASE) ? 'no file under doc_build' : `outside the ${BASE} base`); + continue; + } + if (fragment === '' || !target.endsWith('.html')) continue; + anchors += 1; + if (!idsOf(target).has(fragment)) report(`no id="${fragment}" in ${path.relative(options.dir, target)}`); + } + } + for (const line of [...broken.values()].sort()) console.log(line); + console.log(`${broken.size} broken links / ${anchors} anchors checked (${links} internal links across ${files.length} files under ${options.dir})`); + if (broken.size > 0) process.exitCode = 1; +}; + +try { + main(); +} catch (error) { + console.error(`check-built-links: ${error.message}`); + process.exitCode = 2; +} diff --git a/website/scripts/check-diagnostics-coverage.mjs b/website/scripts/check-diagnostics-coverage.mjs new file mode 100644 index 000000000..7d6de4765 --- /dev/null +++ b/website/scripts/check-diagnostics-coverage.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +// Diagnostics contract coverage check (issue #590, P1 companion). +// +// `docs/diagnostics.md` is the diagnostics contract and the site's +// `reference/diagnostics` page is a build-time copy of it, but nothing verified +// that every code a reader meets is actually documented there: the family +// catch-all rows (`AB42xx`, `AB474x`/`AB4750`) describe an area, not a code, so +// a page could cite `AB4204` and the compiler could emit `AB6005` while the +// contract said nothing about either. This script parses the contract into the +// explicitly documented codes — a `` `ABnnnn` `` at the start of a table row or +// heading, or an explicit `` `ABnnnn`–`ABmmmm` `` range at the start of a table +// row (en dash, hyphen, or em dash) — and fails when (a) a code cited in an +// authored `website/docs/{en,zh}/**/*.mdx` page (the TypeDoc `api/` tree is +// skipped) or (b) a code literal in `packages/agent-bundle/src/**/*.ts` +// (tests excluded; `--no-src` skips this strict rule) is not covered. +// +// Usage: node scripts/check-diagnostics-coverage.mjs [--repo-root ] [--src|--no-src] [--help] + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const CONTRACT = path.join('docs', 'diagnostics.md'); +const DOCS_ROOTS = [path.join('website', 'docs', 'en'), path.join('website', 'docs', 'zh')]; +const SOURCE_ROOT = path.join('packages', 'agent-bundle', 'src'); +const CODE = /\bAB\d{4}\b/g; +const EXPLICIT_CODE = /^`AB(\d{4})`$/; +const EXPLICIT_RANGE = /^`AB(\d{4})`\s*[–—-]\s*`AB(\d{4})`$/; +const FAMILY_WILDCARD = /^`AB[\dx]{0,3}x+`$/i; +const LOCALES = ['en', 'zh']; + +const usage = `Usage: node scripts/check-diagnostics-coverage.mjs [--repo-root ] [--src|--no-src] + +Requires every ABnnnn code cited in website/docs/{en,zh}/**/*.mdx — and, unless --no-src, +every ABnnnn literal in packages/agent-bundle/src/**/*.ts — to be explicitly documented in +docs/diagnostics.md (a code or code range at the start of a table row, or a code heading). + --repo-root repository root (default: two levels above this script) + --src / --no-src include or skip the source-literal rule (default: --src) + --help print this message`; + +const parseArgs = argv => { + const options = { repoRoot: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'), src: true }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--help' || arg === '-h') return { help: true }; + if (arg === '--src') options.src = true; + else if (arg === '--no-src') options.src = false; + else if (arg === '--repo-root' && argv[index + 1]) options.repoRoot = path.resolve(argv[(index += 1)]); + else if (arg.startsWith('--repo-root=')) options.repoRoot = path.resolve(arg.slice('--repo-root='.length)); + else throw new Error(`unknown argument: ${arg}`); + } + return options; +}; + +/** Sorted files under `dir` accepted by `keep(relativePosixPath, name)`, pruning directories `keep` rejects. */ +const walk = (dir, keep, prefix = '') => { + if (!fs.existsSync(dir)) return []; + return fs + .readdirSync(dir, { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name, 'en')) + .flatMap(entry => { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (!keep(relative, entry.name, entry.isDirectory())) return []; + return entry.isDirectory() ? walk(path.join(dir, entry.name), keep, relative) : [path.join(dir, entry.name)]; + }); +}; + +/** + * The explicitly documented codes. A table row's first cell may list several + * items separated by `,` or `/`; a row whose first cell names a family wildcard + * (`AB42xx`, `AB474x`/`AB4750`) is a catch-all and documents nothing. + */ +const parseContract = source => { + const codes = new Set(); + const ranges = []; + let explicitRows = 0; + const addRange = (from, to) => { + ranges.push(`AB${from}–AB${to}`); + for (let code = Number(from); code <= Number(to); code += 1) codes.add(`AB${String(code).padStart(4, '0')}`); + }; + for (const line of source.split(/\r?\n/)) { + const heading = /^#{1,6}\s+(`AB\d{4}`(?:\s*[–—-]\s*`AB\d{4}`)?)(?=\s|$)/.exec(line); + const items = heading ? [heading[1]] : line.startsWith('|') ? (line.split('|')[1] ?? '').trim().split(/\s*[,/]\s*/) : []; + if (items.length === 0 || items.some(item => FAMILY_WILDCARD.test(item))) continue; + const parsed = items.map(item => EXPLICIT_CODE.exec(item) ?? EXPLICIT_RANGE.exec(item)); + if (parsed.some(match => match === null)) continue; + explicitRows += 1; + for (const match of parsed) { + if (match.length === 2) codes.add(`AB${match[1]}`); + else addRange(match[1], match[2]); + } + } + return { codes, ranges, explicitRows }; +}; + +/** Every distinct code literal across `files`, with the first file that cites it. */ +const collectCodes = files => { + const cited = new Map(); + for (const file of files) { + for (const code of fs.readFileSync(file, 'utf8').match(CODE) ?? []) if (!cited.has(code)) cited.set(code, file); + } + return cited; +}; + +const isAuthoredPage = (relative, name, isDirectory) => (isDirectory ? name !== 'api' : name.endsWith('.mdx')); +const isSourceModule = (relative, name, isDirectory) => + isDirectory ? name !== 'tests' && name !== '__tests__' : name.endsWith('.ts') && !/\.(?:test|spec)\.ts$/.test(name); + +const familyOf = code => `${code.slice(0, 4)}xx`; + +const reportUncovered = (label, cited, covered, repoRoot) => { + const uncovered = [...cited.keys()].filter(code => !covered.has(code)).sort(); + if (uncovered.length === 0) { + console.log(`${label}: ${cited.size} distinct code(s) cited, all documented`); + return 0; + } + console.log(`${label}: ${uncovered.length} of ${cited.size} distinct code(s) not documented in ${CONTRACT}:`); + const byFamily = new Map(); + for (const code of uncovered) byFamily.set(familyOf(code), [...(byFamily.get(familyOf(code)) ?? []), code]); + for (const [family, codes] of [...byFamily].sort()) { + console.log(` ${family}: ${codes.map(code => `${code} (${path.relative(repoRoot, cited.get(code)).split(path.sep).join('/')})`).join(', ')}`); + } + return uncovered.length; +}; + +const main = () => { + const options = parseArgs(process.argv.slice(2)); + if (options.help) return void console.log(usage); + const contractPath = path.join(options.repoRoot, CONTRACT); + if (!fs.existsSync(contractPath)) throw new Error(`${contractPath} not found; pass --repo-root`); + const { codes, ranges, explicitRows } = parseContract(fs.readFileSync(contractPath, 'utf8')); + console.log(`${CONTRACT}: ${codes.size} code(s) documented explicitly from ${explicitRows} row(s)/heading(s), ${ranges.length} range(s): ${ranges.join(', ')}`); + const pages = DOCS_ROOTS.flatMap(root => walk(path.join(options.repoRoot, root), isAuthoredPage)); + let failures = reportUncovered(`website/docs/{${LOCALES.join(',')}}/**/*.mdx`, collectCodes(pages), codes, options.repoRoot); + if (options.src) { + const modules = walk(path.join(options.repoRoot, SOURCE_ROOT), isSourceModule); + failures += reportUncovered(`${SOURCE_ROOT.split(path.sep).join('/')}/**/*.ts`, collectCodes(modules), codes, options.repoRoot); + } else { + console.log(`${SOURCE_ROOT.split(path.sep).join('/')}: skipped (--no-src)`); + } + if (failures > 0) process.exitCode = 1; +}; + +try { + main(); +} catch (error) { + console.error(`check-diagnostics-coverage: ${error.message}`); + process.exitCode = 2; +} diff --git a/website/scripts/check-locale-drift.mjs b/website/scripts/check-locale-drift.mjs new file mode 100644 index 000000000..a3318d807 --- /dev/null +++ b/website/scripts/check-locale-drift.mjs @@ -0,0 +1,296 @@ +#!/usr/bin/env node +// Locale drift check for website/docs (issue #590, P2). +// +// Rspress's `languageParity` compares only the SET of `.md`/`.mdx` paths under +// each locale, so a `zh/` page whose code samples, diagnostic codes, tables, or +// section structure fell behind its `en/` twin — or a `_meta.json`/`_nav.json` +// whose entries or link targets diverged — still builds green. This script +// walks every authored `en/**` page (skipping the TypeDoc `api/` tree and the +// four build-time reference copies) and fails when the `zh/` twin is missing, +// has a different number of fenced code blocks, a fence whose code differs +// (comments in `sh`/`bash`/`ts`/`tsx`/`js`/`json` fences are stripped first so +// translated comments do not count; `text`/`md` fences compare verbatim), a +// different set of `ABnnnn` codes, a different table-row count, or an h2/h3 +// count that drifts by more than two. `_meta.json`/`_nav.json` must have the +// same entries in the same order with the same `link`/`activeMatch` targets +// modulo the `/zh` locale prefix. No dependencies; Node >= 22. +// +// Usage: node scripts/check-locale-drift.mjs [--root ] [--help] + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HEADING_TOLERANCE = 2; +const SOURCE_LOCALE = 'en'; +const TARGET_LOCALE = 'zh'; +const SKIPPED_DIRS = new Set(['api']); +const GENERATED_PAGES = new Set([ + 'reference/hosts.md', + 'reference/events.md', + 'reference/notices.md', + 'reference/diagnostics.md', +]); +const HASH_COMMENT_LANGS = new Set(['sh', 'bash', 'shell', 'zsh']); +const SLASH_COMMENT_LANGS = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json', 'jsonc']); +// Tree listings (`text` fences) annotate entries with a trailing ` # note`; +// only that trailing form is stripped there, the lines themselves compare verbatim. +const TRAILING_HASH_ONLY_LANGS = new Set(['text']); +// Twoslash directives are comments syntactically but change what renders. +const TWOSLASH_DIRECTIVE = /^\s*\/\/\s*(?:---cut|@|\^\?|\^\|)/; +// `/* … */` preceded by whitespace or line start; a glob like `src/**/*.ts` is left alone. +const BLOCK_COMMENT = /(?<=^|\s)\/\*[\s\S]*?\*\//gm; +const TRANSLATED_KEYS = new Set(['text', 'label']); +const LOCALE_PREFIXED_KEYS = new Set(['link', 'activeMatch']); + +const usage = `Usage: node scripts/check-locale-drift.mjs [--root ] + +Compares every authored ${SOURCE_LOCALE}/ page and _meta.json/_nav.json with its ${TARGET_LOCALE}/ twin. + --root docs root holding ${SOURCE_LOCALE}/ and ${TARGET_LOCALE}/ (default: website/docs) + --help print this message`; + +const parseArgs = argv => { + const options = { root: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'docs') }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--help' || arg === '-h') return { help: true }; + if (arg === '--root') { + const value = argv[index + 1]; + if (!value) throw new Error('--root requires a directory'); + options.root = path.resolve(value); + index += 1; + continue; + } + if (arg.startsWith('--root=')) { + options.root = path.resolve(arg.slice('--root='.length)); + continue; + } + throw new Error(`unknown argument: ${arg}`); + } + return options; +}; + +const toPosix = relative => relative.split(path.sep).join('/'); + +/** Sorted relative paths of every file under `dir` matching `matches`, skipping SKIPPED_DIRS at any depth. */ +const walk = (dir, matches, prefix = '') => { + const found = []; + if (!fs.existsSync(dir)) return found; + const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en')); + for (const entry of entries) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + if (SKIPPED_DIRS.has(entry.name)) continue; + found.push(...walk(path.join(dir, entry.name), matches, relative)); + } else if (matches(entry.name)) { + found.push(relative); + } + } + return found.sort(); +}; + +const isPage = name => /\.mdx?$/.test(name); +const isMetaFile = name => name === '_meta.json' || name === '_nav.json'; + +/** Drops comment-only lines and trailing ` # …` / ` // …` comments for the languages that have them. */ +const normalizeCodeLine = (line, lang) => { + if (HASH_COMMENT_LANGS.has(lang)) { + if (/^\s*#(?!!)/.test(line)) return ''; + return line.replace(/\s+#.*$/, '').trimEnd(); + } + if (TRAILING_HASH_ONLY_LANGS.has(lang)) return line.replace(/\s+#.*$/, '').trimEnd(); + if (SLASH_COMMENT_LANGS.has(lang)) { + if (TWOSLASH_DIRECTIVE.test(line)) return line.trimEnd(); + if (/^\s*\/\//.test(line)) return ''; + return line.replace(/\s+\/\/.*$/, '').trimEnd(); + } + return line.trimEnd(); +}; + +/** The comparable lines of a fence: block comments removed, then each line normalized, blanks dropped. */ +const comparableLines = fence => { + const body = fence.raw.join('\n'); + const stripped = SLASH_COMMENT_LANGS.has(fence.lang) ? body.replace(BLOCK_COMMENT, '') : body; + return stripped + .split('\n') + .map(line => normalizeCodeLine(line, fence.lang)) + .filter(line => line.trim() !== ''); +}; + +/** + * Splits a page into the facts the twin must share. Fenced blocks are tracked + * so a `# comment` in a shell fence is not counted as a heading and a `| a |` + * line inside a text fence is not counted as a table row. + */ +const analyzePage = source => { + const lines = source.split(/\r?\n/); + const fences = []; + let headings = 0; + let tableRows = 0; + let open = null; + let inFrontmatter = lines[0] === '---'; + for (let index = inFrontmatter ? 1 : 0; index < lines.length; index += 1) { + const line = lines[index]; + if (inFrontmatter) { + if (line === '---') inFrontmatter = false; + continue; + } + if (open) { + const close = new RegExp(`^\\s*${open.marker[0]}{${open.marker.length},}\\s*$`); + if (close.test(line)) { + open = null; + continue; + } + open.raw.push(line.startsWith(open.indent) ? line.slice(open.indent.length) : line); + continue; + } + const opener = /^(\s*)(`{3,}|~{3,})([^`\s]*)\s*(.*)$/.exec(line); + if (opener) { + const info = `${opener[3]} ${opener[4]}`.trim().replace(/\s+/g, ' '); + open = { indent: opener[1], marker: opener[2], lang: opener[3], info, raw: [] }; + fences.push(open); + continue; + } + if (/^#{2,3}\s+\S/.test(line)) headings += 1; + if (/^\s*\|.*\|\s*$/.test(line)) tableRows += 1; + } + const codes = new Set(source.match(/\bAB\d{4}\b/g) ?? []); + return { + fences: fences.map(fence => ({ info: fence.info, lang: fence.lang, lines: comparableLines(fence) })), + headings, + tableRows, + codes, + }; +}; + +const setDifference = (a, b) => [...a].filter(item => !b.has(item)).sort(); + +const comparePages = (relative, en, zh, failures) => { + const fail = message => failures.push(`${SOURCE_LOCALE}/${relative}: ${message}`); + if (en.fences.length !== zh.fences.length) { + fail(`fenced code block count differs (en=${en.fences.length}, zh=${zh.fences.length})`); + } else { + en.fences.forEach((fence, index) => { + const twin = zh.fences[index]; + const label = `fence #${index + 1} (${fence.info || 'no language'})`; + if (fence.info !== twin.info) { + fail(`${label} info string differs (en=${JSON.stringify(fence.info)}, zh=${JSON.stringify(twin.info)})`); + return; + } + if (fence.lines.length !== twin.lines.length) { + fail(`${label} line count differs after comment stripping (en=${fence.lines.length}, zh=${twin.lines.length})`); + return; + } + const at = fence.lines.findIndex((line, lineIndex) => line !== twin.lines[lineIndex]); + if (at !== -1) { + fail(`${label} code differs at code line ${at + 1} (en=${JSON.stringify(fence.lines[at])}, zh=${JSON.stringify(twin.lines[at])})`); + } + }); + } + const onlyEn = setDifference(en.codes, zh.codes); + const onlyZh = setDifference(zh.codes, en.codes); + if (onlyEn.length > 0 || onlyZh.length > 0) { + fail(`diagnostic code set differs (en=${onlyEn.length ? `+${onlyEn.join(' ')}` : '='}, zh=${onlyZh.length ? `+${onlyZh.join(' ')}` : '='})`); + } + if (en.tableRows !== zh.tableRows) { + fail(`table row count differs (en=${en.tableRows}, zh=${zh.tableRows})`); + } + if (Math.abs(en.headings - zh.headings) > HEADING_TOLERANCE) { + fail(`h2/h3 heading count differs by more than ${HEADING_TOLERANCE} (en=${en.headings}, zh=${zh.headings})`); + } +}; + +/** Structural shape of a _meta.json/_nav.json value: translated labels removed, locale prefix stripped from links. */ +const shapeOf = (value, locale) => { + if (Array.isArray(value)) return value.map(item => shapeOf(item, locale)); + if (value && typeof value === 'object') { + const shaped = {}; + for (const [key, item] of Object.entries(value)) { + if (TRANSLATED_KEYS.has(key)) continue; + // `activeMatch` is a regex, so the locale prefix may open each `|` alternative. + shaped[key] = + LOCALE_PREFIXED_KEYS.has(key) && typeof item === 'string' && locale !== SOURCE_LOCALE + ? item + .split('|') + .map(branch => branch.replace(new RegExp(`^/${locale}(?=/|$)`), '') || '/') + .join('|') + : shapeOf(item, locale); + } + return shaped; + } + return value; +}; + +const compareMeta = (relative, enSource, zhSource, failures) => { + const fail = message => failures.push(`${SOURCE_LOCALE}/${relative}: ${message}`); + let en; + let zh; + try { + en = JSON.parse(enSource); + zh = JSON.parse(zhSource); + } catch (error) { + fail(`invalid JSON (${error.message})`); + return; + } + const enShape = JSON.stringify(shapeOf(en, SOURCE_LOCALE), null, 0); + const zhShape = JSON.stringify(shapeOf(zh, TARGET_LOCALE), null, 0); + if (enShape === zhShape) return; + const enEntries = Array.isArray(en) ? en : [en]; + const zhEntries = Array.isArray(zh) ? zh : [zh]; + if (enEntries.length !== zhEntries.length) { + fail(`entry count differs (en=${enEntries.length}, zh=${zhEntries.length})`); + return; + } + const at = enEntries.findIndex( + (entry, index) => JSON.stringify(shapeOf(entry, SOURCE_LOCALE)) !== JSON.stringify(shapeOf(zhEntries[index], TARGET_LOCALE)), + ); + fail( + `entry ${at + 1} differs in keys, order, or link targets (en=${JSON.stringify(shapeOf(enEntries[at], SOURCE_LOCALE))}, zh=${JSON.stringify(shapeOf(zhEntries[at], TARGET_LOCALE))})`, + ); +}; + +const main = () => { + const options = parseArgs(process.argv.slice(2)); + if (options.help) { + console.log(usage); + return; + } + const enRoot = path.join(options.root, SOURCE_LOCALE); + const zhRoot = path.join(options.root, TARGET_LOCALE); + if (!fs.existsSync(enRoot) || !fs.existsSync(zhRoot)) { + throw new Error(`${options.root} must contain ${SOURCE_LOCALE}/ and ${TARGET_LOCALE}/`); + } + const failures = []; + const pages = walk(enRoot, isPage).filter(relative => !GENERATED_PAGES.has(toPosix(relative))); + for (const relative of pages) { + const twin = path.join(zhRoot, relative); + if (!fs.existsSync(twin)) { + failures.push(`${SOURCE_LOCALE}/${relative}: missing ${TARGET_LOCALE}/ twin (en=present, zh=absent)`); + continue; + } + comparePages(relative, analyzePage(fs.readFileSync(path.join(enRoot, relative), 'utf8')), analyzePage(fs.readFileSync(twin, 'utf8')), failures); + } + const metaFiles = walk(enRoot, isMetaFile); + for (const relative of metaFiles) { + const twin = path.join(zhRoot, relative); + if (!fs.existsSync(twin)) { + failures.push(`${SOURCE_LOCALE}/${relative}: missing ${TARGET_LOCALE}/ twin (en=present, zh=absent)`); + continue; + } + compareMeta(relative, fs.readFileSync(path.join(enRoot, relative), 'utf8'), fs.readFileSync(twin, 'utf8'), failures); + } + for (const failure of failures.sort()) console.log(failure); + if (failures.length > 0) { + console.log(`locale drift: ${failures.length} failure(s) across ${pages.length} page pair(s) and ${metaFiles.length} meta file(s) under ${options.root}`); + process.exitCode = 1; + return; + } + console.log(`locale drift: 0 failures across ${pages.length} page pair(s) and ${metaFiles.length} meta file(s) under ${options.root}`); +}; + +try { + main(); +} catch (error) { + console.error(`check-locale-drift: ${error.message}`); + process.exitCode = 2; +} diff --git a/website/styles/index.css b/website/styles/index.css index bacf35e18..dd12bbe2f 100644 --- a/website/styles/index.css +++ b/website/styles/index.css @@ -41,6 +41,25 @@ ); } +/* + * Desktop doc layout. Rspress sizes the doc column as + * 100vw − sidebar − outline − 2 × content padding (`layout/DocLayout/index.css`), + * which at 1440 × 900 leaves a 686 px code scroller: 81 monospace columns at + * the theme's 14 px code size, short of the ~90 columns the code samples wrap + * at (AGENTS.md). Trimming the three tokens widens the column to 824 px, so a + * 90-column line fits without a horizontal scrollbar once Shiki's 20 px line + * padding is paid. `html:root` outranks the theme's plain `:root` declaration + * regardless of stylesheet order; the ≤1279 px breakpoints keep the theme's + * own values. + */ +@media (width >= 1280px) { + html:root { + --rp-sidebar-width: 280px; + --rp-outline-width: 240px; + --rp-content-padding-x: 48px; + } +} + /* * Bounded homepage accent. The default theme already brand-colors the feature * card border on hover, so this only tints the icon chip and adds a small lift. diff --git a/website/theme/index.tsx b/website/theme/index.tsx index d0b0a08fa..e3cf0c65f 100644 --- a/website/theme/index.tsx +++ b/website/theme/index.tsx @@ -1,6 +1,18 @@ import { MDXProvider } from '@mdx-js/react'; -import { Content } from '@rspress/core/runtime'; -import { Layout as BasicLayout, getCustomMDXComponent } from '@rspress/core/theme-original'; +import { Content, useI18n, usePageData, withBase } from '@rspress/core/runtime'; +import { + EditLink as BasicEditLink, + Layout as BasicLayout, + LlmsCopyRow as BasicLlmsCopyRow, + LlmsHint as BasicLlmsHint, + LlmsOpenRow as BasicLlmsOpenRow, + NotFoundLayout as BasicNotFoundLayout, + IconEdit, + Link, + SvgWrapper, + getCustomMDXComponent, +} from '@rspress/core/theme-original'; +import { type JSX, useEffect } from 'react'; /** * The default `HomeLayout` renders only the frontmatter hero and feature cards @@ -21,5 +33,108 @@ const HomeBody = () => ( const Layout = () => } />; -export { Layout }; +/** + * Same value as `repositoryUrl` in `rspress.config.ts`. The config runs in + * Node and this module runs in the browser, so the constant is restated here + * rather than imported. + */ +const repositoryUrl = 'https://github.com/ScriptedAlchemy/agent-bundle'; + +/** + * Generated pages have no source under `website/docs`, so the default edit + * link (`docRepoBaseUrl` + `page._relativePath`) would point at a file that + * only exists in a build. These patterns must track `.gitignore` lines 20–27: + * TypeDoc output under `/api/`, and the reference pages rendered from + * `packages/agent-bundle/src/adapters/capabilities/*.json` and + * `docs/diagnostics.md`. + */ +const generatedApiPage = /^(en|zh)\/api\//; +const generatedCapabilityPage = /^(en|zh)\/reference\/(hosts|events|notices)\.md$/; +const generatedDiagnosticsPage = /^(en|zh)\/reference\/diagnostics\.md$/; +const capabilitiesSourceUrl = `${repositoryUrl}/tree/main/packages/agent-bundle/src/adapters/capabilities`; +const diagnosticsSourceUrl = `${repositoryUrl}/blob/main/docs/diagnostics.md`; + +/** Source location for a generated reference page, or `null` when it is authored. */ +const generatedSourceUrl = (relativePath: string): string | null => { + if (generatedCapabilityPage.test(relativePath)) return capabilitiesSourceUrl; + if (generatedDiagnosticsPage.test(relativePath)) return diagnosticsSourceUrl; + return null; +}; + +/** + * `EditLink` from the default theme builds its href unconditionally. This + * wrapper hides it on TypeDoc pages, points the generated reference pages at + * the source they are rendered from, and defers to the original everywhere + * else. Both the outline and the doc footer import `EditLink` from + * `@rspress/core/theme`, so this named export replaces it site-wide. + */ +const EditLink = ({ isOutline }: { isOutline?: boolean }) => { + const { page, siteData } = usePageData(); + const text = useI18n()('editLinkText'); + const relativePath = typeof page._relativePath === 'string' ? page._relativePath.replace(/\\/g, '/') : ''; + if (generatedApiPage.test(relativePath)) return null; + const sourceUrl = generatedSourceUrl(relativePath); + if (sourceUrl === null) return ; + // Same disable conditions as the original `useEditLink`. + if (!siteData.themeConfig?.editLink?.docRepoBaseUrl || !text) return null; + if (isOutline) { + return ( + + + {text} + + ); + } + return ( + + {text} + + ); +}; + +/** + * Both `pluginLlms` entries in `rspress.config.ts` exclude `/api/`, so the + * generated API routes have no Markdown twin and the copy / open-in-chat + * actions (and the hidden agent hint) would 404. Mirrors that `exclude`. + * + * The outline imports the two rows from `@rspress/core/theme`, so these + * exports replace them. The hint the runtime injects on every page + * (`runtime/App.js`) imports `LlmsHint` from `theme-original` and bypasses + * this file; only theme and MDX consumers get the guarded version. + */ +const hasMarkdownTwin = (routePath: string): boolean => !routePath.includes('/api/'); + +const LlmsCopyRow = () => { + const { page } = usePageData(); + return hasMarkdownTwin(page.routePath) ? : null; +}; + +const LlmsOpenRow = () => { + const { page } = usePageData(); + return hasMarkdownTwin(page.routePath) ? : null; +}; + +const LlmsHint = (): string | JSX.Element | null => { + const { page } = usePageData(); + return hasMarkdownTwin(page.routePath) ? : null; +}; + +/** + * `route.cleanUrls` emits `quick-start.html`, so on GitHub Pages a trailing + * slash (`…/quick-start/`) is a 404 while `…/quick-start` resolves. Retry + * without the slash. Directory index routes (`…/guide/start/`) are served as + * `index.html` and never reach this layout, and the stripped path never ends + * with a slash, so the redirect cannot loop. + */ +const NotFoundLayout = () => { + useEffect(() => { + const { pathname, search, hash } = window.location; + if (pathname.length > withBase('/').length && pathname.endsWith('/')) { + window.location.replace(pathname.replace(/\/+$/, '') + search + hash); + } + }, []); + return ; +}; + +export { EditLink, Layout, LlmsCopyRow, LlmsHint, LlmsOpenRow, NotFoundLayout }; export * from '@rspress/core/theme-original'; diff --git a/website/tsconfig.typedoc.json b/website/tsconfig.typedoc.json index 1eb47736f..c7ddc4897 100644 --- a/website/tsconfig.typedoc.json +++ b/website/tsconfig.typedoc.json @@ -3,10 +3,14 @@ // build must not depend on `pnpm build`: `packages/agent-bundle/src` imports // `@agent-bundle/runtime`, whose declarations only exist after that package // is compiled. Resolving those specifiers to `packages/rsc-runtime/src` - // keeps `pnpm docs:site:build` self-sufficient, mirroring the source-mapped `paths` - // the Twoslash plugin uses for the same reason. The runtime's own workspace + // keeps `pnpm docs:site:build` self-sufficient. The runtime's own workspace // dependency, `rsc-markdown-stream`, publishes its declarations from `dist` // too, so it resolves to the hand-written source declaration file. + // + // `paths` is also the source of truth for the Twoslash plugin: + // `rspress.config.ts` reads this file and passes the same map (made + // absolute) as twoslash `compilerOptions.paths`, so a new source-mapped + // specifier is added here once and both compilers see it. "extends": "../packages/agent-bundle/tsconfig.build.json", "compilerOptions": { // Widened from the package's `src` so the resolved runtime sources are