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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/official-rsbuild-plugins-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'agent-bundle': patch
---

Document the framework-owned Rsbuild plugin set (`@rsbuild/plugin-react`, `rsbuild:react`) and the official plugins a project may add through `tools.rsbuild.plugins` in the configuration reference; `packages/agent-bundle/src/build/framework-plugins.ts` exports that set for the plugin-collision diagnostic and a unit test derives it from the synthesized configs. Remove the standalone `lint:package` script and its CI step: publint already runs inside every publishable package's `rslib build` through `rsbuild-plugin-publint` at `throwOn: 'warning'`, so `lint:release` is now only `attw --pack --profile esm-only`. (#509)
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,10 @@ jobs:
fi
# Build first: checked-in suites and API tests import the package's built type
# declarations, so typecheck requires dist (same order as `pnpm check`).
# publint runs inside each publishable package's `rslib build`
# (rsbuild-plugin-publint, throwOn: 'warning'), so a manifest warning
# already fails this step; there is no separate publint step.
- run: pnpm build
- name: Package lint (publint)
run: pnpm lint:package
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm test
Expand Down
2 changes: 1 addition & 1 deletion docs/effect-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ soon as the trigger fires and retire the row.

| Recorded | Pin (where) | Observed registry state | Trigger / action |
| --- | --- | --- | --- |
| 2026-09-03 | `@rslib/core` **`0.23.2`** — root, `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies. Stays on `0.23.x` until rslib 1.0 leaves rc. | `npm view @rslib/core dist-tags`: `latest` `0.23.2`, `rc` `1.0.0-rc.2`, `beta` `1.0.0-beta.3`, `canary` `0.20.0-canary-202603101`. | `latest` becomes `1.x`. Bump all four pins in one chore; re-run `pnpm build`, `lint:package`, `check:release`, and the Rslib-driven compile tests. |
| 2026-09-03 | `@rslib/core` **`0.23.2`** — root, `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies. Stays on `0.23.x` until rslib 1.0 leaves rc. | `npm view @rslib/core dist-tags`: `latest` `0.23.2`, `rc` `1.0.0-rc.2`, `beta` `1.0.0-beta.3`, `canary` `0.20.0-canary-202603101`. | `latest` becomes `1.x`. Bump all four pins in one chore; re-run `pnpm build` (publint runs inside it), `check:release`, and the Rslib-driven compile tests. |
| 2026-09-03 | `effect-rstest` **pkg.pr.new preview `e5f8d5f`** (`https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f`) — `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies (three pins). Needs a real release pin once published. | `npm view effect-rstest versions`: **E404 — not published to npm** (no versions, no dist-tags). | First npm publish of `effect-rstest`. Replace all three preview URLs with the exact published version, refresh `pnpm-lock.yaml`, re-run `pnpm test:unit` (`it.effect` / `it.live` suites). |
| 2026-09-03 | `effect` **`4.0.0-rc.112`** (`packages/agent-bundle`, `packages/rsc-runtime`, `packages/workbench`, `packages/create-agent-bundle`), `@effect/atom-react` `4.0.0-rc.112` (`packages/workbench`), `@effect/platform-node` `4.0.0-rc.112` (`packages/create-agent-bundle`), `@effect/language-service` `0.87.2` and `@effect/tsgo` `0.39.0` (root). Auto re-pin in lockstep + `repos/effect` subtree + Workbench atom phase 4 unblock (stream-backed derived atoms) once the post-rc.112 disposal fix ships. | `npm view effect dist-tags`: `rc` **`4.0.0-rc.112`** (unchanged), `beta` `4.0.0-beta.107`, `latest` `3.22.1`. `@effect/atom-react`: `rc` `4.0.0-rc.112`. `@effect/language-service`: `latest` `0.87.2`. `@effect/tsgo`: `latest` `0.39.1` (patch ahead of the `0.39.0` pin; rides the lockstep chore). | `effect@rc` advances past `4.0.0-rc.112`. Run the re-pin chore steps 1–6 above, bumping `effect`, `@effect/atom-react`, `@effect/language-service`, and `@effect/tsgo` together, then lift the stream-backed derived-atom ban in the Workbench if the disposal fix is in the new RC. |
| 2026-09-03 | Agent Plugins specification **`1.0.0`** — `packages/agent-bundle/src/adapters/schemas/portable/{plugin,mcp}.schema.json` + `PROVENANCE.json` (spec repo `agentplugins/agent-plugins-spec` @ `ff8ab5e392cc87bd88d87c060815a87490e51003`, 2026-08-19), portable `adapterRevision` `1.8.0`, pins in `tests/adapter-metadata.test.ts`. Spec watch for #426; not an npm pin, so re-verify with `curl`/`gh api`, not `npm view`. | Live `https://agent-plugins.org/schemas/1.0.0/{plugin,mcp}.schema.json` rehash to the pinned sha256 (1805 / 3408 bytes). Repo `main` HEAD unchanged at the pinned commit; **no tags, no GitHub releases**. `spec/1.1.0.md` is "Status: Working Draft" (started 2026-08-15, `a2afd7ec`); in-repo `schemas/1.1.0/*.schema.json` differ from 1.0.0 only in the `$id`/`const`/`description` version strings; `https://agent-plugins.org/schemas/1.1.0/*.schema.json` → 404. Observed latest published version: **1.0.0**. | `spec/1.1.0.md` (or later) flips to "Published" **and** `agent-plugins.org/schemas/<version>/` serves both schemas. Re-pin under `schemas/portable/` with a dated `PROVENANCE.json` (sha/bytes/date/commit), bump the portable `adapterRevision`, refresh the metadata pins, run `pnpm test:unit` (portable adapter + plugin-validation suites) and `pnpm test:host-install:build`, and add a capability row per additive field. |
2 changes: 1 addition & 1 deletion docs/local-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ only `verify (24)` runs hosted.

| Local leg | Node | Steps | Mirrors hosted job |
| --- | --- | --- | --- |
| `verify-node22` | 22.19.x | `install`, `playwright install chrome`, `build`, `lint:package`, `typecheck`, `lint`, `test:unit`, `test:integration` | `verify (22.19.0)` |
| `verify-node22` | 22.19.x | `install`, `playwright install chrome`, `build` (publint runs inside each package's `rslib build`), `typecheck`, `lint`, `test:unit`, `test:integration` | `verify (22.19.0)` |
| `verify-node24` | 24.x | same | `verify (24)` |
| `verify-node26` | 26.x | same | `verify (26)` |
| `gates-node22` | 22.19.x | `install`, `examples:check`, `check:release`, `eval:spot` | `examples-check`, `release-gates`, `rsc-runtime-micro-eval` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ In `package.json`, remove `workspaces`, add `"packageManager": "pnpm@11.23.0"`,
"test:packed:native:claude": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CLAUDE_SMOKE=1 pnpm test:packed:native",
"test:packed:native:codex": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CODEX_SMOKE=1 pnpm test:packed:native",
"pack:dry-run": "pnpm build && npm pack ./packages/agent-bundle --dry-run --json",
"lint:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle",
"lint:release": "attw --pack --profile esm-only packages/agent-bundle",
"check:release": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,6 @@ Expected: zero failures and zero lint warnings.
```bash
pnpm check:release
pnpm test:packed
pnpm lint:package
```

Expected: pack dry-run, publint, ATTW, and packed Workbench tests all pass.
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"packageManager": "pnpm@11.23.0",
"scripts": {
"build": "pnpm --filter @agent-bundle/runtime build && pnpm --filter agent-bundle build && pnpm --filter create-agent-bundle build",
"lint:package": "publint packages/agent-bundle && publint packages/rsc-runtime && publint packages/create-agent-bundle",
"test": "pnpm test:unit && pnpm test:route-unit && pnpm test:projection && pnpm test:integration",
"test:unit": "rstest --config rstest.unit.config.ts",
"test:route-unit": "rstest --config rstest.route-unit.config.ts",
Expand Down Expand Up @@ -49,7 +48,7 @@
"release": "pnpm check:release && changeset publish",
"preview:publish": "pkg-pr-new publish --previewVersion --peerDeps --no-compact --no-template './packages/agent-bundle' './packages/rsc-runtime' './packages/create-agent-bundle'",
"pack:dry-run": "pnpm build && npm pack ./packages/agent-bundle --dry-run --json",
"lint:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle",
"lint:release": "attw --pack --profile esm-only packages/agent-bundle",
"check:release": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed:release",
"check:release:ci": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed",
"example:hooks": "pnpm build && pnpm --filter @agent-bundle-example/hooks-and-scripts dev",
Expand Down Expand Up @@ -81,7 +80,6 @@
"pkg-pr-new": "0.0.88",
"playwright": "1.62.1",
"playwright-core": "1.62.1",
"publint": "0.3.24",
"rsbuild-plugin-publint": "1.0.0",
"typescript": "7.0.2"
}
Expand Down
67 changes: 67 additions & 0 deletions packages/agent-bundle/tests/framework-plugin-registration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from '@rstest/core';

import { frameworkOwnedRsbuildPlugins } from '../src/build/framework-plugins.ts';
import { composeMcpAppsRsbuildConfig } from '../src/build/mcp-apps.ts';
import { composeEntryLibConfig, type RslibEntry } from '../src/build/rslib.ts';
import type { AgentBundleMeta } from '../src/meta.ts';

const meta: AgentBundleMeta = Object.freeze({
name: 'registration-fixture',
packageName: undefined,
packageVersion: undefined,
version: '1.0.0',
});

const entry: RslibEntry = Object.freeze({
name: 'tool',
outputRelativePath: 'scripts/tool.mjs',
source: '/project/src/tool.ts',
sourceInputs: ['/project/src/tool.ts'],
});

const reactView = Object.freeze({ name: 'dashboard', source: '/project/src/apps/dashboard.tsx', template: undefined });
const plainView = Object.freeze({ name: 'status', source: '/project/src/apps/status.ts', template: undefined });

const byName = (left: string, right: string): number => left.localeCompare(right);

const registeredPluginNames = (plugins: unknown): readonly string[] => (Array.isArray(plugins) ? plugins : [])
.flatMap((plugin: unknown) => (
typeof plugin === 'object' && plugin !== null && typeof (plugin as { readonly name?: unknown }).name === 'string'
? [(plugin as { readonly name: string }).name]
: []
))
.sort(byName);

/**
* `frameworkOwnedRsbuildPlugins` is the set the AB4724 collision diagnostic
* and the `tools.rsbuild` reference page describe as registered by the
* framework. This suite derives that set from the configs the framework
* actually synthesizes, so a plugin added to (or dropped from) a profile
* without updating the registry fails here rather than in a consumer's
* build.
*/
describe('framework-owned Rsbuild plugin registry', () => {
const owned = [...frameworkOwnedRsbuildPlugins.keys()].sort(byName);

it('matches exactly the plugins every synthesized entry lib registers', () => {
const lib = composeEntryLibConfig(entry, { meta, outputRoot: '/staged/portable' });
expect(registeredPluginNames(lib.plugins)).toEqual(owned);
});

it('matches exactly the plugins a React-syntax MCP App view registers, and nothing for a plain view', () => {
const apps = composeMcpAppsRsbuildConfig([reactView, plainView], { meta, outDir: '/staged/portable' });
expect(registeredPluginNames(apps.environments?.[reactView.name]?.plugins)).toEqual(owned);
expect(registeredPluginNames(apps.environments?.[plainView.name]?.plugins)).toEqual([]);
// The framework registers plugins per environment, never at the root the
// consumer's `tools.rsbuild.plugins` merges into.
expect(apps.plugins).toBeUndefined();
});

it('registers no framework-owned plugin twice on either path', () => {
const lib = composeEntryLibConfig(entry, { meta, outputRoot: '/staged/portable' });
const apps = composeMcpAppsRsbuildConfig([reactView], { meta, outDir: '/staged/portable' });
for (const names of [registeredPluginNames(lib.plugins), registeredPluginNames(apps.environments?.[reactView.name]?.plugins)]) {
expect(new Set(names).size).toBe(names.length);
}
});
});
89 changes: 89 additions & 0 deletions packages/agent-bundle/tests/publint-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { createRslib, type RslibConfig } from '@rslib/core';
import { afterEach, describe, expect, it } from '@rstest/core';
import { pluginPublint } from 'rsbuild-plugin-publint';

import agentBundleConfig from '../rslib.config.ts';
import createAgentBundleConfig from '../../create-agent-bundle/rslib.config.ts';
import rscRuntimeConfig from '../../rsc-runtime/rslib.config.ts';

/**
* publint is not a separate CI step: every publishable package's `rslib
* build` runs it through rsbuild-plugin-publint and fails on a warning. This
* suite is the proof behind removing the standalone `publint <dir>` script —
* the plugin is registered where the packages build, and a warning-level
* finding rejects the build the way the CI job expects.
*/

const publintPluginName = 'plugin-publint';

const pluginNames = (config: RslibConfig): readonly string[] =>
(config.plugins ?? []).flatMap((plugin) => (
typeof plugin === 'object' && plugin !== null && 'name' in plugin && typeof plugin.name === 'string'
? [plugin.name]
: []
));

describe('publint build gate', () => {
it('is registered in every publishable package build', () => {
for (const config of [agentBundleConfig, createAgentBundleConfig, rscRuntimeConfig]) {
expect(pluginNames(config)).toContain(publintPluginName);
}
});

const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true })));
});

const probePackage = async (manifest: Readonly<Record<string, unknown>>): Promise<string> => {
const root = await realpath(await mkdtemp(join(tmpdir(), 'agent-bundle-publint-gate-')));
roots.push(root);
await mkdir(join(root, 'src'), { recursive: true });
await writeFile(join(root, 'src', 'index.ts'), 'export const probe = 1;\n');
await writeFile(join(root, 'package.json'), `${JSON.stringify({
name: 'publint-gate-probe',
private: true,
type: 'module',
version: '0.0.0',
...manifest,
}, null, 2)}\n`);
return root;
};

const buildProbe = async (root: string): Promise<void> => {
const rslib = await createRslib({
config: {
lib: [{ bundle: true, format: 'esm', syntax: 'es2022' }],
logLevel: 'silent',
output: { filenameHash: false, target: 'node' },
performance: { buildCache: false },
// The exact registration the three package configs use.
plugins: [pluginPublint({ throwOn: 'warning' })],
root,
source: { entry: { index: './src/index.ts' } },
},
cwd: root,
});
const result = await rslib.build();
await result.close();
};

it('fails the build on a warning-level publint finding', async () => {
// `main` beside an `exports` map that never names "." is publint's
// EXPORTS_MISSING_ROOT_ENTRYPOINT, a warning rather than an error.
const root = await probePackage({
exports: { './probe': './dist/index.js' },
main: './dist/index.js',
});
await expect(buildProbe(root)).rejects.toThrow(/publint failed/iu);
});

it('passes a clean manifest', async () => {
const root = await probePackage({ exports: { '.': './dist/index.js' } });
await expect(buildProbe(root)).resolves.toBeUndefined();
});
});
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rstest.integration-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const integrationTestFiles: readonly string[] = [
'packages/agent-bundle/tests/prepack.test.ts',
'packages/agent-bundle/tests/provider-typegen.test.ts',
'packages/agent-bundle/tests/public-api.test.ts',
'packages/agent-bundle/tests/publint-gate.test.ts',
'packages/agent-bundle/tests/route-register-typegen.test.ts',
'packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts',
'packages/agent-bundle/tests/rstest-meta-consumer.test.ts',
Expand Down
4 changes: 2 additions & 2 deletions scripts/local-ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*
* Hosted job → local leg mapping:
* - verify (Node 22.19 / 24 / 26) → legs `verify-node22|24|26`: install,
* playwright chrome, build, lint:package, typecheck, lint, test:unit,
* playwright chrome, build (publint runs inside it), typecheck, lint, test:unit,
* test:integration — the same package scripts in the same order.
* - examples-check + release-gates + rsc-runtime-micro-eval (all Node 22.19)
* → leg `gates-node22`: examples:check, check:release, eval:spot run
Expand Down Expand Up @@ -357,8 +357,8 @@ const main = async () => {
// apt/root-only and the OS packages are a one-time machine setup, so the
// local step installs/validates the browser only.
{ id: 'browsers', hostedJob: 'verify', command: ['pnpm', 'exec', 'playwright', 'install', 'chrome'] },
// publint runs inside each package's `rslib build` (rsbuild-plugin-publint).
{ id: 'build', hostedJob: 'verify', command: ['pnpm', 'build'] },
{ id: 'lint:package', hostedJob: 'verify', command: ['pnpm', 'lint:package'] },
{ id: 'typecheck', hostedJob: 'verify', command: ['pnpm', 'typecheck'] },
{ id: 'lint', hostedJob: 'verify', command: ['pnpm', 'lint'] },
{ id: 'test:unit', hostedJob: 'verify', command: ['pnpm', 'test:unit', ...unitCapArguments] },
Expand Down
8 changes: 5 additions & 3 deletions website/docs/en/guide/distribution/preview-packages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ Before that path is enabled, the release owner has to resolve two things: the fi
and license, and the repository-wide `"access": "restricted"` policy for `agent-bundle`, which is
not currently overridden with `publishConfig.access`.

`pnpm release` runs the release gate — `pnpm pack:dry-run`, `pnpm lint:release`, and
`pnpm test:packed:release` — before publishing. That gate is release-only and does not replace the
ordinary `pnpm check` delivery gate.
`pnpm release` runs the release gate — `pnpm pack:dry-run`, `pnpm lint:release` (`attw` on the
packed `agent-bundle` tarball with the `esm-only` profile), and `pnpm test:packed:release` — before
publishing. publint is not a separate gate: every publishable package's `rslib build` runs it
through `rsbuild-plugin-publint` and fails the build on a warning. That gate is release-only and
does not replace the ordinary `pnpm check` delivery gate.

## Next

Expand Down
Loading
Loading