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

Enforce per-host component feature sets for conventional `src/commands/*.md` and `src/rules/*.mdc` documents (#100). Every frontmatter field is judged against the target's `<kind>.<feature>` capability row: a command or rule that explicitly targets a host which cannot express a field it uses fails the build (`AB4927` commands, `AB4907` rules), while an implicitly selected host still receives the document minus the field and `agent-bundle validate` reports the omission as a warning with the host's reason (`AB4928`, `AB4908`). `agent-bundle inspect` lists the same omissions as `omittedFeatures` on the selected component (`--json`) and as `<kind> <name> omits <feature>: …` lines. Cursor's pinned commands surface is frontmatter-free Markdown, so a Cursor-required command must not carry `description`, `argumentHint`, `allowedTools`, `model`, or `disableModelInvocation`; Cursor rules keep `description`, `globs`, and `alwaysApply` (#427)
21 changes: 19 additions & 2 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ gate a build, a validation, or a dev rebuild.
| `AB474x`/`AB4750` | Prebuilt payloads and prebuilt entries (see below). |
| `AB4760` | The published `agent-bundle/meta` identity module evaluated outside every compiled surface and outside the Rstest presets (see below). |
| `AB4765`–`AB4766` | Artifact-hosted routed CLI: a target without the `cli` capability omits `bin/<name>.mjs`; a host-emitted file collides with it (see below). |
| `AB490x`/`AB492x` | Conventional host components (#100 stage 2): rules `src/rules/*.mdc` (`AB4900`–`AB4906`) and commands `src/commands/*.md` (`AB4920`–`AB4926`); 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`), and provider conventions (see below). |
| `AB5000` | General CLI and adapter failures. |
| `AB60xx` | Built-artifact validation, including schema documents and referenced files (`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). |
Expand Down Expand Up @@ -419,7 +419,7 @@ its entry.
| --- | --- | --- |
| `AB4340` | error | A declaration for a route-generated server sets `entry`, `command`, or `url` while `routes.servers.<id>` is `generated`. The routes already compile this server, so a second entry claim has no reading the compiler could honor. Remove the field to keep the generated server (the other fields still apply), or set the mode to `custom`, `command`, or `remote` to serve the declared entry and omit the routes. Without an explicit mode the same collision is `AB4800`. |

## Conventional host components: rules and commands (`AB4900`–`AB4906`, `AB4920`–`AB4926`)
## Conventional host components: rules and commands (`AB4900`–`AB4908`, `AB4920`–`AB4928`)

Conventional `src/rules/*.mdc` documents compile to the Rule IR (closed
frontmatter: `description`, `globs`, `alwaysApply`, plus the bundle-only
Expand All @@ -434,6 +434,19 @@ the surface is a build error — unsupported components fail before artifact
publication rather than shipping as a broken half. Identity paths are
canonicalized so the model digest is root-independent.

Every frontmatter field is also a **component feature** (#100): each host
publishes one `<kind>.<feature>` capability row per field it can express
(`commands.argumentHint`, `rules.globs`, …; see
[Host components](framework-mode.md#component-feature-sets)). A component
that uses a feature the target's row does not support is judged per target:
an explicitly named target fails closed (`AB4907` / `AB4927`), while an
implicitly selected target still receives the component minus the feature and
the omission is reported as a warning with the host's reason (`AB4908` /
`AB4928`) and on the selected component in `inspect` (`omittedFeatures`).
Targets whose kind row is itself unsupported are judged by the kind-level codes
above, never per feature. Skills keep their own closed per-host schemas
(`AB3006`, `AB3008`, `AB3010`).

| Code | Severity | Trigger | Recovery |
| --- | --- | --- | --- |
| `AB4900` | error | A conventional rule file cannot be read. | Make the `.mdc` file readable, or remove it from `src/rules/`. |
Expand All @@ -443,13 +456,17 @@ canonicalized so the model digest is root-independent.
| `AB4904` | error | A rule's `targets` names a target that is not registered or not selected for the project. | Name only selected targets, or select that target in `targets`. |
| `AB4905` | error | A rule explicitly targets a host whose `rules` capability is `degraded`, `unavailable`, or `prohibited` (the message carries the host's reason). | Drop that host from the rule's `targets`; only Cursor publishes a rules surface. |
| `AB4906` | error | Two rule files share a name. | Rename one file so every rule name is unique. |
| `AB4907` | error | A rule explicitly targets a host that supports rules but whose `rules.<field>` row for a frontmatter field the rule uses is `degraded`, `unavailable`, or `prohibited` (the message carries the host's reason). | Remove the field or drop that host from the rule's `targets`. Cursor documents `description`, `globs`, and `alwaysApply`. |
| `AB4908` | warning | An implicitly selected host supports rules but cannot express a frontmatter field the rule uses; the rule ships there without it. | Accept the omission, restrict the rule's `targets` to hosts that support the field, or remove the field. |
| `AB4920` | error | A conventional command file cannot be read. | Make the `.md` file readable, or remove it from `src/commands/`. |
| `AB4921` | error | Command YAML frontmatter is invalid. | Repair the YAML between the `---` fences. |
| `AB4922` | error | Command frontmatter declares a field outside `description`, `argumentHint`, `allowedTools`, `model`, `disableModelInvocation`, `targets`. | Remove the field; per-host frontmatter is regenerated from the validated fields at lowering time. |
| `AB4923` | error | A command frontmatter field has the wrong shape (`allowedTools` nonempty string or array, string fields, `disableModelInvocation` boolean, `targets` array of target names). | Fix the field's value. |
| `AB4924` | error | A command's `targets` names a target that is not registered or not selected for the project. | Name only selected targets, or select that target in `targets`. |
| `AB4925` | error | A command explicitly targets a host whose `commands` capability is `degraded`, `unavailable`, or `prohibited` (the message carries the host's reason). | Drop that host from the command's `targets`; Cursor and Claude publish command surfaces, Codex and portable do not. |
| `AB4926` | error | Two command files share a name. | Rename one file so every command name is unique. |
| `AB4927` | error | A command explicitly targets a host that supports commands but whose `commands.<field>` row for a frontmatter field the command uses is `degraded`, `unavailable`, or `prohibited` (the message carries the host's reason). Cursor's pinned commands surface is frontmatter-free Markdown, so every field row is unavailable there. | Remove the field or drop that host from the command's `targets`. |
| `AB4928` | warning | An implicitly selected host supports commands but cannot express a frontmatter field the command uses; the command ships there without it (Cursor receives the prompt body only). | Accept the omission, restrict the command's `targets` to hosts that support the field, or remove the field. |

## Route graph, state, layout, and provider conventions (`AB4800`–`AB4832`, `AB4940`–`AB4942`)

Expand Down
9 changes: 7 additions & 2 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,8 +797,13 @@ 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. Every plan also carries
`kinds`: one entry per canonical kind, in kind order, with the target's own
Scripts need no host capability and carry none. A selected component that
uses a feature the target cannot express (a command frontmatter field on
Cursor, for example) carries `omittedFeatures`: one entry per omitted feature
with the host's `<kind>.<feature>` row, matching the `AB4908` / `AB4928`
warnings `validate` reports (see
[Component feature sets](framework-mode.md#component-feature-sets)). Every
plan also carries `kinds`: one entry per canonical kind, in kind order, with the target's own
row for that kind (`capability`) and the counts of selected and skipped
components of it — so a host with no `lsp`, `native-diagnostics`,
`native-extension`, or `agent` surface says so in its own words even when the
Expand Down
34 changes: 34 additions & 0 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,40 @@ 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.

### Component feature sets

Each kind also has a **feature set**: the host features a component of that
kind may use, published as one `<kind capability>.<feature>` row per feature
with the same four-state judgment. A component is judged feature by feature
against every target that supports its kind: a target the author named in
`targets` fails closed when it cannot express a feature (`AB4907` rules,
`AB4927` commands), while an implicitly selected target still receives the
component minus the feature, reports the omission as a warning with the host's
reason (`AB4908` / `AB4928`), and lists it under `omittedFeatures` on the
selected component in `inspect` (human output: `<kind> <name> omits <feature>:
…`). The composite `plugin` bundle is judged by the half that emits the kind
(Claude for commands, Cursor for rules). Skills keep the closed per-host Skill
IR schemas from #108 as their feature mechanism (`AB3006`, `AB3008`, `AB3010`);
their rows below mirror that contract rather than adding a second check.

| Kind | Feature rows | Claude | Codex | Cursor | portable |
| --- | --- | --- | --- | --- | --- |
| `command` | `commands.description`, `commands.argumentHint`, `commands.allowedTools`, `commands.model`, `commands.disableModelInvocation` | supported (documented kebab-case frontmatter) | no commands | unavailable — frontmatter-free Markdown, body only | no commands |
| `rule` | `rules.description`, `rules.globs`, `rules.alwaysApply` | no rules | no rules | supported (`.mdc` frontmatter, retrieved 2026-09-03) | no rules |
| `hook` | `hooks.toolMatchers`, `hooks.timeout` | supported | supported | supported | no hooks |
| `skill` | `skills.hostFrontmatter` (typed host extension / Codex `agents/openai.yaml` sidecar) | supported | supported | supported | unavailable (portable fields only) |
| `skill` | `skills.markdownTokens` (`$ARGUMENTS`, `${CLAUDE_PLUGIN_ROOT}`, …) | supported | unavailable (`AB3008`) | unavailable (`AB3008`) | unavailable (`AB3008`) |

The composite `plugin` bundle ships one shared `skills/` tree and lowers any
skill that declares a host extension or token to the portable document, so
both skill feature rows are `unavailable` there and `inspect` reports the
dropped host frontmatter under `omittedFeatures`; per-host skill trees are
install-time selection (#101).

Hook tool selectors a host cannot map still fail at plan time
(`<target>.hook.tool.<tool>`), and the per-host matcher tables live under
`hooks.matchers` in each capability table.

## Distribution

`agent-bundle build` makes each target directory independently distributable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@
"state": "unavailable"
}
},
"features": {
"timeout": {
"evidence": [
"2026-09-03: the pinned schemas/claude/hooks.schema.json declares an integer `timeout` (minimum 1) on every command hook entry; the hook contract lowers `timeoutMs` to whole seconds."
],
"state": "supported"
},
"toolMatchers": {
"evidence": [
"2026-09-03: the pinned schemas/claude/hooks.schema.json declares a string `matcher` on every hook group, and hooks.matchers above pins the canonical tool selectors (file.read, file.write, mcp, shell) to Claude tool-name regexes; an unmapped selector fails the plan (claude.hook.tool.<tool>)."
],
"state": "supported"
}
},
"matchers": {
"file.read": "^Read$",
"file.write": "^(?:Write|Edit)$",
Expand Down Expand Up @@ -280,6 +294,20 @@
"bindsToPluginMcpServer": true,
"perChannelUserConfig": true
},
"commandFrontmatter": {
"evidence": [
"2026-09-01: https://code.claude.com/docs/en/plugins-reference documents plugin commands/ as Markdown files with optional YAML frontmatter fields description, argument-hint, allowed-tools, model, and disable-model-invocation (see provenance.evidence).",
"2026-09-03: adapters/claude.ts claudeCommandMarkdown regenerates exactly those five kebab-case fields from the validated Command IR (allowedTools, argumentHint, description, disableModelInvocation, model) and never emits an authoring-only key."
],
"fields": {
"allowedTools": "allowed-tools",
"argumentHint": "argument-hint",
"description": "description",
"disableModelInvocation": "disable-model-invocation",
"model": "model"
},
"state": "supported"
},
"commands": true,
"dependencies": {
"autoInstall": true,
Expand Down Expand Up @@ -689,6 +717,20 @@
"supportedKeys": ["agent", "subagentStatusLine"],
"unknownKeysIgnored": true
},
"skillFeatures": {
"hostFrontmatter": {
"evidence": [
"2026-09-03: the Skill IR lowers the typed Claude extension (agent, allowed-tools, argument-hint, arguments, background, context, disable-model-invocation, disallowed-tools, effort, hooks, model, paths, shell, user-invocable, when_to_use) into SKILL.md frontmatter validated against the sha-pinned schemas/skill-hosts/claude-skill-frontmatter.schema.json (#108); unknown fields fail closed with AB3006 and schema violations with AB3010."
],
"state": "supported"
},
"markdownTokens": {
"evidence": [
"2026-09-03: skills/tokens.ts pins every canonical token to a documented Claude Skill Markdown placeholder ($ARGUMENTS, ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR}, ${CLAUDE_SESSION_ID}, ${CLAUDE_SKILL_DIR}) per https://code.claude.com/docs/en/skills; the #242 real-session proof observed $ARGUMENTS, ${CLAUDE_PLUGIN_ROOT}, and ${CLAUDE_SKILL_DIR} resolving on Claude Code 2.1.257."
],
"state": "supported"
}
},
"skills": true,
"userConfig": {
"envVarPrefix": "CLAUDE_PLUGIN_OPTION_",
Expand Down
29 changes: 29 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 @@ -226,6 +226,20 @@
"state": "unavailable"
}
},
"features": {
"timeout": {
"evidence": [
"2026-09-03: the pinned schemas/codex/hooks.schema.json declares an integer `timeout` (minimum 1) on hook entries; hooks.contract.timeoutRules records the per-event default and maximum seconds the host enforces."
],
"state": "supported"
},
"toolMatchers": {
"evidence": [
"2026-09-03: the pinned schemas/codex/hooks.schema.json declares a string `matcher` per hook group, and hooks.matchers above pins the canonical selectors (file.write, mcp, shell) to Codex tool-name regexes; file.read has no Codex mapping and fails the plan (codex.hook.tool.file-read), and hooks.contract.matcherSemantics records the events that ignore matchers."
],
"state": "supported"
}
},
"matchers": {
"file.write": "^(?:apply_patch|Edit|Write)$",
"mcp": "^mcp__",
Expand Down Expand Up @@ -630,6 +644,21 @@
"state": "unavailable"
}
},
"skillFeatures": {
"hostFrontmatter": {
"evidence": [
"2026-09-03: the Skill IR lowers the typed Codex extension (interface, policy, dependencies) into the agents/openai.yaml sidecar validated against the sha-pinned schemas/skill-hosts/codex-openai-yaml.schema.json (#108), while SKILL.md keeps only the portable Agent Skills fields; the #242 host-install proof observed the byte-identical sidecar in the installed Codex cache."
],
"state": "supported"
},
"markdownTokens": {
"evidence": [
"2026-09-03: skills/tokens.ts classifies every canonical token as `none` for Codex Skill Markdown because https://learn.chatgpt.com/docs/build-skills documents no interpolation placeholders inside SKILL.md; lowering fails closed with AB3008 instead of emitting an unresolved token."
],
"reason": "The pinned Codex skills contract (0.147.0) documents no Skill Markdown interpolation placeholders; a skill body token has no Codex equivalent and fails closed (AB3008).",
"state": "unavailable"
}
},
"skills": true
},
"validation": {
Expand Down
Loading
Loading