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
15 changes: 15 additions & 0 deletions .changeset/cursor-contract-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"agent-bundle": patch
---

Add the `cursor.*` config extension so Cursor builds emit manifest metadata
(`author`, `homepage`, `repository`, `license`, `keywords`, `publisher`,
`category`, `tags`, `minClientVersions`) into `.cursor-plugin/plugin.json`;
invalid values are reported as `cursor.manifest.field.unknown`,
`cursor.manifest.author.*`, and `cursor.manifest.<field>.invalid` instead of a
generic schema failure. Cursor `subagentStart`/`subagentStop` hooks now
validate the documented envelope (every field except `git_branch` required),
decode `subagent_model` into `event.model`, and only return a
`followup_message` when the subagent `status` is `completed`; the `cursor` and
`plugin` capability reports list every documented Cursor hook event with its
cloud availability. (#375)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Claude Code language servers are declared under `claude.lspServers`; the `claude

Claude Code plugin defaults are declared under `claude.settings` and emitted as plugin-root `settings.json`, which Claude Code applies when the plugin is enabled. The pinned contract supports only `agent` and `subagentStatusLine`; Agent Bundle rejects any other key rather than shipping a default Claude Code would silently ignore, and it expands no path tokens here because `settings.json` is absent from the host's placeholder-substitution table. Because the plugin `agents/` component is still deferred, declaring `agent` also raises a warning: the referenced agent has to reach the plugin root some other way, such as a prebuilt payload.

Cursor Plugin manifest metadata is declared under `cursor.*` and emitted verbatim into `.cursor-plugin/plugin.json` by the `cursor` target and the Cursor half of `plugin`: `author` (`name`, optional `email`), `homepage`, `repository`, `license`, `keywords`, plus the schema-admitted `publisher`, `category`, `tags`, and `minClientVersions` (for example `{ cursor: '3.13.0' }`). Every field is validated against the pinned `cursor/plugins` manifest schema before emission — `author.url`, non-HTTP URLs, empty strings, and loose semver are rejected with `cursor.manifest.*` errors, and an invalid block emits no partial metadata. The Cursor artifact never mixes Agent Plugin (`plugin.json`, `${PLUGIN_ROOT}`) paths or tokens into the Cursor Plugin format; the portable target owns that format. The full documented-surface contract matrix (every Cursor hook event, cloud availability, hook options, marketplace and team-distribution surfaces, canvases, agents) lives as dated `supported` / `unavailable` rows in `packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json`.

The same config also owns the npm package build — no second bundler config, bin shims, or hand-rolled stdio lifecycles. `bin` and `lib` entries (or the conventions `src/cli.ts`, `src/index.ts`, and `src/mcp/<server-id>.ts`) emit executable `dist/bin/<name>.js` bundles and a library output alongside the host artifacts; an MCP entry that default-exports a server factory runs under a framework-owned stdio lifecycle; `tools.rsbuild` / `tools.rspack` is the one bundler escape hatch. [Entry conventions](docs/entry-conventions.md) is the full contract, and [Framework mode](docs/framework-mode.md) is the whole authoring model on one screen: structure in config and conventions (`src/skills/<name>/SKILL.md` ships with no declaration at all), JSX only where something is rendered.

## Commands
Expand Down
8 changes: 4 additions & 4 deletions docs/canvases/agent-bundle-walkthrough.canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -742,8 +742,8 @@ else try { output = await requestEventRuntime({ ... }); }
[<Text as="span" style={{ fontFamily: MONO }} size="small">tool/before</Text>, "PreToolUse", "PreToolUse", "preToolUse", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">tool/after</Text>, "PostToolUse", "PostToolUse", "postToolUse", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">stop</Text>, "Stop", "Stop", "stop", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">agent/start</Text>, "SubagentStart", "SubagentStart (adds turn_id, model, permission_mode)", "subagentStart", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">agent/stop</Text>, "SubagentStop", "SubagentStop", "subagentStop", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">agent/start</Text>, "SubagentStart", "SubagentStart (adds turn_id, model, permission_mode)", "subagentStart (subagent_id/subagent_type envelope; permission deny)", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">agent/stop</Text>, "SubagentStop", "SubagentStop", "subagentStop (status/loop_count envelope; followup_message)", "—"],
[<Text as="span" style={{ fontFamily: MONO }} size="small">workspace/open</Text>, "unavailable: no such event", "unavailable: no such event", "workspaceOpen (observe-only; optional pluginPaths return not modeled)", "unavailable: spec defines no hooks"],
]}
/>
Expand All @@ -758,8 +758,8 @@ else try { output = await requestEventRuntime({ ... }); }
["tool/before", "yes, reason required", "yes (when not denying)", "Claude + Codex; dropped on Cursor"],
["tool/after", "—", "—", "all hosts"],
["stop", "yes — keeps the agent working (Cursor: followup_message)", "—", "—"],
["agent/start", "no host can block subagent creation", "—", "all hosts"],
["agent/stop", "Claude + Codex (keeps subagent running); Cursor cannot", "—", "Claude + Cursor; Codex schema rejects it"],
["agent/start", "Cursor only (permission: deny + user_message); Claude + Codex cannot", "—", "Claude + Codex; Cursor documents no channel"],
["agent/stop", "Claude + Codex (decision: block keeps subagent running); Cursor (followup_message auto-continue)", "—", "Claude only; Codex schema and Cursor docs reject it"],
["workspace/open", "—", "—", "no channel — observation only (Cursor)"],
]}
/>
Expand Down
14 changes: 11 additions & 3 deletions examples/rsc-agent-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ Host/Origin allowlists mitigate DNS rebinding and cross-origin requests, but the
| `task/create` | Unavailable | `TaskCreated` (deny) | Unavailable |
| `task/complete` | Unavailable | `TaskCompleted` (observe-only; blocking is exit-code-only) | Unavailable |
| `agent/idle` | Unavailable | `TeammateIdle` (deny via continue:false) | Unavailable |
| `agent/start` | `subagentStart` | `SubagentStart` | `SubagentStart` |
| `agent/stop` | `subagentStop` | `SubagentStop` | `SubagentStop` |
| `agent/start` | `subagentStart` (deny via `permission: "deny"`; no context channel) | `SubagentStart` (context) | `SubagentStart` (context) |
| `agent/stop` | `subagentStop` (deny via `followup_message`; no context channel) | `SubagentStop` (deny + context) | `SubagentStop` (deny) |
| `workspace/open` | Supported (observe-only; native `pluginPaths` return not modeled) | Unavailable | Unavailable |

Cursor's native `workspaceOpen` is sessionless and its optional `pluginPaths`
Expand All @@ -260,7 +260,15 @@ block subagent creation. Their `agent/stop` routes can continue the subagent
with the native `decision: "block"` plus `reason` contract. Codex
`SubagentStop` exit-0 output is always JSON; its generated 0.147.0 output
schema has no `additionalContext` field, so the route projection rejects that
unsupported effect rather than silently fabricating one.
unsupported effect rather than silently fabricating one. Cursor inverts the
pair: its `subagentStart` envelope (`subagent_id`, `subagent_type`, `task`,
`parent_conversation_id`, `tool_call_id`, `subagent_model`,
`is_parallel_worker`) accepts `permission: "deny"` plus `user_message`, and its
`subagentStop` envelope (`status`, `loop_count`, `summary`, `modified_files`,
`agent_transcript_path`) accepts only `followup_message`, which Cursor consumes
when `status` is `completed` and caps with `loop_limit` (default 5). Neither
Cursor event documents an additional-context channel, so `Agent.Context` fails
closed on Cursor for both.
`session/end` and `prompt/submit` are event-route-only families and do not add
`config.hooks.sessionEnd` or `config.hooks.promptSubmit` handler keys.
`session/end` rejects every result effect because each native event is
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ observed host version (`observedVersion`) and target, while adapters carry a mon
`adapterRevision`. Git already versions repository-owned content; hashing it again inside the
repository is self-referential and causes churn on every table edit.

`AgentBundleConfig` merges bundled portable, Codex, and Claude declarations
`AgentBundleConfig` merges bundled portable, Codex, Claude, and Cursor declarations
through `AgentBundleConfigExtensions`. `TargetRegistry` owns the unique
extension descriptor and adapter for each target. Ordinary projects need no
extension key; extension values are strict finite JSON and host-specific values
Expand Down
Loading
Loading