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/canonical-component-kinds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'agent-bundle': minor
---

Report every canonical host component kind in `agent-bundle inspect` (`--json` and human output). Each inspection plan now carries a `kinds` matrix — `agent`, `cli`, `command`, `event-route`, `hook`, `lsp`, `mcp-app`, `mcp-server`, `native-diagnostics`, `native-extension`, `rule`, `script`, `skill` — with the target's own four-state judgment and selected/omitted counts, plus a `kinds this host cannot emit:` line, so a host with no LSP, diagnostics-provider, extension, or (G5-deferred) agent surface says so in its own dated words instead of by silence. Filesystem `src/events` routes now report as the `event-route` kind judged by the host's `event:<canonical event>` row instead of folding into `hook` (a breaking change to the `kind` and `name` fields of `inspect --json` for event routes), and `claude.lspServers` entries report as `lsp` components emitted by Claude and the composite `plugin` bundle and excluded elsewhere. The `AgentComponentKind` type and `componentKindCapability` helpers are exported from `agent-bundle/api`. No diagnostic codes change (#425)
25 changes: 19 additions & 6 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -781,9 +781,14 @@ agent-bundle inspect [--target <t>] [--json]
```

Every inspection plan accounts for each host component the project declares
— skills, commands, rules, hooks, MCP servers, MCP Apps, and scripts — as
either `selected` (emitted for that target) or `skipped` (omitted), in one
deterministic order. A skipped component names its cause: `excluded-by-targets`
— skills, commands, rules, config-declared hooks, filesystem event routes,
LSP servers, MCP servers, MCP Apps, and scripts — as either `selected`
(emitted for that target) or `skipped` (omitted), in one deterministic order.
Each component carries its canonical `kind` (`AgentComponentKind`); event
routes report as `event-route`, judged by the host's row for their canonical
event (`event:session/start`, …), separately from `hook`, and Claude-declared
`claude.lspServers` entries report as `lsp` against every target's `lsp` row.
A skipped component names its cause: `excluded-by-targets`
when the author's `targets` left the host out, or `unsupported-capability` when
the host's pinned capability table does not support the surface. Components
that need a host capability carry that target's own four-state judgment as
Expand All @@ -792,9 +797,17 @@ or `{ name, state: 'degraded' | 'unavailable' | 'prohibited', reason }` — so
the JSON explains why a Cursor rule is absent from a Claude bundle in the
host's words rather than the compiler's. An adapter that publishes no row for
a needed capability reads as an honest `unavailable`, never a silent pass.
Scripts need no host capability and carry none. The human output prints one
line per target (`<target>: N component(s) selected, M omitted`) followed by
each omission and its reason.
Scripts need no host capability and carry none. Every plan also carries
`kinds`: one entry per canonical kind, in kind order, with the target's own
row for that kind (`capability`) and the counts of selected and skipped
components of it — so a host with no `lsp`, `native-diagnostics`,
`native-extension`, or `agent` surface says so in its own words even when the
project declares none of them (`script` and `event-route` carry no kind-level
row; event routes are judged per component). The human output prints one line
per target (`<target>: N component(s) selected, M omitted`) followed by each
omission and its reason, then `kinds this host cannot emit:` listing every
kind whose row is not `supported`. The full matrix is in
[Host components](framework-mode.md#host-components).

### `agent-bundle inspect --bundler`

Expand Down
40 changes: 40 additions & 0 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,46 @@ The package README's [Developer workbench](../packages/agent-bundle/README.md#de
section carries the exact commands, install layouts, and event payloads;
[Diagnostics](diagnostics.md) lists every code on this path.

## Host components

Every project component belongs to one canonical kind (`AgentComponentKind`,
exported from `agent-bundle/api`), and every kind that needs a host surface
names the capability row a target adapter must publish for it. Adapters judge
each row with the shared four-state contract — `supported` with pinned
evidence, or `degraded` / `unavailable` / `prohibited` with a dated reason —
and `agent-bundle inspect` reports the judgment per target (see
[component accounting](entry-conventions.md#agent-bundle-inspect-component-accounting)).
A host with no row for a kind reads as an honest `unavailable`, never a silent
pass. The matrix below is the state of the pinned tables (Claude Code
2.1.250, Codex 0.147.0, Cursor 2026-08-28, Agent Plugins 1.0.0); the JSON
tables under `packages/agent-bundle/src/adapters/capabilities/` carry the
evidence strings themselves.

| Kind | Source | Capability row | Claude | Codex | Cursor | portable | `plugin` (composite) |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `skill` | `src/skills/<name>/SKILL.{md,ts,tsx}` | `skills` | supported | supported | supported | supported | supported |
| `command` | `src/commands/*.md` | `commands` | supported | unavailable | supported | unavailable | emitted (Claude format; Cursor pointer omitted) |
| `rule` | `src/rules/*.mdc` | `rules` | unavailable | unavailable | supported | unavailable | emitted (Cursor half) |
| `hook` | `hooks` config block | `hooks` | supported | supported | supported | unavailable | supported |
| `event-route` | `src/events/<family>/<event>.tsx` | `event:<canonical event>` per route | per host `hooks.eventRoutes` table (#258) | per table | per table | unavailable (no hooks) | three-host intersection |
| `mcp-server` | `src/mcp/<server>/**` or `mcp.servers` | `mcp` | supported | supported | supported | supported | supported |
| `mcp-app` | `src/mcp/<server>/apps/*` or `mcp.servers.*.apps` | `mcp` | supported | supported | supported | supported | supported |
| `lsp` | `claude.lspServers` (plugin-root `.lsp.json`) | `lsp` | supported | unavailable | unavailable | unavailable | emitted (Claude half); intersection unavailable |
| `native-diagnostics` | none | `nativeDiagnostics` | unavailable (LSP `diagnostics` option only) | unavailable | unavailable | unavailable | unavailable |
| `native-extension` | none | `nativeExtension` | unavailable | unavailable | unavailable | unavailable | unavailable |
| `agent` | `src/agents` (deferred) | `agents` | unavailable — G5 deferral ([#220](https://github.com/ScriptedAlchemy/agent-bundle/pull/220)) | no row | unavailable (G5) | no row | unavailable (G5) |
| `script` | `src/scripts/**`, `scripts` config | none | emitted | emitted | emitted | emitted | emitted |
| `cli` | routed `src/cli/**` bin (#387) | `cli` | supported | supported | supported | supported | supported |

"Emitted" in the composite column means the multi-host `plugin` bundle writes
the surface for the hosts that support it while its own capability row stays
the honest three-host intersection; inspection judges the composite by what it
emits. `native-diagnostics` and `native-extension` have no authoring surface
at all: the rows exist so the compiler's answer to "can this bundle ship a
diagnostics provider or an editor extension?" is a dated *no* per host rather
than silence. The `agent` kind has no producer until the G5 gate admits it;
Claude's `agents` row and its per-field `agents.*` rows record the deferral.

## Distribution

`agent-bundle build` makes each target directory independently distributable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,20 @@
"transports": ["socket", "stdio"],
"vendorsServerBinary": false
},
"nativeDiagnostics": {
"evidence": [
"2026-09-03: the pinned plugins-reference component set recorded in this table (agents, bin, channels, commands, hooks, LSP servers, MCP servers, monitors, output styles, settings, skills, themes, userConfig, workflows) includes no standalone diagnostics-provider component; `diagnostics` appears only as an optional per-server field of the .lsp.json LSP surface (plugin.lsp.optionalFields), which the `lsp` component kind models."
],
"reason": "The pinned Claude plugin contract (2.1.250) documents no standalone diagnostics-provider component; diagnostics reach Claude Code only through the `diagnostics` option of an `.lsp.json` language server, which is the `lsp` kind.",
"state": "unavailable"
},
"nativeExtension": {
"evidence": [
"2026-09-03: the pinned plugins-reference component set recorded in this table (agents, bin, channels, commands, hooks, LSP servers, MCP servers, monitors, output styles, settings, skills, themes, userConfig, workflows) contains no editor or native extension component; the pinned schemas/claude/plugin.schema.json is closed to author, channels, defaultEnabled, description, dependencies, displayName, hooks, lspServers, metadata, name, userConfig, and version, so a plugin cannot vendor or register an IDE extension."
],
"reason": "The pinned Claude plugin contract (2.1.250) documents no editor or native extension component; Claude Code plugins are CLI-loaded component trees, and IDE integrations ship separately from the plugin manifest.",
"state": "unavailable"
},
"metadata": {
"defaultEnabled": {
"default": true,
Expand Down
23 changes: 23 additions & 0 deletions packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,29 @@
"state": "supported"
}
},
"components": {
"lsp": {
"evidence": [
"2026-09-03: the pinned schemas/codex/plugin.schema.json is closed to apps, author, description, homepage, hooks, interface, keywords, license, mcpServers, name, repository, skills, and version; the 2026-09-02/03 https://developers.openai.com/plugins/build/plugins evidence in this table documents skills, MCP servers, hooks, and apps as the plugin parts and no language-server component."
],
"reason": "The pinned Codex plugin contract publishes no LSP server surface; language-server configuration reaches Claude Code only.",
"state": "unavailable"
},
"nativeDiagnostics": {
"evidence": [
"2026-09-03: the pinned schemas/codex/plugin.schema.json is closed to apps, author, description, homepage, hooks, interface, keywords, license, mcpServers, name, repository, skills, and version; no property or documented plugin part carries a diagnostics provider."
],
"reason": "The pinned Codex plugin contract publishes no diagnostics-provider component; plugin-shipped diagnostics have no schema-backed authoring field, so the compiler emits none.",
"state": "unavailable"
},
"nativeExtension": {
"evidence": [
"2026-09-03: the pinned schemas/codex/plugin.schema.json has no extension property; the plugins-overview 'browser extensions' part recorded under plugin.overviewSurfaces.browserExtensions is host-managed with no authoring field, and no pinned Codex contract documents a plugin-shipped editor or native extension."
],
"reason": "The pinned Codex plugin contract publishes no plugin-shipped editor or native extension component; the host-managed browser-extension part has no authoring field, so the compiler emits none.",
"state": "unavailable"
}
},
"overviewSurfaces": {
"mcpUi": {
"evidence": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,27 @@
"state": "supported"
}
},
"lsp": {
"evidence": [
"2026-09-03: the pinned cursor/plugins@070189284e702e8a4d2e3cc8913994b204c5337a schemas/plugin.schema.json defines component pointers for commands, agents, skills, rules, hooks, and mcpServers only; no property names a language-server configuration, and the 2026-09-01 https://cursor.com/docs/plugins evidence above documents the same component set."
],
"reason": "The pinned Cursor Plugin contract publishes no LSP server component; language intelligence is editor-owned and not a plugin component pointer, so language-server configuration reaches Claude Code only.",
"state": "unavailable"
},
"nativeDiagnostics": {
"evidence": [
"2026-09-03: the pinned cursor/plugins@070189284e702e8a4d2e3cc8913994b204c5337a schemas/plugin.schema.json defines component pointers for commands, agents, skills, rules, hooks, and mcpServers only; no property or documented plugin component carries a diagnostics provider."
],
"reason": "The pinned Cursor Plugin contract publishes no diagnostics-provider component, so the compiler emits none.",
"state": "unavailable"
},
"nativeExtension": {
"evidence": [
"2026-09-03: the pinned cursor/plugins@070189284e702e8a4d2e3cc8913994b204c5337a schemas/plugin.schema.json defines component pointers for commands, agents, skills, rules, hooks, and mcpServers only; no property registers or vendors an editor extension, and the documented physical install root ~/.cursor/plugins/local/<name> is a plugin tree, not an extension install path."
],
"reason": "The pinned Cursor Plugin contract publishes no plugin-shipped editor extension component; editor extensions are outside the plugin manifest at this pin, so a plugin cannot vendor or register one.",
"state": "unavailable"
},
"localInstall": {
"method": "copy",
"root": "~/.cursor/plugins/local/<name>"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@
],
"state": "supported"
},
"lsp": {
"evidence": [
"2026-09-03: Agent Plugins 1.0.0 §7 component types define skills and MCP servers only (https://agent-plugins.org/specification); the pinned schemas/portable/plugin.schema.json is closed to $schema, name, version, description, author, homepage, repository, license, keywords, and extensions, so no manifest field or conventional directory carries a language-server configuration."
],
"reason": "The portable Agent Plugin contract (1.0.0) defines only skills and MCP components; it has no LSP server surface.",
"state": "unavailable"
},
"manifest": "plugin.json",
"manifestMetadata": {
"configKey": "portable",
Expand All @@ -150,6 +157,20 @@
],
"state": "supported"
},
"nativeDiagnostics": {
"evidence": [
"2026-09-03: Agent Plugins 1.0.0 §7 component types define skills and MCP servers only (https://agent-plugins.org/specification); no section documents a diagnostics-provider component, and §8.2 client-owned reverse-domain directories are opaque to the format, so the compiler cannot claim any client consumes plugin-shipped diagnostics."
],
"reason": "The portable Agent Plugin contract (1.0.0) defines only skills and MCP components; it has no native diagnostics surface.",
"state": "unavailable"
},
"nativeExtension": {
"evidence": [
"2026-09-03: Agent Plugins 1.0.0 §7 component types define skills and MCP servers only, and §8.2 reserves top-level reverse-domain directories for client-owned files without any pinned client publishing a native-extension contract for them (https://agent-plugins.org/specification); the manifest `extensions` field (§5.6) is opaque metadata, not an installable editor extension."
],
"reason": "The portable Agent Plugin contract (1.0.0) defines only skills and MCP components; it has no native extension surface, and its reverse-domain `extensions` metadata is not one.",
"state": "unavailable"
},
"skills": true
},
"specificationSections": {
Expand Down
32 changes: 32 additions & 0 deletions packages/agent-bundle/src/adapters/capability-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,38 @@ export const supportedEventRouteNamesFrom = (
.map(([event, capability]) => [event, capability.nativeEvent!]),
));

/** A pinned capability-table row: JSON imports widen the state literal, so unknown states fail closed. */
export interface CapabilityTableRow {
readonly reason?: string;
readonly state: string;
}

/**
* Converts one pinned table row into the shared capability-state namespace.
* `supported` and `degraded` carry the adapter's pinned evidence identity;
* `unavailable` and `prohibited` carry the table's dated reason.
*/
export const capabilityFromTableRow = (
row: CapabilityTableRow,
evidence: CapabilityEvidence,
): CapabilityState => {
switch (row.state) {
case 'supported':
return supportedCapability(evidence);
case 'degraded':
return Object.freeze({ evidence, reason: row.reason ?? '', state: 'degraded' });
case 'unavailable':
return unavailableCapability(row.reason ?? `The pinned ${evidence.target} contract does not support this surface.`);
case 'prohibited':
return Object.freeze({
reason: row.reason ?? `The pinned ${evidence.target} contract prohibits this surface.`,
state: 'prohibited',
});
default:
throw new TypeError(`Unsupported ${evidence.target} capability-table state ${JSON.stringify(row.state)}.`);
}
};

export const capabilityStateFromSupport = (
supported: boolean,
evidence: CapabilityEvidence,
Expand Down
8 changes: 7 additions & 1 deletion packages/agent-bundle/src/adapters/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { createTargetMcpRuntime } from '../services/mcp-runtime.ts';
import {
capabilityEvidence,
capabilityFromTableRow,
capabilityStateFromSupport,
eventRouteCapabilitiesFrom,
supportedEventRouteNamesFrom,
Expand Down Expand Up @@ -427,7 +428,7 @@ const hookContract = Object.freeze({
wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Claude'),
} satisfies TargetHookContract);
const metadata = Object.freeze({
adapterRevision: '1.22.0',
adapterRevision: '1.23.0',
observedVersion: capabilityTable.observedCliVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion),
});
Expand Down Expand Up @@ -3383,6 +3384,11 @@ export const claudeAdapter: TargetAdapter = Object.freeze({
evidence,
'The pinned Claude contract does not support both required modern MCP transports.',
),
// Canonical component kinds the pinned Claude contract does not document
// (#100): diagnostics reach Claude only as an LSP server option, and no
// editor/native extension component exists.
nativeDiagnostics: capabilityFromTableRow(capabilityTable.plugin.nativeDiagnostics, evidence),
nativeExtension: capabilityFromTableRow(capabilityTable.plugin.nativeExtension, evidence),
monitors: capabilityStateFromSupport(
capabilityTable.plugin.monitors.commandTokens.length === 4 &&
['${CLAUDE_PLUGIN_ROOT}', '${CLAUDE_PLUGIN_DATA}', '${CLAUDE_PROJECT_DIR}', '${ENV_VAR}']
Expand Down
Loading
Loading