diff --git a/.changeset/claude-capability-repin-2-1-260.md b/.changeset/claude-capability-repin-2-1-260.md new file mode 100644 index 000000000..f78462812 --- /dev/null +++ b/.changeset/claude-capability-repin-2-1-260.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Re-pin the `claude` host contract from Claude Code 2.1.250 to 2.1.260 (`observedCliVersion`, `hostCli.version`, the `claude-2.1.260.json` capability table, and every `schemas/claude/*` snapshot; Claude `adapterRevision` `1.28.0`, composite `plugin` `1.29.0`). Add the `model-switch/before` and `model-switch/after` canonical event-route families (`src/events/model-switch/before.tsx`, `.../after.tsx`): on Claude Code they compile to `PreModelSwitch` and `PostModelSwitch` (2.1.251 or later), a decided `model-switch/before` projects `outcome: 'allow' | 'ask' | 'deny'` to `hookSpecificOutput.permissionDecision` with the reason as `permissionDecisionReason`, `model-switch/after` is observation-only with `Agent.Context` delivered as `additionalContext`, and Codex, Cursor, and portable carry dated `unavailable` rows. `hooks.schema.json` now admits `PreModelSwitch` and `PostModelSwitch` (`command`, `http`, `mcp_tool` handlers) in a `claude.nativeHooks` document. `build`, `validate --artifact`, and `doctor` request `claude plugin validate --strict --json` first and fall back to the text reporter only when the CLI rejects the flag, so a CLI of unknown version no longer skips the JSON report. The Claude agents capability rows record that 2.1.260 `--strict --json` accepts `color`, `initialPrompt`, and `experimental.cacheTtl` without validating their values; the agents component stays deferred. (#542) diff --git a/docs/canvases/agent-bundle-walkthrough.canvas.tsx b/docs/canvases/agent-bundle-walkthrough.canvas.tsx index f347046d3..2718cc806 100644 --- a/docs/canvases/agent-bundle-walkthrough.canvas.tsx +++ b/docs/canvases/agent-bundle-walkthrough.canvas.tsx @@ -275,7 +275,7 @@ const WIRE_IPC_REQUEST = `{ "protocolVersion": 1, "artifactEpoch": "17903a885df8d142e2fc4457e61bb34479e81a8f4cc64e24cb92db49eaabe3f1", "event": "tool/before", - "hostContractRevision": "2.1.250", + "hostContractRevision": "2.1.260", "target": "claude", "native": { ...the validated stdin envelope, value-preserving (re-serialized JSON, not raw bytes)... } } @@ -287,7 +287,7 @@ const WIRE_RENDER_PROPS = `props = { event: "tool/before", idempotencyKey: sha256({ event, native, target }), observedAt: "2026-09-01T23:41:07.512Z", - provenance: { host: "claude", hostContractRevision: "2.1.250", + provenance: { host: "claude", hostContractRevision: "2.1.260", nativeEvent: "PreToolUse", source: "native" }, sequence: 1, }, @@ -499,7 +499,7 @@ export default function AgentBundleWalkthrough() { silent guess.
session/start, "SessionStart", "SessionStart", "sessionStart", "—"], @@ -963,7 +963,7 @@ agent-bundle install cursor --from artifact/cursor # staged copy`} docs/framework-mode.md · examples/{hooks-and-scripts,audiobook-curator,rsc-agent-runtime}. Wire payloads in section 4 are illustrative values over verified shapes. Reflects the post-PR-#280 split of React rendering (events/project.ts) from envelope projection - (events/projection.ts). Host capability facts pinned at Claude Code 2.1.250, Codex + (events/projection.ts). Host capability facts pinned at Claude Code 2.1.260, Codex 0.147.0, Cursor 2026-08-28, Agent Plugins 1.0.0 (observed 2026-08-28 … 2026-09-01). diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 9583d905e..a54d9c64f 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -65,10 +65,11 @@ first, which covers `plugin.json`, `hooks/hooks.json`, and the `skills/`, `agents/`, and `commands/` directories, and then `claude plugin validate /.claude-plugin/marketplace.json --strict`, dropping the marketplace run's `plugins[N] plugin.json →` copies of manifest findings the -plugin run already reported. On Claude Code 2.1.259 or later both runs add -`--json` and each finding is attributed to its file (`generatedPath`); older -releases fall back to the text report, attributed by its `Validating : -` headers. +plugin run already reported. Both runs ask for `--json` first (Claude Code +2.1.259 or later; the pinned 2.1.260 prints it) and each finding is attributed +to its file (`generatedPath`); when the CLI rejects the flag (`unknown option +'--json'`) or the probed version predates 2.1.259, the runs fall back to the +text report, attributed by its `Validating : ` headers. `claude plugin validate --strict` is not a load verdict: Claude Code 2.1.250 through 2.1.260 accept manifests and component files (for example an invalid diff --git a/docs/framework-mode.md b/docs/framework-mode.md index 9ad096fa7..e42166cd3 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -453,7 +453,7 @@ 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 +2.1.260, 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. diff --git a/examples/rsc-agent-runtime/README.md b/examples/rsc-agent-runtime/README.md index b6316366c..fe4d87b41 100644 --- a/examples/rsc-agent-runtime/README.md +++ b/examples/rsc-agent-runtime/README.md @@ -227,7 +227,7 @@ Host/Origin allowlists mitigate DNS rebinding and cross-origin requests, but the ### Semantic event-route support -| Event family | Cursor | Claude Code 2.1.250 | Codex 0.147.0 | +| Event family | Cursor | Claude Code 2.1.260 | Codex 0.147.0 | | --- | --- | --- | --- | | `session/start` | Supported | `SessionStart` | `SessionStart` | | `session/end` | `sessionEnd` (observe-only; desktop only) | `SessionEnd` (observe-only) | `SessionEnd` (observe-only) | @@ -246,6 +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 | +| `model-switch/before` | Unavailable | `PreModelSwitch` (allow/ask/deny; 2.1.251+) | Unavailable | +| `model-switch/after` | Unavailable | `PostModelSwitch` (observe-only + context; 2.1.251+) | Unavailable | | `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 | diff --git a/examples/rsc-agent-runtime/scripts/eval-hosts.mjs b/examples/rsc-agent-runtime/scripts/eval-hosts.mjs index dc5036a0a..acfe5ae8c 100644 --- a/examples/rsc-agent-runtime/scripts/eval-hosts.mjs +++ b/examples/rsc-agent-runtime/scripts/eval-hosts.mjs @@ -13,7 +13,7 @@ import { sanitizedHostEnvironment } from './eval-host-environment.mjs'; import { exampleRootFromModule } from './eval-host-paths.mjs'; const exampleRoot = exampleRootFromModule(import.meta.url); -const expectedVersions = { claude: '2.1.250', codex: '0.147.0' }; +const expectedVersions = { claude: '2.1.260', codex: '0.147.0' }; const parseHost = (argv) => { const hostIndex = argv.indexOf('--host'); diff --git a/packages/agent-bundle/fixtures/contracts/claude/capabilities.json b/packages/agent-bundle/fixtures/contracts/claude/capabilities.json index 520a3d5a4..2f4681f56 100644 --- a/packages/agent-bundle/fixtures/contracts/claude/capabilities.json +++ b/packages/agent-bundle/fixtures/contracts/claude/capabilities.json @@ -13,7 +13,7 @@ "marketplace": ".claude-plugin/marketplace.json", "mcp": ".mcp.json" }, - "observedCliVersion": "2.1.250", + "observedCliVersion": "2.1.260", "pathTokens": { "pluginData": "${CLAUDE_PLUGIN_DATA}", "pluginRoot": "${CLAUDE_PLUGIN_ROOT}", diff --git a/packages/agent-bundle/fixtures/contracts/claude/cli-help.txt b/packages/agent-bundle/fixtures/contracts/claude/cli-help.txt index 59aa667b5..34a2a9ab5 100644 --- a/packages/agent-bundle/fixtures/contracts/claude/cli-help.txt +++ b/packages/agent-bundle/fixtures/contracts/claude/cli-help.txt @@ -1,26 +1,114 @@ -version: 2.1.250 +version: 2.1.260 $ claude plugin --help Usage: claude plugin|plugins [options] [command] Manage Claude Code plugins +Options: + -h, --help Display help for command + Commands: - details [options] Show a plugin's component inventory and projected token cost - install|i [options] Install a plugin from available marketplaces + details [options] Show a plugin's component inventory and + projected token cost + disable [options] [plugin] Disable an enabled plugin + enable [options] Enable a disabled plugin + eval [options] [target] Run eval cases (/**/case.yaml + or prompt.md + graders/*.md; the eval dir + is evals/ unless --eval-dir or the + manifest says otherwise) against a plugin + and report scored results. Target is a + path, a plugin name, or a + `plugin@marketplace` id — installed and + skills-dir plugins both resolve (and add + a no-plugin baseline arm) + help [command] display help for command + init|new [options] Scaffold a new plugin at + ~/.claude/skills// (auto-loads next + session as @skills-dir) + install|i [options] Install a plugin from available + marketplaces (use plugin@marketplace for + specific marketplace) + list [options] List installed plugins marketplace Manage Claude Code marketplaces - validate [options] Validate a plugin or marketplace manifest + prune|autoremove [options] Remove auto-installed dependencies that + are no longer needed + tag [options] [path] Create a {name}--v{version} git tag for a + plugin release, validating that + plugin.json and any enclosing marketplace + entry agree + uninstall|remove [options] Uninstall an installed plugin + update [options] Update a plugin to the latest version + (restart required to apply) + validate [options] Validate a plugin or marketplace + manifest, or the skills, agents, and + commands in a directory +$ claude plugin validate --help +Usage: claude plugin validate [options] + +Validate a plugin or marketplace manifest, or the skills, agents, and commands +in a directory + +Options: + -h, --help Display help for command + --json Output the validation report as JSON (same exit codes) + --strict Treat warnings as errors (exit 1). Use in CI to fail on + unrecognized fields, missing metadata, and other issues that the + runtime tolerates. +$ claude plugin list --help +Usage: claude plugin list [options] + +List installed plugins + +Options: + --available Include available plugins from marketplaces (requires --json) + -h, --help Display help for command + --json Output as JSON +$ claude plugin marketplace --help +Usage: claude plugin marketplace [options] [command] +Manage Claude Code marketplaces + +Options: + -h, --help Display help for command + +Commands: + add [options] Add a marketplace from a URL, path, or GitHub repo + help [command] display help for command + list [options] List all configured marketplaces + remove|rm [options] Remove a configured marketplace + update [options] [name] Update marketplace(s) from their source - updates + all if no name specified $ claude mcp add --help Usage: claude mcp add [options] [args...] Add an MCP server to Claude Code. Examples: + # Add HTTP server: claude mcp add --transport http sentry https://mcp.sentry.dev/mcp + + # Add HTTP server with headers: + claude mcp add --transport http corridor https://app.corridor.dev/api/mcp +--header "Authorization: Bearer ..." + + # Add stdio server with environment variables: claude mcp add my-server -e API_KEY=xxx -- npx my-mcp-server + # Add stdio server with subprocess flags: + claude mcp add my-server -- my-command --some-flag arg1 + Options: - -e, --env Set environment variables - -H, --header Set WebSocket headers - -t, --transport Transport type (stdio, sse, http). Defaults to stdio if not specified. + --callback-port Fixed port for OAuth callback (for servers + requiring pre-registered redirect URIs) + --client-id OAuth client ID for HTTP/SSE servers + --client-secret Prompt for OAuth client secret (or set + MCP_CLIENT_SECRET env var) + -e, --env Set environment variables (e.g. -e KEY=value) + -H, --header Set headers for HTTP/SSE servers (e.g. -H + "X-Api-Key: abc123" -H "X-Custom: value") + -h, --help Display help for command + -s, --scope Configuration scope (local, user, or project) + (default: "local") + -t, --transport Transport type (stdio, sse, http). Defaults to + stdio if not specified. diff --git a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.260.json similarity index 91% rename from packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json rename to packages/agent-bundle/src/adapters/capabilities/claude-2.1.260.json index 5c77ce958..cd5a1431a 100644 --- a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json +++ b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.260.json @@ -4,7 +4,8 @@ "evidence": [ "Local marketplaces accept a directory containing .claude-plugin/marketplace.json.", "claude plugin install accepts plugin@marketplace and user, project, or local scope.", - "2026-09-01: Claude Code 2.1.257 rejects a bare `.` marketplace source but accepts `./` and absolute directory paths." + "2026-09-01: Claude Code 2.1.257 rejects a bare `.` marketplace source but accepts `./` and absolute directory paths.", + "2026-09-03: re-pin 2.1.250 -> 2.1.260 — `claude plugin --help`, `claude plugin marketplace --help`, and every plugin/marketplace subcommand --help are byte-identical between 2.1.250 and 2.1.260 except `plugin validate --help`, which gains `--json` (schemas/claude/PROVENANCE.json repinNotes); packages/agent-bundle/fixtures/contracts/claude/cli-help.txt is the redacted 2.1.260 capture." ], "marketplaceAdd": "claude plugin marketplace add ./", "pluginInstall": "claude plugin install @ --scope ", @@ -79,18 +80,10 @@ "reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: delivery-channel surface tracked by #99 delivery evidence, not an application event route (#258 defer list).", "state": "unavailable" }, - "PostModelSwitch": { - "reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: model-lifecycle callback outside the canonical application-event vocabulary (#258 defer list).", - "state": "unavailable" - }, "PostToolBatch": { "reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: batch-scheduling callback outside the canonical application-event vocabulary (#258 defer list).", "state": "unavailable" }, - "PreModelSwitch": { - "reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: model-lifecycle callback outside the canonical application-event vocabulary (#258 defer list).", - "state": "unavailable" - }, "Setup": { "reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: one-time init/maintenance callback outside the canonical application-event vocabulary (#258 defer list).", "state": "unavailable" @@ -133,6 +126,8 @@ "compact/before": { "nativeEvent": "PreCompact", "state": "supported" }, "config/change": { "nativeEvent": "ConfigChange", "state": "supported" }, "file/change": { "nativeEvent": "FileChanged", "state": "supported" }, + "model-switch/after": { "nativeEvent": "PostModelSwitch", "state": "supported" }, + "model-switch/before": { "nativeEvent": "PreModelSwitch", "state": "supported" }, "permission/denied": { "nativeEvent": "PermissionDenied", "state": "supported" }, "permission/request": { "nativeEvent": "PermissionRequest", "state": "supported" }, "prompt/submit": { "nativeEvent": "UserPromptSubmit", "state": "supported" }, @@ -146,7 +141,7 @@ "tool/before": { "nativeEvent": "PreToolUse", "state": "supported" }, "tool/failure": { "nativeEvent": "PostToolUseFailure", "state": "supported" }, "workspace/open": { - "reason": "The pinned Claude Code 2.1.250 hooks contract has no workspace-open event.", + "reason": "The pinned Claude Code 2.1.260 hooks contract has no workspace-open event.", "state": "unavailable" } }, @@ -177,7 +172,7 @@ "emitted": { "command": ["command", "timeout", "type"] }, "evidence": [ "uploaded 2026-09-03: Claude Code docs hooks reference (hooks-2.md, \"Hook handler fields\", \"Common fields\", \"Command hook fields\", \"Exec form and shell form\", \"HTTP hook fields\", \"MCP tool hook fields\", \"Prompt and agent hook fields\", \"Prompt-based hooks\", \"Agent hook configuration\", \"Run hooks in the background\") documents the five handler types, the common if/timeout/statusMessage/once fields, and the per-type fields pinned in schemas/claude/hooks.schema.json (#477); `async` and `args` belong to command hooks only, and agent hooks have no continueOnBlock.", - "2026-09-03: the pinned Claude Code 2.1.250 (`claude plugin validate --strict`, text reporter) and 2.1.260 (`--strict --json`) both accept a hooks/hooks.json using every documented type and field (tests/fixtures/claude-hooks-schema/cases/full.json) and both reject under --strict a missing per-type required field, an unknown handler type, an unknown event, and each wrong-typed field; both ignore unknown hooks.json keys and cross-type fields without a warning, which the closed pinned schema rejects instead. They disagree only on PreModelSwitch and PostModelSwitch, which the reference gates to v2.1.251 or later and 2.1.250 rejects as unknown keys, so the schema omits them until the re-pin (schemas/claude/PROVENANCE.json hooksSchemaNotes).", + "2026-09-03: the pinned Claude Code 2.1.260 (`claude plugin validate --strict --json`) and the previous 2.1.250 pin (`--strict`, text reporter) both accept a hooks/hooks.json using every documented type and field (tests/fixtures/claude-hooks-schema/cases/full.json) and both reject under --strict a missing per-type required field, an unknown handler type, an unknown event, and each wrong-typed field; both ignore unknown hooks.json keys and cross-type fields without a warning, which the closed pinned schema rejects instead. They disagree only on PreModelSwitch and PostModelSwitch, which the reference gates to v2.1.251 or later: 2.1.250 rejects the keys, 2.1.260 accepts them, and the re-pin to 2.1.260 admits both into the schema with the command/http/mcp_tool partition (schemas/claude/PROVENANCE.json hooksSchemaNotes, repinNotes).", "2026-09-03: the compiler emits only shell-form command handlers carrying type, command, and timeout (hook-contract planHooks); args (exec form, the reference's recommendation for path placeholders such as ${CLAUDE_PLUGIN_ROOT}), async, asyncRewake, shell, if, once, statusMessage, and the http/mcp_tool/prompt/agent handlers reach a bundle only through an authored `claude.nativeHooks` document, validated against the pinned schema and merged after the generated groups." ] }, @@ -190,6 +185,18 @@ "2026-09-03: no agent/start or agent/stop route selector lowers an agent name yet (the agents component stays behind the #100 stage-2 G5 gate, #478); when one does, the Claude lowering must emit the anchored plugin-scoped form for agents this plugin ships, not the bare frontmatter name." ] }, + "modelSwitch": { + "matcherField": "to_model (canonical model name)", + "inputFields": ["from_model", "to_model", "requested_model", "source", "context_tokens", "prompt_cache_warm", "cache_ttl", "estimated_cache_write_usd", "pricing"], + "decision": { + "PostModelSwitch": "observation plus hookSpecificOutput.additionalContext delivered with the next request; cannot block", + "PreModelSwitch": "hookSpecificOutput.permissionDecision allow|deny|ask with permissionDecisionReason; deny > ask > allow; no updatedInput or additionalContext" + }, + "evidence": [ + "uploaded 2026-09-03: Claude Code docs hooks reference (hooks-2.md, \"PreModelSwitch\", \"PreModelSwitch input\", \"PreModelSwitch decision control\", \"PostModelSwitch\", \"PostModelSwitch input\", \"PostModelSwitch decision control\", \"Prompt-based hooks\") — both events require Claude Code v2.1.251 or later and run command, http, and mcp_tool hooks only; the matcher is compared against the canonical name of to_model (an alias, dated ID, or provider ID resolves to it; every hook runs when no canonical name is known); input adds from_model, to_model, requested_model (string or null), source (command, picker, sdk; PostModelSwitch adds auto and resume), context_tokens, prompt_cache_warm, cache_ttl (5m or 1h), estimated_cache_write_usd, and pricing (configured, catalog, default). PreModelSwitch cancels the switch with exit code 2, top-level decision: block, or hookSpecificOutput.permissionDecision deny; ask prompts only in an interactive /model session and is a refusal elsewhere; allow skips the warm-cache confirmation; it accepts no defer, updatedInput, or additionalContext, and a hook that times out (default 30 seconds) blocks the switch. PostModelSwitch cannot block; plain-text stdout or hookSpecificOutput.additionalContext reaches Claude with the next request, only for the last switch's target when several happen first.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` accepts hooks/hooks.json documents naming PreModelSwitch and PostModelSwitch (tests/fixtures/claude-hooks-schema/reports/2.1.260/model-switch-events.json, all-pinned-events.json) and `claude --plugin-dir plugin details` inventories them as `Hooks (2) PreModelSwitch, PostModelSwitch`; the previous 2.1.250 pin rejected both keys (`Invalid key in record`), which is why the canonical model-switch/before and model-switch/after families waited for this re-pin (#258 defer list, #477 follow-up). The route wrappers project a decided model-switch/before to hookSpecificOutput.permissionDecision and a model-switch/after context to hookSpecificOutput.additionalContext; no live model-switch envelope has been captured yet, so the decode fixtures (tests/fixtures/events/claude-pre-model-switch.json, claude-post-model-switch.json) are the reference's documented example input." + ] + }, "matchers": { "file.read": "^Read$", "file.write": "^(?:Write|Edit)$", @@ -269,7 +276,7 @@ ] } }, - "observedCliVersion": "2.1.250", + "observedCliVersion": "2.1.260", "plugin": { "agents": { "component": { @@ -376,6 +383,7 @@ "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that hooks is unsupported and ignored when an agent is loaded from a plugin.", "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") marks hooks \"Ignored for [plugin subagents]\" in the frontmatter table (#478).", "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring hooks without warning, proving the validator does not enforce the loader's ignore contract.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` still accepts a plugin agent declaring hooks without a finding (re-pin evidence, schemas/claude/PROVENANCE.json repinNotes).", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." ] }, @@ -386,6 +394,7 @@ "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that mcpServers is unsupported and ignored when an agent is loaded from a plugin.", "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") marks mcpServers \"Ignored for [plugin subagents]\" in the frontmatter table (#478).", "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring mcpServers without warning, proving the validator does not enforce the loader's ignore contract.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` still accepts a plugin agent declaring mcpServers without a finding (re-pin evidence, schemas/claude/PROVENANCE.json repinNotes).", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." ] }, @@ -396,6 +405,7 @@ "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that permissionMode is unsupported and ignored when an agent is loaded from a plugin.", "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") marks permissionMode \"Ignored for [plugin subagents]\" in the frontmatter table (#478).", "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring permissionMode: bypassPermissions without warning, proving the validator does not enforce the loader's ignore contract.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` still accepts a plugin agent declaring permissionMode without a finding (re-pin evidence, schemas/claude/PROVENANCE.json repinNotes).", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." ] }, @@ -404,7 +414,8 @@ "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent color field together with agents/ emission.", "evidence": [ "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") documents color as the display color for the subagent in the task list and transcript, accepting red, blue, green, yellow, purple, orange, pink, or cyan.", - "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement." + "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` accepts a plugin agent declaring `color: cyan` and equally accepts a misspelled `colour: red` key without a finding, so the validator does not enforce the frontmatter vocabulary; a re-admitted agents component must validate the eight documented values itself (re-pin evidence, schemas/claude/PROVENANCE.json repinNotes)." ] }, "initialPrompt": { @@ -412,7 +423,8 @@ "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent initialPrompt field together with agents/ emission.", "evidence": [ "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") documents initialPrompt as the text auto-submitted as the first user turn when the agent runs as the main session agent via --agent or the agent setting, with commands and skills processed and the text prepended to any user-provided prompt.", - "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement." + "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` accepts a plugin agent declaring `initialPrompt` without a finding; its only agent-level finding on 2.1.260 is the `description` warning (`No description in frontmatter ...`), which --strict promotes to failure (re-pin evidence, schemas/claude/PROVENANCE.json repinNotes)." ] }, "experimentalCacheTtl": { @@ -420,7 +432,8 @@ "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent experimental.cacheTtl field together with agents/ emission.", "evidence": [ "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") documents experimental as a map whose cacheTtl key, 5m or 1h and written inside the experimental map, chooses the prompt cache lifetime for the subagent's requests; Claude Code ignores any other value, ignores 1h while a subscription is using usage credits, reads the field only from subagent files, and requires Claude Code 2.1.248 or later.", - "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement." + "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement.", + "2026-09-03: the pinned Claude Code 2.1.260 `claude plugin validate --strict --json` accepts a plugin agent declaring `experimental.cacheTtl: 1h` and equally accepts the undocumented `cacheTtl: 2h` without a finding, so the 5m/1h vocabulary is loader-side only (\"Claude Code ignores any other value\"); the 2.1.248 floor is below the 2.1.260 pin (re-pin evidence, schemas/claude/PROVENANCE.json repinNotes)." ] } }, @@ -727,14 +740,14 @@ "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.", + "reason": "The pinned Claude plugin contract (2.1.260) 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.", + "reason": "The pinned Claude plugin contract (2.1.260) 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": { @@ -904,13 +917,14 @@ "LSP servers section: \"Location: .lsp.json in plugin root, or inline in plugin.json\"; the file-locations table lists .lsp.json as the default LSP location, alongside .mcp.json, at the plugin root rather than inside .claude-plugin/.", "Component path fields table: lspServers is typed string|array|object with the example \"./.lsp.json\", so a path string, an array of path strings, and an inline server map are all documented forms.", "Required per-server fields are command (\"The LSP binary to execute (must be in PATH)\") and extensionToLanguage; transport accepts socket but Claude Code runs every server over stdio.", - "restartOnCrash and shutdownTimeout require Claude Code v2.1.205 or later; before that revision either option made Claude Code skip the server entirely. The pinned 2.1.250 revision is past that floor.", + "restartOnCrash and shutdownTimeout require Claude Code v2.1.205 or later; before that revision either option made Claude Code skip the server entirely. The pinned 2.1.260 revision is past that floor.", "First-registered-wins collision rule: when more than one enabled server declares the same extension in extensionToLanguage, from one plugin or from different plugins, the first registered handles the extension and the others never start.", "The server binary is never vendored: \"You must install the language server binary separately. LSP plugins configure how Claude Code connects to a language server, but they don't include the server itself.\"", "Placeholder substitution for LSP servers is limited to command, args, env, and workspaceFolder.", "Codex and Cursor publish no plugin LSP surface at their pinned revisions, so the unified bundle's .lsp.json reaches Claude Code only.", "Plugin developer tools reference: `claude plugin validate ` checks plugin.json, hooks/hooks.json, and default-directory Skill, agent, and command frontmatter; manifest-less component directories require 2.1.233 or later.", - "2026-09-03: plugins-reference (plugin validate) documents `--json` as requiring Claude Code v2.1.259 or later, with a report of `success`, `strict`, `target`, `manifest`, and per-file `contents` entries carrying `errors`, `warnings`, and `notes`; the pinned 2.1.250 revision answers `error: unknown option '--json'`, so Agent Bundle gates the flag on the probed version and parses the text report below it.", + "2026-09-03: plugins-reference (plugin validate) documents `--json` as requiring Claude Code v2.1.259 or later, with a report of `success`, `strict`, `target`, `manifest`, and per-file `contents` entries carrying `errors`, `warnings`, and `notes`; the pinned 2.1.260 revision (and 2.1.259) prints that report and the previous 2.1.250 pin answers `error: unknown option '--json'` (exit 1), so Agent Bundle requests `--json` when the probed version is 2.1.259 or later and parses the text reporter below it (host-contracts/claude-plugin-validation.ts; scratch-installed 2.1.250 re-probed 2026-09-03 for the re-pin).", + "2026-09-03: on 2.1.260 `plugin validate --strict --json` lists under `contents` only the files that produced findings (an accepted plugin with hooks/hooks.json and agents/*.md reports `contents: []`), each finding is `{path, message, code}` with `code` null, and the report's `success` agrees with the exit code on all 52 hooks-schema fixture cases (tests/fixtures/claude-hooks-schema/reports/2.1.260).", "2026-09-03: plugin-marketplaces (Marketplace validation errors) states that from a marketplace directory Claude Code does not open the plugins' skill, agent, command, or hook files; because the emitted bundle places .claude-plugin/marketplace.json beside plugin.json, a directory run validates the marketplace only, and artifact validation names each manifest explicitly.", "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; the filename stem is the plugin-namespaced command name.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents plugin-root bin/ as \"Plugin executables added to PATH\", with executables \"Invokable as bare command in Bash tool\"; its file-locations table says: \"Executables added to the Bash tool's PATH and invokable as bare commands while the plugin is enabled. You can't include this directory in a plugin you distribute through claude.ai organization settings\".", @@ -930,14 +944,14 @@ "retrieved 2026-09-02: https://code.claude.com/docs/en/hooks documents PreCompact input as trigger plus nullable custom_instructions and allows decision:block with reason, while discarding continue and systemMessage. PostCompact adds compact_summary, has no decision control, and discards continue and systemMessage.", "2026-09-02: live Claude Code 2.1.257 non-interactive captures recorded PreCompact and PostCompact for manual compaction; scrubbed envelopes are tests/fixtures/events/claude-pre-compact.json and tests/fixtures/events/claude-post-compact.json.", "retrieved 2026-09-03: https://code.claude.com/docs/en/hooks#pretooluse-decision-control documents hookSpecificOutput.permissionDecision allow|deny|ask|defer, where allow skips the permission prompt, ask forces it, and a hook that returns no decision (or no JSON at all) leaves the normal permission flow in place; updatedInput is evaluated against permission rules whether or not a decision accompanies it. The tool/before projection therefore writes permissionDecision only for an explicit allow, ask, or deny and writes nothing for a pass-through result (#461).", - "The hooks reference states prompt_id requires Claude Code 2.1.196 or later; the pinned 2.1.250 release covers that input field without a version bump.", + "The hooks reference states prompt_id requires Claude Code 2.1.196 or later; the pinned 2.1.260 release covers that input field without a version bump.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents an agents/ component with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation: worktree; #100 stage 2 deferred the agents component per the G5 narrowing in #107; the plugin.agents capability rows published by #346 are parity evidence only — the G5 gate still prohibits emitting agent components until a later stage admits them.", "2026-09-01: https://code.claude.com/docs/en/plugins \"Ship default settings with your plugin\": a plugin may include settings.json at the plugin root \"to apply default configuration when the plugin is enabled. Currently, only the `agent` and `subagentStatusLine` keys are supported.\" The file-locations table of https://code.claude.com/docs/en/plugins-reference repeats the same bound: \"Settings | settings.json | Default configuration applied when the plugin is enabled. Only the agent and subagentStatusLine keys are supported\".", "2026-09-01: The same plugins section fixes precedence and host tolerance: \"Settings from `settings.json` take priority over `settings` declared in `plugin.json`. Unknown keys are silently ignored.\" Agent Bundle tightens the silent ignore into the build error claude.settings.field.unknown, the same way an unknown LSP server field is rejected, so a documented component an author asked for is never dropped without a diagnostic; an empty settings object is rejected too, because it declares no default configuration.", "2026-09-01: \"Setting `agent` activates one of the plugin's custom agents as the main thread, applying its system prompt, tool restrictions, and model\"; the documented example value \"security-reviewer\" names an agent in the plugin's agents/ directory. The plugin agents/ component remains deferred by the #100 stage-2 G5 gate recorded in merged PR #220, so this compiler emits no agents/ tree: a declared `agent` resolves only when the author ships that agent by other means, such as the prebuilt payload surface, and the compiler emits the claude.settings.agent.deferred warning to keep the dangling-reference risk visible instead of implying an agents component exists.", "2026-09-01: https://code.claude.com/docs/en/statusline documents subagentStatusLine as a command object - {\"type\": \"command\", \"command\": \"~/.claude/subagent-statusline.sh\"} - whose command \"renders a custom row body for each subagent shown in the agent panel below the prompt\", and records the plugin policy: \"Plugins can ship a default `subagentStatusLine` in their `settings.json`, but unlike hooks, plugin values don't run under `allowManagedHooksOnly` even when the plugin is force-enabled in managed settings `enabledPlugins`.\" The pinned schema admits only the two fields the subagentStatusLine examples show; statusLine's optional `padding` is documented for the user status line alone and is deliberately not admitted.", "2026-09-01: The plugins-reference placeholder table (\"Which fields substitute them inline depends on the plugin component\") enumerates Skill and agent content, hook and monitor commands, MCP stdio and remote fields, and LSP servers - settings.json appears nowhere - so an Agent Bundle path token in a settings value is rejected (claude.settings.token.unsupported) rather than emitted as a placeholder Claude Code never resolves.", - "2026-09-01: Local host proof against the observed Claude Code 2.1.257 binary (newer than the pinned 2.1.250 table): `claude plugin validate --strict` accepts an emitted claude artifact whose plugin root carries settings.json with both supported keys, for both the marketplace-manifest and plugin-manifest validation modes, printing only the validated manifest path and \"Validation passed\". The same command also passes for a deliberately malformed settings.json (empty `agent`, a user-scope `statusLine` key, a stray `padding`) and never names the file, so the host validator does not inspect plugin settings.json at all: the compiler's claude.settings.* diagnostics are the only guard an author gets before the plugin is enabled (host-adapters.native.test.ts).", + "2026-09-01: Local host proof against the observed Claude Code 2.1.257 binary (older than the pinned 2.1.260 table): `claude plugin validate --strict` accepts an emitted claude artifact whose plugin root carries settings.json with both supported keys, for both the marketplace-manifest and plugin-manifest validation modes, printing only the validated manifest path and \"Validation passed\". The same command also passes for a deliberately malformed settings.json (empty `agent`, a user-scope `statusLine` key, a stray `padding`) and never names the file, so the host validator does not inspect plugin settings.json at all: the compiler's claude.settings.* diagnostics are the only guard an author gets before the plugin is enabled (host-adapters.native.test.ts).", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents manifest userConfig as a nonempty option map whose keys are valid identifiers; each option requires type (string, number, boolean, directory, or file), title, and description, and may declare sensitive, required, default, multiple for string arrays, and min/max for numbers.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents `${user_config.KEY}` substitution in MCP and LSP server configs and hook commands, plus non-sensitive substitution in skill and agent content; every option is exported to hook processes as `CLAUDE_PLUGIN_OPTION_` with the key uppercased.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference rejects `${user_config.*}` in shell-form hook commands (use exec form with args or `CLAUDE_PLUGIN_OPTION_`), monitor commands (read a config file), and MCP `headersHelper` (read a config file); before Claude Code v2.1.207 those fields performed substitution.", @@ -953,10 +967,11 @@ "2026-09-01: https://code.claude.com/docs/en/plugin-dependencies requires cross-marketplace targets in `allowCrossMarketplaceDependenciesOn` on the root marketplace; only the root allowlist is consulted, trust does not chain, and users may manually install a blocked dependency first.", "2026-09-01: https://code.claude.com/docs/en/plugin-dependencies documents intersection of constraints from multiple dependents, constrained auto-update, transitive enable, disable refusal while depended upon, release of constraints after uninstall, and pruning only auto-installed orphan dependencies.", "2026-09-01: https://code.claude.com/docs/en/plugin-dependencies exposes dependency-unsatisfied, range-conflict, dependency-version-unsatisfied, and no-matching-tag in `claude plugin list --json` errors.", - "2026-09-03: the plugins reference (\"Component path fields\", \"File locations reference\") documents `hooks/hooks.json` as the default hook location and the manifest `hooks` field (string|array|object) as \"Hook config paths or inline config\" with the example `./my-extra-hooks.json`. Claude Code 2.1.250 (the pinned revision), 2.1.251, 2.1.257, and 2.1.259 all refuse a plugin whose manifest names the auto-loaded file again (probed 2026-09-03 with `claude --plugin-dir plugin list --json`; the rule is not a 2.1.255 regression as ScriptedAlchemy/agent-bundle#462 assumed): `claude plugin list --json` carries `errors: [\"Hook load failed: Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file /hooks/hooks.json. The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference additional hook files.\"]`, the human listing prints `Status: ✘ failed to load`, and no hook, MCP server, or skill from that plugin reaches the session. `claude plugin validate --strict` and `claude plugin details` accept the same manifest, so the list verb is the only surface that exposes the refusal (#463).", + "2026-09-03: the plugins reference (\"Component path fields\", \"File locations reference\") documents `hooks/hooks.json` as the default hook location and the manifest `hooks` field (string|array|object) as \"Hook config paths or inline config\" with the example `./my-extra-hooks.json`. Claude Code 2.1.250 (the previous pin), 2.1.251, 2.1.257, 2.1.259, and 2.1.260 (the pinned revision) all refuse a plugin whose manifest names the auto-loaded file again (probed 2026-09-03 with `claude --plugin-dir plugin list --json`; the rule is not a 2.1.255 regression as ScriptedAlchemy/agent-bundle#462 assumed): `claude plugin list --json` carries `errors: [\"Hook load failed: Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file /hooks/hooks.json. The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference additional hook files.\"]`, the human listing prints `Status: ✘ failed to load`, and no hook, MCP server, or skill from that plugin reaches the session. `claude plugin validate --strict` and `claude plugin details` accept the same manifest, so the list verb is the only surface that exposes the refusal (#463).", "2026-09-03: `claude plugin list --json` (2.1.259) rows carry `id`, `version`, `scope`, `enabled`, `installPath`, `installedAt`, `lastUpdated`, optional `mcpServers`, and — only for a plugin Claude Code failed to load — `errors` as a nonempty array of strings; healthy rows omit the key. `--plugin-dir` rows use the `@inline` id and carry the same `errors` array (#464).", + "2026-09-03: on the pinned 2.1.260 (and 2.1.259) a `--plugin-dir` row is `{id: \"@inline\", version, scope: \"session\", enabled: true, installPath}` with no installedAt or lastUpdated, plus `errors[]` only when the load was refused; the duplicate-hooks refusal text is unchanged from 2.1.259 (schemas/claude/PROVENANCE.json repinNotes).", "2026-09-03: Claude Code 2.1.259 loads exactly one hook set (`plugin details` reports `Hooks (1) SessionStart`) for a plugin that ships `hooks/hooks.json` beside `hooks/hooks-cursor.json` with no manifest `hooks` key, so the unified bundle needs no manifest pointer to keep Claude Code off the Cursor-format document.", - "2026-09-01: Local host proof against the observed Claude Code 2.1.257 binary (newer than the pinned 2.1.250 table): `claude plugin validate --strict` accepts an emitted plugin manifest declaring one bare dependency and one `{name, version}` dependency object and prints \"Validation passed\" (host-adapters.native.test.ts).", + "2026-09-01: Local host proof against the observed Claude Code 2.1.257 binary (older than the pinned 2.1.260 table): `claude plugin validate --strict` accepts an emitted plugin manifest declaring one bare dependency and one `{name, version}` dependency object and prints \"Validation passed\" (host-adapters.native.test.ts).", "2026-09-02 (retrieved 2026-09-02): https://code.claude.com/docs/en/plugin-marketplaces documents required top-level marketplace name, owner, and plugins plus optional $schema, description, version, metadata.pluginRoot, allowCrossMarketplaceDependenciesOn, and renames; owner requires name and optionally accepts email and url.", "2026-09-02 (retrieved 2026-09-02): https://code.claude.com/docs/en/plugin-marketplaces documents plugin-entry name and source plus standard metadata, category, tags, strict, relevance, defaultEnabled, component configuration, headers, and headersHelper. Agent Bundle keeps generated `./` as the default and lets the authored plugin overlay record a distributed relative, github, git URL, git-subdir, npm, archive, or command source.", "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents relative ./ paths, bare names under metadata.pluginRoot, github owner/repo, HTTPS or git@ URL sources, git-subdir repository plus required path, npm package/version/registry, HTTPS archive, and command copy/link as the complete plugin source matrix.", @@ -984,7 +999,7 @@ "2026-09-01: The same Claude Code 2.1.257 strict probe rejects a deprecated top-level `monitors` manifest key, confirming that the documented migration warning is promoted to failure by `--strict`; default-location emission avoids that unstable manifest key entirely.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference requires component paths to be relative to the plugin root and start with `./`, except skills also accepts `.` starting in v2.1.221; before that version `.` failed manifest validation. A marketplace-root source that declares specific skills subdirectories replaces the default skills scan.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents that a default folder shadowed by a replacing manifest path still allows the plugin to load but warns in `claude plugin list` and the `/plugin` detail view. Its file-locations table defines commands/ as flat Markdown Skill files and recommends skills/ for new plugins; Agent Bundle already emits flat `.md` commands in the canonical commands/ directory and deliberately leaves custom-path discovery to the host.", - "2026-09-01: Local host proof against the observed Claude Code 2.1.257 binary (newer than the pinned 2.1.250 table): `claude plugin validate --strict` accepts an emitted plugin manifest containing displayName, object metadata, and defaultEnabled, and separately accepts `\"commands\": \"./custom/deploy.md\"` when that flat Markdown file exists and no default commands/ directory exists (host-adapters.native.test.ts). These positive probes establish acceptance; they do not claim that the CLI checks custom-path existence or contents.", + "2026-09-01: Local host proof against the observed Claude Code 2.1.257 binary (older than the pinned 2.1.260 table): `claude plugin validate --strict` accepts an emitted plugin manifest containing displayName, object metadata, and defaultEnabled, and separately accepts `\"commands\": \"./custom/deploy.md\"` when that flat Markdown file exists and no default commands/ directory exists (host-adapters.native.test.ts). These positive probes establish acceptance; they do not claim that the CLI checks custom-path existence or contents.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents plugin-root `workflows/` as workflow script files and `output-styles/` as output style definitions; their `workflows` and `outputStyles` manifest path fields accept string or array values and replace the corresponding default directory when present.", "2026-09-01: https://code.claude.com/docs/en/output-styles documents custom output styles as Markdown files containing optional frontmatter metadata plus prompt instructions. The documented frontmatter fields are `name`, `description`, `keep-coding-instructions`, and plugin-only `force-for-plugin`; a filename supplies the style name when `name` is omitted.", "2026-09-01: https://code.claude.com/docs/en/plugins-reference documents workflow scripts only as files in `workflows/`, without a deeper file schema, so Agent Bundle treats regular workflow files as opaque payloads.", diff --git a/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json b/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json index 7cea0d03d..beefb3c8d 100644 --- a/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json +++ b/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json @@ -228,6 +228,14 @@ "reason": "retrieved 2026-09-02: the complete rust-v0.147.0 generated hook schema directory contains no file-changed event.", "state": "unavailable" }, + "model-switch/after": { + "reason": "2026-09-03: the complete rust-v0.147.0 generated hook schema directory contains no post-model-switch event, and https://learn.chatgpt.com/docs/hooks (retrieved 2026-09-02) documents no model-switch hook.", + "state": "unavailable" + }, + "model-switch/before": { + "reason": "2026-09-03: the complete rust-v0.147.0 generated hook schema directory contains no pre-model-switch event, and https://learn.chatgpt.com/docs/hooks (retrieved 2026-09-02) documents no model-switch hook.", + "state": "unavailable" + }, "permission/denied": { "reason": "retrieved 2026-09-02: the complete rust-v0.147.0 generated hook schema directory contains no permission-denied event.", "state": "unavailable" diff --git a/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json b/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json index f82c3e619..90671531e 100644 --- a/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json +++ b/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json @@ -149,6 +149,14 @@ "reason": "retrieved 2026-09-02: Cursor documents afterFileEdit as an agent-edit tool variant that overlaps canonical tool/after per the #258 selector rule, not a disk-watch FileChanged equivalent; no dedicated family is inferred without fixture evidence of loss.", "state": "unavailable" }, + "model-switch/after": { + "reason": "2026-09-03: https://cursor.com/docs/hooks (retrieved 2026-09-02) documents no hook that runs after the session model changes; the model reaches Cursor hooks only as the `model` input field of session-scoped events.", + "state": "unavailable" + }, + "model-switch/before": { + "reason": "2026-09-03: https://cursor.com/docs/hooks (retrieved 2026-09-02) documents no hook that runs before a model switch and no output that could block one.", + "state": "unavailable" + }, "permission/denied": { "reason": "retrieved 2026-09-02: the complete https://cursor.com/docs/hooks event inventory documents no permission-denied event.", "state": "unavailable" diff --git a/packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json b/packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json index 2b08a49cd..bb801b763 100644 --- a/packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json +++ b/packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json @@ -62,6 +62,14 @@ "reason": "2026-09-02: Agent Plugins 1.0.0 defines no hooks and therefore no native FileChanged equivalent (https://agent-plugins.org/).", "state": "unavailable" }, + "model-switch/after": { + "reason": "2026-09-03: Agent Plugins 1.0.0 defines no hooks and therefore no native PostModelSwitch equivalent (https://agent-plugins.org/).", + "state": "unavailable" + }, + "model-switch/before": { + "reason": "2026-09-03: Agent Plugins 1.0.0 defines no hooks and therefore no native PreModelSwitch equivalent (https://agent-plugins.org/).", + "state": "unavailable" + }, "permission/denied": { "reason": "2026-09-02: Agent Plugins 1.0.0 defines no hooks and therefore no native PermissionDenied equivalent (https://agent-plugins.org/).", "state": "unavailable" diff --git a/packages/agent-bundle/src/adapters/claude.ts b/packages/agent-bundle/src/adapters/claude.ts index c2cf2d3c7..20da19567 100644 --- a/packages/agent-bundle/src/adapters/claude.ts +++ b/packages/agent-bundle/src/adapters/claude.ts @@ -30,7 +30,7 @@ import { supportedCapability, unavailableCapability, } from './capability-state.ts'; -import capabilityTable from './capabilities/claude-2.1.250.json' with { type: 'json' }; +import capabilityTable from './capabilities/claude-2.1.260.json' with { type: 'json' }; import { createNativeEventStarter, mergeHookDocuments, @@ -150,7 +150,7 @@ export interface ClaudeSubagentStatusLineConfig { /** * Default configuration Claude Code applies when the plugin is enabled, - * emitted as `settings.json` at the plugin root. The pinned 2.1.250 contract + * emitted as `settings.json` at the plugin root. The pinned 2.1.260 contract * supports exactly two keys, and `settings.json` takes priority over * `settings` declared in the manifest. * @@ -431,7 +431,7 @@ const hookContract = Object.freeze({ wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Claude'), } satisfies TargetHookContract); const metadata = Object.freeze({ - adapterRevision: '1.27.0', + adapterRevision: '1.28.0', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); @@ -3432,7 +3432,7 @@ export const claudeAdapter: TargetAdapter = Object.freeze({ pluginReload: unavailableCapability(distributionPolicy.pluginReload.reason), pluginTrustGates: unavailableCapability(distributionPolicy.pluginTrustGates.reason), rules: unavailableCapability( - 'The pinned Claude Code plugin contract (2.1.250) defines no rules component; project guidance ships through CLAUDE.md memory, not a rules directory.', + 'The pinned Claude Code plugin contract (2.1.260) defines no rules component; project guidance ships through CLAUDE.md memory, not a rules directory.', ), settings: capabilityStateFromSupport( capabilityTable.plugin.settings.config === claudeArtifactPaths.settings && diff --git a/packages/agent-bundle/src/adapters/hook-contract.ts b/packages/agent-bundle/src/adapters/hook-contract.ts index 3b9ecf2f8..2676b871a 100644 --- a/packages/agent-bundle/src/adapters/hook-contract.ts +++ b/packages/agent-bundle/src/adapters/hook-contract.ts @@ -307,6 +307,22 @@ export const createNativeEventStarter = ( team_name: 'lifecycle-replay-team', teammate_name: 'lifecycle-replay-teammate', }); + case 'model-switch/before': + case 'model-switch/after': + // hooks reference "PreModelSwitch input" / "PostModelSwitch input": the + // documented example for `/model opus` from a Sonnet session. + return deepFreeze({ + ...base, + cache_ttl: '5m', + context_tokens: 0, + estimated_cache_write_usd: 0, + from_model: 'claude-sonnet-5', + pricing: 'catalog', + prompt_cache_warm: false, + requested_model: 'opus', + source: 'command', + to_model: 'claude-opus-5', + }); case 'workspace/open': return deepFreeze(target === 'cursor' ? { diff --git a/packages/agent-bundle/src/adapters/plugin.ts b/packages/agent-bundle/src/adapters/plugin.ts index 5a91dc938..af66a77ac 100644 --- a/packages/agent-bundle/src/adapters/plugin.ts +++ b/packages/agent-bundle/src/adapters/plugin.ts @@ -17,7 +17,7 @@ import { unavailableCapability, unionCapabilityStates, } from './capability-state.ts'; -import claudeCapabilityTable from './capabilities/claude-2.1.250.json' with { type: 'json' }; +import claudeCapabilityTable from './capabilities/claude-2.1.260.json' with { type: 'json' }; import codexCapabilityTable from './capabilities/codex-0.147.0.json' with { type: 'json' }; import cursorCapabilityTable from './capabilities/cursor-2026-08-28.json' with { type: 'json' }; import { claudeAdapter, claudeArtifactPaths, claudeHooksValidator, planClaudeArtifacts } from './claude.ts'; @@ -240,7 +240,7 @@ const artifactValidation = deepFreeze({ }); const metadata = Object.freeze({ - adapterRevision: '1.28.0', + adapterRevision: '1.29.0', observedVersion: `${claudeAdapter.metadata.observedVersion}+${codexAdapter.metadata.observedVersion}+${cursorAdapter.metadata.observedVersion}`, // Metadata schemas must exactly match the validation contract: each host's // documents, with one shared Claude-format hook schema (the pinned Codex diff --git a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json index f55e1ce6a..913f7ca0b 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json @@ -1,56 +1,57 @@ { - "observedCliVersion": "2.1.250", + "observedCliVersion": "2.1.260", "hostCli": { "package": "@anthropic-ai/claude-code", - "version": "2.1.250", + "version": "2.1.260", "notes": "Exact npm version CI installs for the binary-gated real-host proofs (host-install, packed host-install, packed Claude plugin validation). scripts/host-cli-pins.mjs reads this block, refuses a version that differs from observedCliVersion, and fails the CI job when the installed `claude --version` is not this pin, so a CLI bump is a deliberate re-pin of the schemas observed against it. The proofs need no login: `claude plugin validate/list/details`, `plugin marketplace add`, and `plugin install` all run against an isolated CLAUDE_CONFIG_DIR without credentials." }, "retrievedAt": "2026-09-02", "schemaSource": "https://docs.anthropic.com/en/docs/claude-code/plugins", - "notes": "lsp.schema.json and plugin.json's `lspServers` property were pinned from the Claude Code 2.1.x plugin reference LSP servers section (retrieved 2026-09-01), which documents `.lsp.json` at the plugin root or inline `lspServers` in the manifest, required `command` / `extensionToLanguage`, and the optional `args`, `transport`, `env`, `initializationOptions`, `settings`, `workspaceFolder`, `startupTimeout`, `shutdownTimeout`, `restartOnCrash`, `maxRestarts`, and `diagnostics` fields. `restartOnCrash` and `shutdownTimeout` require Claude Code v2.1.205 or later, which the pinned 2.1.250 revision satisfies. Manifest `lspServers` keeps the documented `string|array|object` union rather than being narrowed to the one emitted form the way `hooks` is; the emitted document itself is `.lsp.json` at the plugin root. Two agent-bundle tightenings over the documented text: a server map and an `extensionToLanguage` map must both be nonempty, because an empty map claims no extension and can never start a server. The current hooks reference at https://code.claude.com/docs/en/hooks supplies the SubagentStart/SubagentStop wire and decision evidence recorded in claude-2.1.250.json. settings.schema.json was pinned (retrieved 2026-09-01) from the \"Ship default settings with your plugin\" section of https://code.claude.com/docs/en/plugins and the file-locations row of https://code.claude.com/docs/en/plugins-reference, which bound the plugin-root settings.json to the `agent` and `subagentStatusLine` keys, plus https://code.claude.com/docs/en/statusline for the subagentStatusLine command-object shape. Three agent-bundle tightenings over the documented text: the closed schema rejects the unknown keys the host \"silently ignores\", so a requested default never disappears at runtime; minProperties 1 rejects an empty settings.json, which declares no default configuration at all; and subagentStatusLine admits only the two fields its own examples show (`type` and `command`) - statusLine's optional `padding` is documented for the user status line, never for the plugin default, so it stays out of the pinned shape. The plugins-reference placeholder table (\"Which fields substitute them inline depends on the plugin component\") lists Skill and agent content, hook and monitor commands, MCP servers, and LSP servers but not settings.json, so the adapter rejects Agent Bundle path tokens in settings values rather than emitting a placeholder Claude Code never resolves. plugin.json's `userConfig` property and closed `userConfigOption` definition were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle deliberately tightens the documented \"valid identifiers\" requirement to `^[A-Za-z_][A-Za-z0-9_]*$`, rejects option keys that collide after uppercasing because both would map to one `CLAUDE_PLUGIN_OPTION_` environment variable, requires the userConfig record to be nonempty, and rejects `sensitive: true` with `default` because a baked-in secure-storage default would ship a secret in the manifest. plugin.schema.json's `dependencies` property was pinned (retrieved 2026-09-01) from https://code.claude.com/docs/en/plugin-dependencies and the manifest schema in https://code.claude.com/docs/en/plugins-reference: a nonempty array whose entries are nonempty plugin-name strings or closed objects with required name and optional version and marketplace strings. Agent Bundle tightens dependency names to the manifest's existing lowercase kebab-case name pattern, rejects an empty array, and closes object fields so malformed declarations fail before distribution; semver range grammar remains plan-time validation because JSON Schema cannot honestly encode npm range syntax. plugin.schema.json's `displayName`, `metadata`, and `defaultEnabled` properties were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle tightens Claude Code's warning-and-ignore handling for a non-object `metadata` value into build error claude.manifest.metadata.invalid, rejects an empty or whitespace-only `displayName` with claude.manifest.displayName.invalid, and rejects a non-boolean `defaultEnabled` with claude.manifest.defaultEnabled.invalid. The normalized generic model currently carries description but not homepage, repository, license, keywords, or `$schema`, so this slice deliberately emits only the three new Claude host-config fields and does not widen the generic model. Component path fields are deliberately excluded from the emitted schema and config surface: the generator owns the canonical default commands/, skills/, hooks/hooks.json, .mcp.json, .lsp.json, settings.json, workflows/, and output-styles/ layout, while custom replace/add path rules remain documented host-discovery evidence in claude-2.1.250.json. plugin.json's `channels` property was pinned from the Channels section of https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01): a nonempty array of closed declarations with required nonempty `server` and optional per-channel `userConfig` reusing the top-level option definition. Agent Bundle tightens the documented contract by rejecting an empty channels array, empty per-channel userConfig, unknown channel fields, invalid or uppercase-colliding option identifiers, and any server name absent from the MCP server map successfully planned for the selected target. Duplicate channel declarations for one server remain allowed and preserve authored order because the reference imposes no uniqueness rule. Per-channel `sensitive: true` follows the top-level secure-storage semantics because the reference says the channel field uses the same schema; the existing prohibition on sensitive defaults therefore applies unchanged. Claude Code 2.1.257 strict validation accepts both valid bindings and deliberately dangling server names, so claude.channels.server.unknown is an intentional compiler tightening and the only pre-enable cross-document binding guard. Workflows and output styles deliberately reuse the bin slice's normalized directory/file payload shape and byte-faithful copy entries, but retain separate registry hooks, normalized fields, destination paths, and diagnostics so the executable policy cannot leak into non-executable components. The shared enumerator preserves source file modes through copy entries and rejects lexical or realpath escapes, including a configured directory symlink that resolves outside the project. The adapter emits only the canonical plugin-root workflows/ and output-styles/ directories, so it does not emit the optional `workflows` or `outputStyles` manifest path fields and leaves plugin.schema.json plus its SHA-256 pin unchanged. https://code.claude.com/docs/en/output-styles (retrieved 2026-09-01) explicitly defines output styles as Markdown, so Agent Bundle tightens the directory to `.md` files with claude.outputStyles.file.invalid. It does not validate frontmatter: `name` is optional because the filename supplies it, other documented fields are optional, and Claude Code 2.1.257 strict plugin validation accepts a Markdown output style with no frontmatter. The plugins reference gives workflow scripts no deeper file schema, so workflow file contents and suffixes remain opaque. monitors.schema.json and theme.schema.json were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01), which marks both components experimental and warns their manifest schema may change between releases. The monitor schema is a nonempty array of closed entries requiring nonempty unique name, command, and description; optional when admits only always or on-skill-invoke:, and the plan tightens the latter by requiring the named skill to be emitted by this plugin. The schema and plan both reject ${user_config.*} in monitor commands because Claude Code rejects it at shell execution time and supplies no CLAUDE_PLUGIN_OPTION_ variables to monitors. The theme schema is closed, requires a nonempty base, and admits optional nonempty name plus a sparse string-valued overrides map. Agent Bundle further requires a nonempty themes declaration, safe file-stem keys, defaults omitted name to that key, and rejects empty override strings; it deliberately accepts any nonempty color string because the reference shows hex examples but does not mandate hex syntax. Generated themes and monitors use the default locations, so no unstable experimental.* manifest fields are emitted. Local proof against Claude Code 2.1.257 shows strict validation accepts valid emitted themes and monitors but does not inspect either default-location document's contents: malformed themes missing base and carrying non-string overrides pass, as do monitors missing command. The pinned schemas and compiler diagnostics are therefore the content guard. The same strict host rejects the deprecated top-level monitors manifest key, so default-location emission also avoids the warning-to-error migration path. The artifact contract records themes/*.json as a schema family and validates every concrete generated theme path. marketplace.schema.json was re-pinned from https://code.claude.com/docs/en/plugin-marketplaces, https://code.claude.com/docs/en/plugins-reference, https://code.claude.com/docs/en/plugin-dependencies, and the linked relevance contract (retrieved 2026-09-02). It loosens the old generated-only snapshot by making top-level description and plugin-entry description/version optional, allowing documented owner email/url, top-level $schema/version/metadata/renames/allowCrossMarketplaceDependenciesOn, and every documented plugin-entry metadata, component, relevance, authentication, dependency, channel, userConfig, settings, and experimental field; plugin-entry version is now any nonempty documented version string rather than generated-only semver. It deliberately retains only the relative ./ source form: github, git URL, git-subdir, npm, archive, and command sources are deferred to the source-matrix follow-up. The authored overlay is narrower than the validation schema and does not expose plugin name/source or component routing, because generated identity/layout remains authoritative and the agents component remains gate-deferred. Agent Bundle tightens the prose contract by closing every structured object (free-form metadata uses recursive patternProperties with additionalProperties false), requiring nonempty arrays/maps where an empty declaration has no effect, requiring unique kebab-case cross-marketplace allowlist names and kebab-case rename keys/targets, rejecting the documented exact reserved marketplace names, requiring relative sources and component paths to stay inside the marketplace, limiting owner/author URLs to absolute HTTP(S), requiring metadata.pluginRoot to be an internal ./ path, requiring nonempty header names/values, and requiring explicit strict:false with headersHelper. Relevance enforces the documented topic, item-count, character-count, bare-hostname, and regular-expression bounds; Claude Code 2.1.257 strict validation does not reject an empty allowCrossMarketplaceDependenciesOn entry. The same native validator warns that headers and headersHelper have no effect on the generated relative source and promotes that warning to failure under --strict, so the authored overlay rejects both fields with claude.marketplace.plugin.headers.inapplicable and claude.marketplace.plugin.headersHelper.inapplicable rather than emitting a manifest the host's strict validation refuses; the pinned schema keeps the documented fields, including the strict:false requirement beside headersHelper, for the URL-capable source-matrix follow-up.", + "notes": "lsp.schema.json and plugin.json's `lspServers` property were pinned from the Claude Code 2.1.x plugin reference LSP servers section (retrieved 2026-09-01), which documents `.lsp.json` at the plugin root or inline `lspServers` in the manifest, required `command` / `extensionToLanguage`, and the optional `args`, `transport`, `env`, `initializationOptions`, `settings`, `workspaceFolder`, `startupTimeout`, `shutdownTimeout`, `restartOnCrash`, `maxRestarts`, and `diagnostics` fields. `restartOnCrash` and `shutdownTimeout` require Claude Code v2.1.205 or later, which the pinned 2.1.260 revision satisfies. Manifest `lspServers` keeps the documented `string|array|object` union rather than being narrowed to the one emitted form the way `hooks` is; the emitted document itself is `.lsp.json` at the plugin root. Two agent-bundle tightenings over the documented text: a server map and an `extensionToLanguage` map must both be nonempty, because an empty map claims no extension and can never start a server. The current hooks reference at https://code.claude.com/docs/en/hooks supplies the SubagentStart/SubagentStop wire and decision evidence recorded in claude-2.1.260.json. settings.schema.json was pinned (retrieved 2026-09-01) from the \"Ship default settings with your plugin\" section of https://code.claude.com/docs/en/plugins and the file-locations row of https://code.claude.com/docs/en/plugins-reference, which bound the plugin-root settings.json to the `agent` and `subagentStatusLine` keys, plus https://code.claude.com/docs/en/statusline for the subagentStatusLine command-object shape. Three agent-bundle tightenings over the documented text: the closed schema rejects the unknown keys the host \"silently ignores\", so a requested default never disappears at runtime; minProperties 1 rejects an empty settings.json, which declares no default configuration at all; and subagentStatusLine admits only the two fields its own examples show (`type` and `command`) - statusLine's optional `padding` is documented for the user status line, never for the plugin default, so it stays out of the pinned shape. The plugins-reference placeholder table (\"Which fields substitute them inline depends on the plugin component\") lists Skill and agent content, hook and monitor commands, MCP servers, and LSP servers but not settings.json, so the adapter rejects Agent Bundle path tokens in settings values rather than emitting a placeholder Claude Code never resolves. plugin.json's `userConfig` property and closed `userConfigOption` definition were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle deliberately tightens the documented \"valid identifiers\" requirement to `^[A-Za-z_][A-Za-z0-9_]*$`, rejects option keys that collide after uppercasing because both would map to one `CLAUDE_PLUGIN_OPTION_` environment variable, requires the userConfig record to be nonempty, and rejects `sensitive: true` with `default` because a baked-in secure-storage default would ship a secret in the manifest. plugin.schema.json's `dependencies` property was pinned (retrieved 2026-09-01) from https://code.claude.com/docs/en/plugin-dependencies and the manifest schema in https://code.claude.com/docs/en/plugins-reference: a nonempty array whose entries are nonempty plugin-name strings or closed objects with required name and optional version and marketplace strings. Agent Bundle tightens dependency names to the manifest's existing lowercase kebab-case name pattern, rejects an empty array, and closes object fields so malformed declarations fail before distribution; semver range grammar remains plan-time validation because JSON Schema cannot honestly encode npm range syntax. plugin.schema.json's `displayName`, `metadata`, and `defaultEnabled` properties were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle tightens Claude Code's warning-and-ignore handling for a non-object `metadata` value into build error claude.manifest.metadata.invalid, rejects an empty or whitespace-only `displayName` with claude.manifest.displayName.invalid, and rejects a non-boolean `defaultEnabled` with claude.manifest.defaultEnabled.invalid. The normalized generic model currently carries description but not homepage, repository, license, keywords, or `$schema`, so this slice deliberately emits only the three new Claude host-config fields and does not widen the generic model. Component path fields are deliberately excluded from the emitted schema and config surface: the generator owns the canonical default commands/, skills/, hooks/hooks.json, .mcp.json, .lsp.json, settings.json, workflows/, and output-styles/ layout, while custom replace/add path rules remain documented host-discovery evidence in claude-2.1.260.json. plugin.json's `channels` property was pinned from the Channels section of https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01): a nonempty array of closed declarations with required nonempty `server` and optional per-channel `userConfig` reusing the top-level option definition. Agent Bundle tightens the documented contract by rejecting an empty channels array, empty per-channel userConfig, unknown channel fields, invalid or uppercase-colliding option identifiers, and any server name absent from the MCP server map successfully planned for the selected target. Duplicate channel declarations for one server remain allowed and preserve authored order because the reference imposes no uniqueness rule. Per-channel `sensitive: true` follows the top-level secure-storage semantics because the reference says the channel field uses the same schema; the existing prohibition on sensitive defaults therefore applies unchanged. Claude Code 2.1.257 strict validation accepts both valid bindings and deliberately dangling server names, so claude.channels.server.unknown is an intentional compiler tightening and the only pre-enable cross-document binding guard. Workflows and output styles deliberately reuse the bin slice's normalized directory/file payload shape and byte-faithful copy entries, but retain separate registry hooks, normalized fields, destination paths, and diagnostics so the executable policy cannot leak into non-executable components. The shared enumerator preserves source file modes through copy entries and rejects lexical or realpath escapes, including a configured directory symlink that resolves outside the project. The adapter emits only the canonical plugin-root workflows/ and output-styles/ directories, so it does not emit the optional `workflows` or `outputStyles` manifest path fields and leaves plugin.schema.json plus its SHA-256 pin unchanged. https://code.claude.com/docs/en/output-styles (retrieved 2026-09-01) explicitly defines output styles as Markdown, so Agent Bundle tightens the directory to `.md` files with claude.outputStyles.file.invalid. It does not validate frontmatter: `name` is optional because the filename supplies it, other documented fields are optional, and Claude Code 2.1.257 strict plugin validation accepts a Markdown output style with no frontmatter. The plugins reference gives workflow scripts no deeper file schema, so workflow file contents and suffixes remain opaque. monitors.schema.json and theme.schema.json were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01), which marks both components experimental and warns their manifest schema may change between releases. The monitor schema is a nonempty array of closed entries requiring nonempty unique name, command, and description; optional when admits only always or on-skill-invoke:, and the plan tightens the latter by requiring the named skill to be emitted by this plugin. The schema and plan both reject ${user_config.*} in monitor commands because Claude Code rejects it at shell execution time and supplies no CLAUDE_PLUGIN_OPTION_ variables to monitors. The theme schema is closed, requires a nonempty base, and admits optional nonempty name plus a sparse string-valued overrides map. Agent Bundle further requires a nonempty themes declaration, safe file-stem keys, defaults omitted name to that key, and rejects empty override strings; it deliberately accepts any nonempty color string because the reference shows hex examples but does not mandate hex syntax. Generated themes and monitors use the default locations, so no unstable experimental.* manifest fields are emitted. Local proof against Claude Code 2.1.257 shows strict validation accepts valid emitted themes and monitors but does not inspect either default-location document's contents: malformed themes missing base and carrying non-string overrides pass, as do monitors missing command. The pinned schemas and compiler diagnostics are therefore the content guard. The same strict host rejects the deprecated top-level monitors manifest key, so default-location emission also avoids the warning-to-error migration path. The artifact contract records themes/*.json as a schema family and validates every concrete generated theme path. marketplace.schema.json was re-pinned from https://code.claude.com/docs/en/plugin-marketplaces, https://code.claude.com/docs/en/plugins-reference, https://code.claude.com/docs/en/plugin-dependencies, and the linked relevance contract (retrieved 2026-09-02). It loosens the old generated-only snapshot by making top-level description and plugin-entry description/version optional, allowing documented owner email/url, top-level $schema/version/metadata/renames/allowCrossMarketplaceDependenciesOn, and every documented plugin-entry metadata, component, relevance, authentication, dependency, channel, userConfig, settings, and experimental field; plugin-entry version is now any nonempty documented version string rather than generated-only semver. It deliberately retains only the relative ./ source form: github, git URL, git-subdir, npm, archive, and command sources are deferred to the source-matrix follow-up. The authored overlay is narrower than the validation schema and does not expose plugin name/source or component routing, because generated identity/layout remains authoritative and the agents component remains gate-deferred. Agent Bundle tightens the prose contract by closing every structured object (free-form metadata uses recursive patternProperties with additionalProperties false), requiring nonempty arrays/maps where an empty declaration has no effect, requiring unique kebab-case cross-marketplace allowlist names and kebab-case rename keys/targets, rejecting the documented exact reserved marketplace names, requiring relative sources and component paths to stay inside the marketplace, limiting owner/author URLs to absolute HTTP(S), requiring metadata.pluginRoot to be an internal ./ path, requiring nonempty header names/values, and requiring explicit strict:false with headersHelper. Relevance enforces the documented topic, item-count, character-count, bare-hostname, and regular-expression bounds; Claude Code 2.1.257 strict validation does not reject an empty allowCrossMarketplaceDependenciesOn entry. The same native validator warns that headers and headersHelper have no effect on the generated relative source and promotes that warning to failure under --strict, so the authored overlay rejects both fields with claude.marketplace.plugin.headers.inapplicable and claude.marketplace.plugin.headersHelper.inapplicable rather than emitting a manifest the host's strict validation refuses; the pinned schema keeps the documented fields, including the strict:false requirement beside headersHelper, for the URL-capable source-matrix follow-up.", "sourceMatrixNotes": "Source-matrix addendum (retrieved 2026-09-02): marketplace.schema.json now admits the documented relative, github, git URL, git-subdir, npm, archive, and command plugin sources as closed alternatives. Agent Bundle tightens relative paths and git-subdir paths against lexical escapes; GitHub shorthand to one owner/repo pair; git URLs to HTTPS or git@ forms; npm names to npm's lowercase package grammar and npm versions to exact, ^, or ~ semantic versions; registry URLs to HTTP(S); archive URLs to HTTPS with lexical rejection for localhost, IPv4/IPv6 loopback, 169.254/16 and fe80::/10 link-local addresses, and known AWS, Azure, and Google metadata hostnames; git sha to 40 hex; archive sha256 to 64 case-insensitive hex; command text to 1-500 printable ASCII characters without four-space runs; timeout to integer seconds from 1 through 600; and mode to copy or link. Entry headers remain a nonempty record of nonempty string values, and headersHelper requires a nonempty command plus strict:false. These checks do not resolve DNS, follow redirects, contact registries or repositories, download archives, verify archive bytes, execute command sources, inspect produced directories, or attempt to predict Windows link-mode support; those remain native install-time behavior.", - "agentsComponentNotes": "Agents-component addendum (retrieved 2026-09-02): https://code.claude.com/docs/en/plugins-reference documents plugin-root agents/ Markdown files with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter, with worktree as the only valid plugin-agent isolation value. https://code.claude.com/docs/en/sub-agents documents each field's runtime behavior and states that hooks, mcpServers, and permissionMode are ignored for plugin subagents. Local Claude Code 2.1.257 `claude plugin validate --strict` accepts one plugin agent exercising all supported fields and also accepts a negative fixture declaring hooks, mcpServers, and permissionMode: bypassPermissions without warning, so strict validation does not enforce the loader-side ignore contract. Agent Bundle adds no agent schema or emission surface in this revision: merged PR #220 and #107 revision 3 deliberately keep agents behind the #100 stage-2 G5 gate pending a separate evidence-backed re-admission increment. Drift addendum (#478, uploaded 2026-09-03 sub-agents reference sub-agents-3.md, \"Supported frontmatter fields\"): the contract gained color (red, blue, green, yellow, purple, orange, pink, cyan), initialPrompt (main-session agents via --agent or the agent setting), and experimental.cacheTtl (5m or 1h, Claude Code 2.1.248 or later, read only from subagent files); permissionMode, mcpServers, and hooks are each documented as \"Ignored for plugin subagents\"; name may not contain a colon (reserved for plugin-scoped identifiers, unloaded since 2.1.218) and hooks receive it as agent_type. \"Project-level hooks for subagent events\" and the hooks reference's SubagentStart matcher row give the plugin-scoped matcher form ^:$, anchored because the colon puts it on the unanchored regular-expression path. claude-2.1.250.json plugin.agents and hooks.agentTypeMatcher record these rows; the component stays deferred.", + "agentsComponentNotes": "Agents-component addendum (retrieved 2026-09-02): https://code.claude.com/docs/en/plugins-reference documents plugin-root agents/ Markdown files with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter, with worktree as the only valid plugin-agent isolation value. https://code.claude.com/docs/en/sub-agents documents each field's runtime behavior and states that hooks, mcpServers, and permissionMode are ignored for plugin subagents. Local Claude Code 2.1.257 `claude plugin validate --strict` accepts one plugin agent exercising all supported fields and also accepts a negative fixture declaring hooks, mcpServers, and permissionMode: bypassPermissions without warning, so strict validation does not enforce the loader-side ignore contract. Agent Bundle adds no agent schema or emission surface in this revision: merged PR #220 and #107 revision 3 deliberately keep agents behind the #100 stage-2 G5 gate pending a separate evidence-backed re-admission increment. Drift addendum (#478, uploaded 2026-09-03 sub-agents reference sub-agents-3.md, \"Supported frontmatter fields\"): the contract gained color (red, blue, green, yellow, purple, orange, pink, cyan), initialPrompt (main-session agents via --agent or the agent setting), and experimental.cacheTtl (5m or 1h, Claude Code 2.1.248 or later, read only from subagent files); permissionMode, mcpServers, and hooks are each documented as \"Ignored for plugin subagents\"; name may not contain a colon (reserved for plugin-scoped identifiers, unloaded since 2.1.218) and hooks receive it as agent_type. \"Project-level hooks for subagent events\" and the hooks reference's SubagentStart matcher row give the plugin-scoped matcher form ^:$, anchored because the colon puts it on the unanchored regular-expression path. claude-2.1.260.json plugin.agents and hooks.agentTypeMatcher record these rows; the component stays deferred.", "hooksManifestNotes": "Manifest `hooks` addendum (2026-09-03, Claude Code plugins reference \"Plugin manifest schema\" > \"Component path fields\" and \"Plugin directory structure\" > \"File locations reference\"; #462/#463, adapter fix in #470): the default hooks location is `hooks/hooks.json`, which Claude Code loads on its own, and the manifest `hooks` field (string|array|object) is documented as \"Hook config paths or inline config\" with the example `./my-extra-hooks.json`, i.e. additional hook files. Claude Code 2.1.250, 2.1.251, 2.1.257, and 2.1.259 (`claude --plugin-dir plugin list --json`, observed 2026-09-03) all refuse a plugin whose manifest names the auto-loaded file again with `errors: [\"Hook load failed: Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file /hooks/hooks.json. The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference additional hook files.\"]`; `claude plugin validate --strict` and `claude plugin details` accept that manifest, so only the list verb exposes the refusal. plugin.schema.json therefore admits the documented `./`-prefixed path, path array, and inline object forms for `hooks` but rejects the literal `./hooks/hooks.json`, and the Claude and unified `plugin` adapters emit the hook document at the default location without a manifest `hooks` key (#470). The same 2.1.259 binary lists exactly one hook set for a plugin shipping `hooks/hooks.json` beside `hooks/hooks-cursor.json` and no manifest `hooks` key, so the unified bundle needs no pointer to keep Claude Code off the Cursor document.", - "hooksSchemaNotes": "hooks.schema.json was re-pinned (uploaded 2026-09-03 hooks reference hooks-2.md, #477) from the single shell-form command handler to the documented handler union: \"Hook handler fields\" names five types (command, http, mcp_tool, prompt, agent); \"Common fields\" gives every type if, timeout, statusMessage, and once; \"Command hook fields\" adds command, args (exec form), async, asyncRewake, and shell (bash or powershell); \"HTTP hook fields\" adds url, headers, and allowedEnvVars; \"MCP tool hook fields\" adds server, tool, and input; \"Prompt and agent hook fields\" adds prompt and model, with continueOnBlock on prompt hooks only (\"Agent hook configuration\": agent hooks have no continueOnBlock field). Each handler carries the documented required fields (command; url; server and tool; prompt) and is closed. The `hooks` map is closed to the 31 events under \"Hook events\" that the pinned 2.1.250 host knows: PreModelSwitch and PostModelSwitch are documented there too but \"require Claude Code v2.1.251 or later\", 2.1.250 --strict rejects both keys (`Invalid key in record`), so they stay out until the observedCliVersion re-pin moves past that gate. \"Prompt-based hooks\" partitions the admitted events: 13 admit all five types, 16 admit command, http, and mcp_tool, and SessionStart and Setup admit command and mcp_tool only. Cross-check against the pinned Claude Code 2.1.250 (`claude plugin validate --strict`; that release has no --json, so its text reporter is recorded into the same report shape) and against 2.1.260 (`--strict --json`), both under tests/fixtures/claude-hooks-schema and recorded by scripts/record-claude-hooks-schema-fixtures.mjs: each CLI accepts a document using every type and field and a document naming all 31 admitted events, and each rejects under --strict an unknown event, an unknown handler type, a missing type, each missing required field, and each wrong-typed field (timeout 0 or a string, shell other than bash/powershell, non-string args, non-object headers or input, non-array allowedEnvVars, non-boolean async/once, non-string if/statusMessage/matcher, a non-string description, and a document without `hooks`), all of which the schema rejects too. The two CLIs disagree on exactly one case, the 2.1.251 model-switch events above. Agent Bundle tightenings neither CLI shares: unknown keys on the document, a matcher group, or a handler are rejected (plugins-reference \"plugin validate\" documents unrecognized manifest fields as warnings that --strict promotes to errors, but both 2.1.250 and 2.1.260 --strict ignore unknown hooks.json keys silently, so the closed pin is the only guard against a misspelled field); a field of another handler type (args or async on http, continueOnBlock on agent, model or url on command, shell or asyncRewake on mcp_tool) is rejected rather than silently ignored; a prompt, agent, or http handler on an event the reference excludes it from is rejected although the CLI accepts it; and the pre-existing minLength 1 on command (now also server, tool, prompt, url, model), integer timeout (the hook contract lowers timeoutMs to whole seconds; the CLI accepts 1.5), and minItems 1 on a group's hooks array stay, where both CLIs accept empty strings, fractional timeouts, and empty groups. The undocumented top-level `modules` key both validators mention is not admitted. The compiler still emits only shell-form command handlers with command and timeout; exec form, async, if, and statusMessage are pass-through fields for authored nativeHooks documents until a separate emission increment.", + "hooksSchemaNotes": "hooks.schema.json was re-pinned (uploaded 2026-09-03 hooks reference hooks-2.md, #477) from the single shell-form command handler to the documented handler union: \"Hook handler fields\" names five types (command, http, mcp_tool, prompt, agent); \"Common fields\" gives every type if, timeout, statusMessage, and once; \"Command hook fields\" adds command, args (exec form), async, asyncRewake, and shell (bash or powershell); \"HTTP hook fields\" adds url, headers, and allowedEnvVars; \"MCP tool hook fields\" adds server, tool, and input; \"Prompt and agent hook fields\" adds prompt and model, with continueOnBlock on prompt hooks only (\"Agent hook configuration\": agent hooks have no continueOnBlock field). Each handler carries the documented required fields (command; url; server and tool; prompt) and is closed. The `hooks` map is closed to the 33 events under \"Hook events\" that the pinned 2.1.260 host knows. PreModelSwitch and PostModelSwitch \"require Claude Code v2.1.251 or later\": the previous 2.1.250 pin rejected both keys under --strict (`Invalid key in record`) and the schema kept them out until the re-pin to 2.1.260 (2026-09-03, #477 follow-up) admitted them with the command/http/mcp_tool partition \"Prompt-based hooks\" documents for them (\"PreModelSwitch runs command, http, and mcp_tool hooks only\"). \"Prompt-based hooks\" partitions the admitted events: 13 admit all five types, 18 admit command, http, and mcp_tool, and SessionStart and Setup admit command and mcp_tool only. Cross-check against the pinned Claude Code 2.1.260 (`claude plugin validate --strict --json`) and against the previous 2.1.250 pin (`--strict`; that release has no --json, so its text reporter is recorded into the same report shape), both under tests/fixtures/claude-hooks-schema and recorded by scripts/record-claude-hooks-schema-fixtures.mjs: each CLI accepts a document using every type and field, 2.1.260 accepts a document naming all 33 admitted events (2.1.250 rejects that document on the two model-switch keys), and each rejects under --strict an unknown event, an unknown handler type, a missing type, each missing required field, and each wrong-typed field (timeout 0 or a string, shell other than bash/powershell, non-string args, non-object headers or input, non-array allowedEnvVars, non-boolean async/once, non-string if/statusMessage/matcher, a non-string description, and a document without `hooks`), all of which the schema rejects too. The two CLIs disagree only on the 2.1.251 model-switch events above (cases model-switch-events and all-pinned-events): rejected by 2.1.250, accepted by the pinned 2.1.260 and by the schema. 2.1.260 --strict also accepts a prompt handler on PostModelSwitch, which \"Prompt-based hooks\" excludes, so the schema's per-event partition is the only guard there as it is for the other command/http/mcp_tool events. Agent Bundle tightenings neither CLI shares: unknown keys on the document, a matcher group, or a handler are rejected (plugins-reference \"plugin validate\" documents unrecognized manifest fields as warnings that --strict promotes to errors, but both 2.1.250 and 2.1.260 --strict ignore unknown hooks.json keys silently, so the closed pin is the only guard against a misspelled field); a field of another handler type (args or async on http, continueOnBlock on agent, model or url on command, shell or asyncRewake on mcp_tool) is rejected rather than silently ignored; a prompt, agent, or http handler on an event the reference excludes it from is rejected although the CLI accepts it; and the pre-existing minLength 1 on command (now also server, tool, prompt, url, model), integer timeout (the hook contract lowers timeoutMs to whole seconds; the CLI accepts 1.5), and minItems 1 on a group's hooks array stay, where both CLIs accept empty strings, fractional timeouts, and empty groups. The undocumented top-level `modules` key both validators mention is not admitted. The compiler still emits only shell-form command handlers with command and timeout; exec form, async, if, and statusMessage are pass-through fields for authored nativeHooks documents until a separate emission increment.", + "repinNotes": "Re-pin 2.1.250 -> 2.1.260 (2026-09-03). The exact CLI surface was diffed with an isolated CLAUDE_CONFIG_DIR/HOME (scratch `npm install --prefix @anthropic-ai/claude-code@2.1.250` beside the 2.1.259 and 2.1.260 native binaries): `claude plugin --help`, `claude plugin marketplace --help`, and the --help of every plugin subcommand (install, uninstall, enable, disable, update, list, details, validate, prune, tag, init) and marketplace subcommand (add, list, remove, update) are byte-identical between 2.1.250 and 2.1.260 except `claude plugin validate --help`, which gains `--json Output the validation report as JSON (same exit codes)`; 2.1.259 and 2.1.260 differ only in the version string. `plugin validate --strict --json` (2.1.259+) returns {success, strict, target, manifest{file,type,errors,warnings,notes}, contents[]} where contents lists only files with findings and each finding is {path, message, code}; 2.1.250 answers `error: unknown option '--json'` (exit 1), so host-contracts/claude-plugin-validation.ts requests --json when the probed version is 2.1.259 or later and parses the text reporter below that. `claude --plugin-dir plugin list --json` rows for a session-loaded plugin carry id (@inline), version, scope \"session\", enabled, installPath, and errors[] only when the load was refused; installed rows additionally carry installedAt, lastUpdated, and optional mcpServers (2.1.259 evidence in claude-2.1.260.json). The duplicate-`hooks` manifest refusal (#470/#479) is unchanged: 2.1.259 and 2.1.260 both list `Hook load failed: Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file /hooks/hooks.json ...` while `plugin validate --strict --json` reports success. Marketplace source constraints: 2.1.250 reports a short github sha as `plugins.0.source: Invalid input`; 2.1.260 reports `plugins.0.source.sha: Invalid input` plus `plugins.0.source.sha: Must be a full 40-character lowercase git commit SHA` (the wording host-adapters.native.test.ts pins). Hook events: 2.1.260 --strict accepts PreModelSwitch and PostModelSwitch (v2.1.251+) and every other event under \"Hook events\"; hooks.schema.json admits both. Agents frontmatter: 2.1.260 --strict --json accepts an agents/*.md declaring color, initialPrompt, and experimental.cacheTtl (sub-agents-3.md \"Supported frontmatter fields\"), and equally accepts an unknown `colour` key, `cacheTtl: 2h`, and the hooks/mcpServers/permissionMode fields the reference marks ignored for plugin subagents; its only agent finding is the `description` warning (`No description in frontmatter ...`), which --strict promotes to failure, so the validator does not enforce the frontmatter vocabulary and the agents component stays behind the #100 stage-2 G5 gate with the capability rows updated. Nothing observed on 2.1.260 contradicts the uploaded hooks-2.md, plugins-reference-1.md, sub-agents-3.md, or plugin-marketplaces-0.md; the validator is looser than the documents in the places recorded above and in hooksSchemaNotes.", "schemas": { "hooks.schema.json": { - "bytes": 7086, - "sha256": "7535806f86481b89b628a0ccf8e34261e6f93163f562c2146ca8dbfe4d95766e", + "bytes": 7229, + "sha256": "0cdfc5eb5201f2c3091768559ac82f4c563ccb1b7bce7a39d5f99e5f404654cb", "url": "https://code.claude.com/docs/en/hooks" }, "lsp.schema.json": { "bytes": 1369, - "sha256": "c81fd2f57c410f70f8e5c3f84483f5ec1b575ee02802b424977826f757dccd8e", + "sha256": "b4419c5d857267c7e2b21e3e1eb98b4fdc302c87109007190d3258e6ba7096e4", "url": "https://docs.anthropic.com/en/docs/claude-code/plugins" }, "marketplace.schema.json": { "bytes": 14895, - "sha256": "4ffa94e8024966e8080b9d3b338c9612bdfa255802a8491b43f74f90427bc988", + "sha256": "31ee4cc43ba5ce2be248030a69da2d31171550b2cc51c1fcb9f788a8ab92783d", "url": "https://code.claude.com/docs/en/plugin-marketplaces" }, "mcp.schema.json": { "bytes": 1279, - "sha256": "76ccf02c7bfe2d57945ba18e84da8d655529bd68b4d692f72bce28238c99067e", + "sha256": "edd4770e41d6aee5beae1ff918d33139613a243e454ecbd52b72fa824be4a662", "url": "https://docs.anthropic.com/en/docs/claude-code/mcp" }, "monitors.schema.json": { "bytes": 679, - "sha256": "d45abdf7561e4316ba217ce9c2ac84f32e1049622b74abb081693b479a54b48d", + "sha256": "6378d94b51fb7c784eaee178237008a03da0f889d0520d57486784954a1d484c", "url": "https://code.claude.com/docs/en/plugins-reference" }, "plugin.schema.json": { "bytes": 7116, - "sha256": "e8524e1b1bf9d72f6866c31b37b7312147fcf29d0504f7e5dce5719bc908830b", + "sha256": "2a976091b81ad07ae8eca57f6f9c5749efeba17fa089a0c12b6ca84d6e70f118", "url": "https://code.claude.com/docs/en/plugins-reference" }, "settings.schema.json": { "bytes": 554, - "sha256": "9e86d8c5e4053e8de0e468d349e2c3dde5834d22d6769372b88570e301700073", + "sha256": "2bbca553621dbf9433a9b7d1ff7952543a368434f2d999a4e4e360b95b6d4c3d", "url": "https://code.claude.com/docs/en/plugins" }, "theme.schema.json": { "bytes": 451, - "sha256": "721aa9b0bc7c60cd9359343e5c7205ffbdfea82da312f601720c9dfaa2d8cf0d", + "sha256": "9931264e6f5a1d4b3b854ce7a17d602c9ba1b57cc1991f1d3991b316eaa81ac2", "url": "https://code.claude.com/docs/en/plugins-reference" } }, @@ -62,7 +63,8 @@ "`claude plugin validate ` validates plugin.json, hooks/hooks.json, and frontmatter in default skills, agents, and commands directories; manifest-less directories require Claude Code 2.1.233 or later.", "`--strict` treats warnings as errors and is recommended for CI, including unrecognized or near-miss fields and non-object experimental/metadata values.", "`claude --plugin-dir plugin list` includes session-loaded plugins only when the same flag precedes the subcommand; `--json` provides machine-readable registration evidence.", - "`claude plugin details` owns component inventory and token-cost estimation; Agent Bundle does not reimplement its count_tokens or character-fallback costing." + "`claude plugin details` owns component inventory and token-cost estimation; Agent Bundle does not reimplement its count_tokens or character-fallback costing.", + "2026-09-03: `claude plugin validate --json` is present on 2.1.259 and 2.1.260 (`--help`: \"Output the validation report as JSON (same exit codes)\") and absent on 2.1.250; the pinned 2.1.260 report shape is {success, strict, target, manifest, contents[]} with per-file {file, type, errors[], warnings[], notes[]} entries and {path, message, code} findings." ] } } diff --git a/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json index d1d2a0b1a..0112ccec0 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/hooks.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/hooks.schema.json", "$defs": { "commandHandler": { "type": "object", @@ -166,10 +166,12 @@ "PermissionDenied": { "$ref": "#/$defs/allHandlerGroups" }, "PermissionRequest": { "$ref": "#/$defs/allHandlerGroups" }, "PostCompact": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PostModelSwitch": { "$ref": "#/$defs/nonModelHandlerGroups" }, "PostToolBatch": { "$ref": "#/$defs/allHandlerGroups" }, "PostToolUse": { "$ref": "#/$defs/allHandlerGroups" }, "PostToolUseFailure": { "$ref": "#/$defs/allHandlerGroups" }, "PreCompact": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PreModelSwitch": { "$ref": "#/$defs/nonModelHandlerGroups" }, "PreToolUse": { "$ref": "#/$defs/allHandlerGroups" }, "SessionEnd": { "$ref": "#/$defs/nonModelHandlerGroups" }, "SessionStart": { "$ref": "#/$defs/startupHandlerGroups" }, diff --git a/packages/agent-bundle/src/adapters/schemas/claude/lsp.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/lsp.schema.json index fc9ddd4f0..4dd6867b9 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/lsp.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/lsp.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/lsp.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/lsp.schema.json", "$defs": { "server": { "additionalProperties": false, diff --git a/packages/agent-bundle/src/adapters/schemas/claude/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/marketplace.schema.json index fa6e51a2d..b6bc20902 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/marketplace.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/marketplace.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/marketplace.schema.json", "$defs": { "channel": { "additionalProperties": false, diff --git a/packages/agent-bundle/src/adapters/schemas/claude/mcp.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/mcp.schema.json index d706a4f08..b23080236 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/mcp.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/mcp.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/mcp.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/mcp.schema.json", "additionalProperties": false, "properties": { "mcpServers": { diff --git a/packages/agent-bundle/src/adapters/schemas/claude/monitors.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/monitors.schema.json index 2ca7d04a1..738286f08 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/monitors.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/monitors.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/monitors.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/monitors.schema.json", "items": { "additionalProperties": false, "properties": { diff --git a/packages/agent-bundle/src/adapters/schemas/claude/plugin.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/plugin.schema.json index 93541aa47..f859e9aab 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/plugin.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/plugin.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/plugin.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/plugin.schema.json", "$defs": { "additionalHooksPath": { "description": "A plugin-root-relative path to an additional hook configuration file: `./` followed by non-empty segments, none of which is `.` or `..`, so every accepted value is already normalized. Claude Code loads hooks/hooks.json on its own and refuses a plugin whose manifest names that file again (Duplicate hooks file detected), so the auto-loaded path is rejected here; aliases such as `./hooks/./hooks.json` or `./hooks/../hooks/hooks.json` fail the segment rule instead.", diff --git a/packages/agent-bundle/src/adapters/schemas/claude/settings.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/settings.schema.json index 342bc13c6..0d9aec655 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/settings.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/settings.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/settings.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/settings.schema.json", "additionalProperties": false, "minProperties": 1, "properties": { diff --git a/packages/agent-bundle/src/adapters/schemas/claude/theme.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/theme.schema.json index 8cf13ff95..577961fe1 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/theme.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/theme.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/theme.schema.json", + "$id": "https://agent-bundle.dev/schemas/claude/2.1.260/theme.schema.json", "additionalProperties": false, "properties": { "base": { "enum": ["dark", "light"] }, diff --git a/packages/agent-bundle/src/config/normalize.ts b/packages/agent-bundle/src/config/normalize.ts index 117e0fb4a..7e56ca929 100644 --- a/packages/agent-bundle/src/config/normalize.ts +++ b/packages/agent-bundle/src/config/normalize.ts @@ -127,6 +127,8 @@ const hookEventForRoute: Readonly { @@ -704,6 +734,53 @@ export const projectEventDocument = ( } return undefined; } + if (event === 'model-switch/before') { + // https://code.claude.com/docs/en/hooks#premodelswitch-decision-control + // (uploaded 2026-09-03): permissionDecision allow | deny | ask with + // permissionDecisionReason (shown for deny and ask, ignored for allow); + // no defer, updatedInput, or additionalContext. Precedence across hooks + // is deny > ask > allow, and `ask` is a refusal outside interactive /model. + if (parsedValue?.updatedInput !== undefined) { + throw new TypeError('model-switch/before cannot replace native input; PreModelSwitch accepts no updatedInput.'); + } + if (additionalContext !== undefined) { + throw new TypeError('model-switch/before has no additional-context channel; PreModelSwitch accepts no additionalContext.'); + } + if (parsedValue?.reason !== undefined && parsedValue.outcome !== 'deny' && parsedValue.outcome !== 'ask') { + throw new TypeError('model-switch/before reason is only valid when outcome is deny or ask.'); + } + if (parsedValue?.outcome === undefined || parsedValue.outcome === 'continue') return undefined; + return deepFreeze({ + hookSpecificOutput: { + hookEventName: nativeEvent, + permissionDecision: parsedValue.outcome, + ...(parsedValue.outcome === 'deny' + ? { permissionDecisionReason: requireDenyReason() } + : parsedValue.outcome === 'ask' && parsedValue.reason !== undefined + ? { permissionDecisionReason: parsedValue.reason } + : {}), + }, + }); + } + if (event === 'model-switch/after') { + // https://code.claude.com/docs/en/hooks#postmodelswitch-decision-control: + // the model has already changed; only additionalContext reaches Claude, + // with the next request after the switch. + if ( + parsedValue?.outcome === 'deny' + || parsedValue?.reason !== undefined + || parsedValue?.updatedInput !== undefined + ) { + throw new TypeError('model-switch/after observes a completed model switch and cannot deny or replace native input.'); + } + if (additionalContext === undefined) return undefined; + return deepFreeze({ + hookSpecificOutput: { + additionalContext, + hookEventName: nativeEvent, + }, + }); + } if (event === 'file/change') { if ( parsedValue?.outcome === 'deny' diff --git a/packages/agent-bundle/src/host-contracts/claude-plugin-validation.ts b/packages/agent-bundle/src/host-contracts/claude-plugin-validation.ts index 400eb388c..b9a3e4091 100644 --- a/packages/agent-bundle/src/host-contracts/claude-plugin-validation.ts +++ b/packages/agent-bundle/src/host-contracts/claude-plugin-validation.ts @@ -60,7 +60,7 @@ export interface ValidateClaudePluginOptions { * Also run `claude --plugin-dir plugin list --json` after the * validation runs and read the plugin's row (default `true`). `plugin * validate --strict` accepts manifests Claude Code then refuses to load - * (observed 2.1.250–2.1.259); the listing's `errors[]` is the only load + * (observed 2.1.250–2.1.260); the listing's `errors[]` is the only load * verdict. Doctor passes `false` because it runs its own registration proof. */ readonly loadCheck?: boolean; @@ -264,7 +264,11 @@ interface ClaudeFinding { type ClaudeValidationRun = 'marketplace' | 'plugin'; -/** `claude plugin validate --json` landed in Claude Code 2.1.259 (plugins-reference §plugin validate). */ +/** + * `claude plugin validate --json` landed in Claude Code 2.1.259 (plugins-reference + * §plugin validate); the pinned 2.1.260 prints it and 2.1.250 answers + * `error: unknown option '--json'`. + */ const jsonReportMinimumVersion: readonly [number, number, number] = [2, 1, 259]; const parseVersion = (version: string): readonly [number, number, number] | undefined => { @@ -272,16 +276,26 @@ const parseVersion = (version: string): readonly [number, number, number] | unde return match === null ? undefined : [Number(match[1]), Number(match[2]), Number(match[3])]; }; +/** + * Whether to request the JSON report first. `--json` is the primary path: it + * is skipped only for a probed version known to predate 2.1.259. An unknown + * or unparseable version asks for JSON and falls back to the text reporter + * when the CLI rejects the flag (`unknownJsonOption`). + */ export const claudeSupportsJsonValidationReport = (version: string | undefined): boolean => { - if (version === undefined) return false; + if (version === undefined) return true; const parsed = parseVersion(version); - if (parsed === undefined) return false; + if (parsed === undefined) return true; for (let index = 0; index < 3; index += 1) { if (parsed[index] !== jsonReportMinimumVersion[index]) return parsed[index] > jsonReportMinimumVersion[index]; } return true; }; +/** A CLI without `--json` (Claude Code before 2.1.259) rejects the flag on stderr with exit 1. */ +const unknownJsonOption = (result: ClaudePluginCommandResult): boolean => + result.exitCode !== 0 && /unknown option '--json'/u.test(result.stderr); + const relativeToPlugin = (file: string, pluginDirectory: string): string => { const absolute = resolve(pluginDirectory, file); const rel = relative(pluginDirectory, absolute); @@ -533,25 +547,32 @@ export const validateClaudePlugin = async ( if ('report' in probed) return probed.report; const version = probed.version; - const jsonReport = claudeSupportsJsonValidationReport(version); + // `--json` first (2.1.259+); a CLI that rejects the flag drops every run + // of this validation to the text reporter, so the two runs stay consistent. + let jsonReport = claudeSupportsJsonValidationReport(version); const findingsByRun: Partial> = {}; for (const validationTarget of await validationTargets(pluginDirectory)) { const label = validationTarget.run === 'marketplace' ? 'Claude marketplace manifest validation' : 'Claude host artifact validation'; + const validate = (json: boolean): Promise => run(Object.freeze({ + args: Object.freeze([ + 'plugin', + 'validate', + validationTarget.path, + '--strict', + ...(json ? ['--json'] : []), + ]), + cwd, + executable, + })); let result: ClaudePluginCommandResult; try { - result = await run(Object.freeze({ - args: Object.freeze([ - 'plugin', - 'validate', - validationTarget.path, - '--strict', - ...(jsonReport ? ['--json'] : []), - ]), - cwd, - executable, - })); + result = await validate(jsonReport); + if (jsonReport && result.termination === undefined && unknownJsonOption(result)) { + jsonReport = false; + result = await validate(false); + } } catch { return failedReport(`${label} could not be started.`, options.target, version); } diff --git a/packages/agent-bundle/src/routes/public.ts b/packages/agent-bundle/src/routes/public.ts index 85094b6be..5dfa6d409 100644 --- a/packages/agent-bundle/src/routes/public.ts +++ b/packages/agent-bundle/src/routes/public.ts @@ -29,6 +29,8 @@ export const canonicalAgentEvents = Object.freeze([ 'task/create', 'task/complete', 'agent/idle', + 'model-switch/before', + 'model-switch/after', ] as const); export type CanonicalAgentEvent = (typeof canonicalAgentEvents)[number]; diff --git a/packages/agent-bundle/src/skills/tokens.ts b/packages/agent-bundle/src/skills/tokens.ts index 5f434c112..20bb55e81 100644 --- a/packages/agent-bundle/src/skills/tokens.ts +++ b/packages/agent-bundle/src/skills/tokens.ts @@ -42,8 +42,8 @@ export interface SkillTokenClassification { readonly token: SkillTokenId; } -const claudeSkills = 'https://code.claude.com/docs/en/skills (Claude Code 2.1.250 pin)'; -const claudePlugins = 'https://code.claude.com/docs/en/plugins-reference (Claude Code 2.1.250 pin)'; +const claudeSkills = 'https://code.claude.com/docs/en/skills (Claude Code 2.1.260 pin)'; +const claudePlugins = 'https://code.claude.com/docs/en/plugins-reference (Claude Code 2.1.260 pin)'; const codexSkills = 'https://learn.chatgpt.com/docs/build-skills (Codex 0.147.0 pin)'; const codexPlugins = 'https://developers.openai.com/plugins/build/plugins (Codex 0.147.0 pin)'; const cursorSkills = 'https://prod.cursor.com/docs/skills (Cursor 2026-08-28 pin)'; diff --git a/packages/agent-bundle/tests/adapter-capability-states.test.ts b/packages/agent-bundle/tests/adapter-capability-states.test.ts index 439ca69ee..f221f2bdd 100644 --- a/packages/agent-bundle/tests/adapter-capability-states.test.ts +++ b/packages/agent-bundle/tests/adapter-capability-states.test.ts @@ -14,7 +14,7 @@ import { unavailableCapability, unionCapabilityStates, } from '../src/adapters/capability-state.ts'; -import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.250.json' with { type: 'json' }; +import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.260.json' with { type: 'json' }; import codexCapabilityTable from '../src/adapters/capabilities/codex-0.147.0.json' with { type: 'json' }; import cursorCapabilityTable from '../src/adapters/capabilities/cursor-2026-08-28.json' with { type: 'json' }; import cursorHooksSchema from '../src/adapters/schemas/cursor/hooks.schema.json' with { type: 'json' }; @@ -78,7 +78,7 @@ it('records an honest four-state rules row on every adapter', () => { state: 'supported', }); expect(registry.get('claude').capabilities.rules).toEqual({ - reason: 'The pinned Claude Code plugin contract (2.1.250) defines no rules component; project guidance ships through CLAUDE.md memory, not a rules directory.', + reason: 'The pinned Claude Code plugin contract (2.1.260) defines no rules component; project guidance ships through CLAUDE.md memory, not a rules directory.', state: 'unavailable', }); expect(registry.get('codex').capabilities.rules).toEqual({ @@ -195,7 +195,7 @@ it('reports Claude LSP support and honest unavailable composite coverage', () => expect(registry.get('claude').capabilities.lsp).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -336,7 +336,7 @@ it('reports Claude bin support without inventing coverage on other native hosts' expect(registry.get('claude').capabilities.bin).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -360,7 +360,7 @@ it.each([ expect(registry.get('claude').capabilities[capability]).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -382,7 +382,7 @@ it('reports Claude plugin settings support and honest unavailable composite cove expect(registry.get('claude').capabilities.settings).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -522,7 +522,7 @@ it('reports Claude userConfig support and honest unavailable composite coverage' expect(registry.get('claude').capabilities.userConfig).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -543,7 +543,7 @@ it('reports Claude channels support and honest unavailable composite coverage', expect(registry.get('claude').capabilities.channels).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -567,7 +567,7 @@ it.each([ expect(registry.get('claude').capabilities[capability]).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -589,7 +589,7 @@ it('reports Claude dependency support and honest unavailable composite coverage' expect(registry.get('claude').capabilities.dependencies).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -638,7 +638,7 @@ it('reports Claude plugin install scopes from the scoped installer implementatio state: 'supported', }); expect(capability).toMatchObject({ - evidence: { observedVersion: '2.1.250', target: 'claude' }, + evidence: { observedVersion: '2.1.260', target: 'claude' }, state: 'supported', }); }); @@ -670,7 +670,7 @@ it('records dated unavailable Claude distribution and policy capability rows', ( expect(row.evidence.every((line) => line.includes('retrieved 2026-09-02'))).toBe(true); if (capability === 'pluginInstallScopes') { expect(registry.get('claude').capabilities[capability]).toMatchObject({ - evidence: { observedVersion: '2.1.250', target: 'claude' }, + evidence: { observedVersion: '2.1.260', target: 'claude' }, state: 'supported', }); } else { @@ -782,7 +782,7 @@ it.each([ expect(registry.get('claude').capabilities[capability]).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -831,7 +831,7 @@ it('reports Claude manifestPaths support without inventing shared composite cove expect(registry.get('claude').capabilities.manifestPaths).toMatchObject({ evidence: { - observedVersion: '2.1.250', + observedVersion: '2.1.260', target: 'claude', }, state: 'supported', @@ -1306,7 +1306,7 @@ it('reports the evidence-backed G10 event family matrix without inferred support reason: expect.not.stringContaining('pluginPaths'), state: 'unavailable', }); - expect(workspaceOpen).toMatchObject({ reason: expect.stringContaining('Claude Code 2.1.250') }); + expect(workspaceOpen).toMatchObject({ reason: expect.stringContaining('Claude Code 2.1.260') }); expect(workspaceOpen).toMatchObject({ reason: expect.stringContaining('Codex 0.147.0') }); }); @@ -1332,15 +1332,15 @@ it('reports evidence-backed installation support only for real host targets', () it('pins dated deferral rows for every explicitly deferred native callback from #258', async () => { const { readFile } = await import('node:fs/promises'); const tables = { - claude: JSON.parse(await readFile(new URL('../src/adapters/capabilities/claude-2.1.250.json', import.meta.url), 'utf8')) as Record, + claude: JSON.parse(await readFile(new URL('../src/adapters/capabilities/claude-2.1.260.json', import.meta.url), 'utf8')) as Record, codex: JSON.parse(await readFile(new URL('../src/adapters/capabilities/codex-0.147.0.json', import.meta.url), 'utf8')) as Record, cursor: JSON.parse(await readFile(new URL('../src/adapters/capabilities/cursor-2026-08-28.json', import.meta.url), 'utf8')) as Record, }; const expected = { claude: [ 'ConfigChange-policy_settings', 'CwdChanged', 'DirectoryAdded', 'Elicitation', 'ElicitationResult', - 'InstructionsLoaded', 'MessageDisplay', 'Notification', 'PostModelSwitch', 'PostToolBatch', - 'PreModelSwitch', 'Setup', 'UserPromptExpansion', 'WorktreeCreate', 'WorktreeRemove', + 'InstructionsLoaded', 'MessageDisplay', 'Notification', 'PostToolBatch', + 'Setup', 'UserPromptExpansion', 'WorktreeCreate', 'WorktreeRemove', ], codex: ['Interrupt'], cursor: [ @@ -1362,7 +1362,7 @@ it('advertises conversation lineage per host with dated 2026-09-03 evidence', as const { readFile } = await import('node:fs/promises'); const rows = ['depth', 'mcp-correlation', 'parent', 'root', 'subagent-events']; const files = { - claude: 'claude-2.1.250.json', + claude: 'claude-2.1.260.json', codex: 'codex-0.147.0.json', cursor: 'cursor-2026-08-28.json', portable: 'portable-1.0.0.json', @@ -1397,7 +1397,7 @@ it('advertises notice delivery routes per host with dated unavailability (#99 st const { readFile } = await import('node:fs/promises'); const routes = ['current-response', 'directed-push', 'host-toast', 'mcp-inbox', 'mcp-resource-updated', 'next-event']; const files = { - claude: 'claude-2.1.250.json', + claude: 'claude-2.1.260.json', codex: 'codex-0.147.0.json', cursor: 'cursor-2026-08-28.json', portable: 'portable-1.0.0.json', diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index b4d241be1..266b262db 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -99,48 +99,48 @@ it('records exact immutable metadata for every built-in target', () => { ], }); expect(registryMetadata(registry, 'claude')).toEqual({ - adapterRevision: '1.27.0', - observedVersion: '2.1.250', + adapterRevision: '1.28.0', + observedVersion: '2.1.260', schemas: [ { name: 'hooks', - revision: '2.1.250', - sha256: '7535806f86481b89b628a0ccf8e34261e6f93163f562c2146ca8dbfe4d95766e', + revision: '2.1.260', + sha256: '0cdfc5eb5201f2c3091768559ac82f4c563ccb1b7bce7a39d5f99e5f404654cb', }, { name: 'lsp', - revision: '2.1.250', - sha256: 'c81fd2f57c410f70f8e5c3f84483f5ec1b575ee02802b424977826f757dccd8e', + revision: '2.1.260', + sha256: 'b4419c5d857267c7e2b21e3e1eb98b4fdc302c87109007190d3258e6ba7096e4', }, { name: 'marketplace', - revision: '2.1.250', - sha256: '4ffa94e8024966e8080b9d3b338c9612bdfa255802a8491b43f74f90427bc988', + revision: '2.1.260', + sha256: '31ee4cc43ba5ce2be248030a69da2d31171550b2cc51c1fcb9f788a8ab92783d', }, { name: 'mcp', - revision: '2.1.250', - sha256: '76ccf02c7bfe2d57945ba18e84da8d655529bd68b4d692f72bce28238c99067e', + revision: '2.1.260', + sha256: 'edd4770e41d6aee5beae1ff918d33139613a243e454ecbd52b72fa824be4a662', }, { name: 'monitors', - revision: '2.1.250', - sha256: 'd45abdf7561e4316ba217ce9c2ac84f32e1049622b74abb081693b479a54b48d', + revision: '2.1.260', + sha256: '6378d94b51fb7c784eaee178237008a03da0f889d0520d57486784954a1d484c', }, { name: 'plugin', - revision: '2.1.250', - sha256: 'e8524e1b1bf9d72f6866c31b37b7312147fcf29d0504f7e5dce5719bc908830b', + revision: '2.1.260', + sha256: '2a976091b81ad07ae8eca57f6f9c5749efeba17fa089a0c12b6ca84d6e70f118', }, { name: 'settings', - revision: '2.1.250', - sha256: '9e86d8c5e4053e8de0e468d349e2c3dde5834d22d6769372b88570e301700073', + revision: '2.1.260', + sha256: '2bbca553621dbf9433a9b7d1ff7952543a368434f2d999a4e4e360b95b6d4c3d', }, { name: 'theme', - revision: '2.1.250', - sha256: '721aa9b0bc7c60cd9359343e5c7205ffbdfea82da312f601720c9dfaa2d8cf0d', + revision: '2.1.260', + sha256: '9931264e6f5a1d4b3b854ce7a17d602c9ba1b57cc1991f1d3991b316eaa81ac2', }, ], }); @@ -170,7 +170,7 @@ it('records exact immutable metadata for every built-in target', () => { }, ], }); - expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.28.0'); + expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.29.0'); }); it('records observed capability versions and rehashes schema snapshots against pinned provenance', async () => { @@ -178,7 +178,7 @@ it('records observed capability versions and rehashes schema snapshots against p const targets = [ { capabilityFile: 'portable-1.0.0.json', provenanceFile: 'portable/PROVENANCE.json', target: 'portable', versionKey: 'version' }, { capabilityFile: 'codex-0.147.0.json', provenanceFile: 'codex/PROVENANCE.json', target: 'codex', versionKey: 'observedCliVersion' }, - { capabilityFile: 'claude-2.1.250.json', provenanceFile: 'claude/PROVENANCE.json', target: 'claude', versionKey: 'observedCliVersion' }, + { capabilityFile: 'claude-2.1.260.json', provenanceFile: 'claude/PROVENANCE.json', target: 'claude', versionKey: 'observedCliVersion' }, { capabilityFile: 'cursor-2026-08-28.json', provenanceFile: 'cursor/PROVENANCE.json', target: 'cursor', versionKey: 'observedCliVersion' }, ] as const; diff --git a/packages/agent-bundle/tests/api.test.ts b/packages/agent-bundle/tests/api.test.ts index a6ed55584..14746a769 100644 --- a/packages/agent-bundle/tests/api.test.ts +++ b/packages/agent-bundle/tests/api.test.ts @@ -1061,7 +1061,7 @@ it('accounts lsp servers and event routes as distinct canonical kinds with a per } } expect(planFor('claude').kinds.find((report) => report.kind === 'lsp')).toEqual({ - capability: { evidence: { observedVersion: '2.1.250', target: 'claude' }, name: 'lsp', state: 'supported' }, + capability: { evidence: { observedVersion: '2.1.260', target: 'claude' }, name: 'lsp', state: 'supported' }, kind: 'lsp', selected: 1, skipped: 0, diff --git a/packages/agent-bundle/tests/claude-hook-event-name.test.ts b/packages/agent-bundle/tests/claude-hook-event-name.test.ts index a90f4ecad..f6272bef3 100644 --- a/packages/agent-bundle/tests/claude-hook-event-name.test.ts +++ b/packages/agent-bundle/tests/claude-hook-event-name.test.ts @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'; import { expect, it } from '@rstest/core'; -import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.250.json' with { type: 'json' }; +import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.260.json' with { type: 'json' }; import { claudeAdapter } from '../src/adapters/claude.ts'; import { pluginAdapter } from '../src/adapters/plugin.ts'; import type { NormalizedHook, NormalizedHookEvent, NormalizedPlugin } from '../src/core/types.ts'; @@ -31,6 +31,8 @@ const claudeEventRoutes: readonly { { hookEvent: 'compactBefore', native: 'claude-pre-compact.json', route: 'compact/before' }, { hookEvent: 'configChange', native: 'claude-config-change.json', route: 'config/change' }, { hookEvent: 'fileChange', native: 'claude-file-changed.json', route: 'file/change' }, + { hookEvent: 'modelSwitchAfter', native: 'claude-post-model-switch.json', route: 'model-switch/after' }, + { hookEvent: 'modelSwitchBefore', native: 'claude-pre-model-switch.json', route: 'model-switch/before' }, { hookEvent: 'permissionDenied', native: 'claude-permission-denied.json', route: 'permission/denied' }, { hookEvent: 'permissionRequest', native: 'claude-permission-request.json', route: 'permission/request' }, { hookEvent: 'promptSubmit', native: 'claude-user-prompt-submit.json', route: 'prompt/submit' }, diff --git a/packages/agent-bundle/tests/claude-hooks-schema.test.ts b/packages/agent-bundle/tests/claude-hooks-schema.test.ts index 723212ada..2384a0ed1 100644 --- a/packages/agent-bundle/tests/claude-hooks-schema.test.ts +++ b/packages/agent-bundle/tests/claude-hooks-schema.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { expect, it } from '@rstest/core'; -import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.250.json' with { type: 'json' }; +import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.260.json' with { type: 'json' }; import { readStandardNativeHookCommands } from '../src/adapters/hook-contract.ts'; import { createDefaultRegistry } from '../src/adapters/registry.ts'; import hooksSchema from '../src/adapters/schemas/claude/hooks.schema.json' with { type: 'json' }; @@ -16,12 +16,15 @@ import type { LoadedConfig } from '../src/config/load.ts'; const fixtureRoot = new URL('./fixtures/claude-hooks-schema/', import.meta.url); /** * The Claude CLI the schema is pinned to (PROVENANCE.json observedCliVersion, the - * exact version CI installs); its `plugin validate --strict` verdicts must be - * recorded beside the cases. Reports from later releases may sit beside them. + * exact version CI installs); its `plugin validate --strict --json` verdicts must + * be recorded beside the cases. */ const pinnedCliVersion = schemaProvenance.observedCliVersion; -/** The newest release recorded; the reference the schema is drawn from describes it. */ -const latestRecordedCliVersion = '2.1.260'; +/** + * The previous pin, kept as the evidence for the v2.1.251 gate: it has no + * `--json`, so its verdicts come from the text reporter. + */ +const previousPinCliVersion = '2.1.250'; const validate = createAdapterValidator().compile(hooksSchema); interface RecordedReport { @@ -63,26 +66,27 @@ const deliberateTightenings: Readonly> = { 'server-empty': 'an empty server name can never resolve to a connected MCP server.', 'shell-on-mcp': 'closed per-type handler shape: `shell` is a command-hook field.', 'timeout-float': 'the hook contract lowers timeoutMs to whole seconds; `timeout` stays an integer as it was before this pin.', - 'unknown-group-field': 'closed matcher-group shape: Claude Code 2.1.260 --strict ignores unknown hooks.json keys without a warning, so the pin is the only guard against a misspelled field.', - 'unknown-handler-field': 'closed handler shape: Claude Code 2.1.260 --strict ignores unknown hooks.json keys without a warning.', - 'unknown-top-level-field': 'closed document shape: Claude Code 2.1.260 --strict ignores unknown hooks.json keys without a warning.', + 'unknown-group-field': 'closed matcher-group shape: Claude Code 2.1.250 and 2.1.260 --strict ignore unknown hooks.json keys without a warning, so the pin is the only guard against a misspelled field.', + 'unknown-handler-field': 'closed handler shape: Claude Code 2.1.250 and 2.1.260 --strict ignore unknown hooks.json keys without a warning.', + 'unknown-top-level-field': 'closed document shape: Claude Code 2.1.250 and 2.1.260 --strict ignore unknown hooks.json keys without a warning.', 'url-on-command': 'closed per-type handler shape: `url` is an http-hook field.', }; /** - * Cases the reference documents but the pinned host predates: the schema and the - * pinned CLI reject them, a later CLI accepts them, and admitting them waits for - * the re-pin that moves observedCliVersion past the gate. + * Cases the reference gates to a release after the previous pin: the schema and + * the pinned 2.1.260 CLI accept them, the previous 2.1.250 pin rejected the keys + * as unknown. Recorded so the gate stays visible for anyone still on 2.1.250. */ -const newerThanPin: Readonly> = { - 'model-switch-events': 'hooks reference "PreModelSwitch" / "PostModelSwitch": both require Claude Code v2.1.251 or later; 2.1.250 rejects the keys.', +const newerThanPreviousPin: Readonly> = { + 'all-pinned-events': 'every documented event, including PreModelSwitch and PostModelSwitch (v2.1.251 or later); 2.1.250 rejects the two keys.', + 'model-switch-events': 'hooks reference "PreModelSwitch" / "PostModelSwitch": both require Claude Code v2.1.251 or later; 2.1.250 rejects the keys, the pinned 2.1.260 accepts them.', }; -/** The reference's "Hook events" minus PreModelSwitch and PostModelSwitch, which require v2.1.251 (newerThanPin). */ +/** The reference's "Hook events": all 33 events the pinned 2.1.260 host knows. */ const documentedEvents = [ 'ConfigChange', 'CwdChanged', 'DirectoryAdded', 'Elicitation', 'ElicitationResult', 'FileChanged', 'InstructionsLoaded', - 'MessageDisplay', 'Notification', 'PermissionDenied', 'PermissionRequest', 'PostCompact', 'PostToolBatch', - 'PostToolUse', 'PostToolUseFailure', 'PreCompact', 'PreToolUse', 'SessionEnd', 'SessionStart', 'Setup', 'Stop', + 'MessageDisplay', 'Notification', 'PermissionDenied', 'PermissionRequest', 'PostCompact', 'PostModelSwitch', 'PostToolBatch', + 'PostToolUse', 'PostToolUseFailure', 'PreCompact', 'PreModelSwitch', 'PreToolUse', 'SessionEnd', 'SessionStart', 'Setup', 'Stop', 'StopFailure', 'SubagentStart', 'SubagentStop', 'TaskCompleted', 'TaskCreated', 'TeammateIdle', 'UserPromptExpansion', 'UserPromptSubmit', 'WorktreeCreate', 'WorktreeRemove', ] as const; @@ -95,13 +99,13 @@ const startupEvents = ['SessionStart', 'Setup'] as const; const handlerGroup = (handler: Readonly>): unknown => [{ hooks: [handler] }]; -it('agrees with the recorded verdict of the pinned Claude CLI and every later one on each fixture, except the recorded tightenings', async () => { +it('agrees with the recorded verdict of the pinned Claude CLI and the previous pin on each fixture, except the recorded tightenings', async () => { const names = await caseNames(); expect(names.length).toBeGreaterThan(40); const recordedVersions = (await readdir(new URL('./reports/', fixtureRoot))).sort(); - expect(pinnedCliVersion).toBe('2.1.250'); - expect(recordedVersions).toEqual([pinnedCliVersion, latestRecordedCliVersion]); - for (const tightening of [...Object.keys(deliberateTightenings), ...Object.keys(newerThanPin)]) expect(names).toContain(tightening); + expect(pinnedCliVersion).toBe('2.1.260'); + expect(recordedVersions).toEqual([previousPinCliVersion, pinnedCliVersion]); + for (const tightening of [...Object.keys(deliberateTightenings), ...Object.keys(newerThanPreviousPin)]) expect(names).toContain(tightening); for (const version of recordedVersions) { const pinned = version === pinnedCliVersion; @@ -111,17 +115,17 @@ it('agrees with the recorded verdict of the pinned Claude CLI and every later on const report = await readJson(`./reports/${version}/${name}.json`); const label = `${name} @ ${version}`; expect(report.strict, label).toBe(true); - // 2.1.250 has no `--json`; its verdicts come from the text reporter. - expect(report.reporter, label).toBe(pinned ? 'text' : undefined); + // 2.1.250 has no `--json`; its verdicts come from the text reporter. The pin records `--json`. + expect(report.reporter, label).toBe(pinned ? undefined : 'text'); const accepted = validate(document); if (Object.hasOwn(deliberateTightenings, name)) { expect(report.success, `${label}: a tightening must be a case Claude accepts`).toBe(true); expect(accepted, `${label}: the schema must reject what the tightening describes`).toBe(false); continue; } - if (Object.hasOwn(newerThanPin, name)) { - expect(accepted, `${label}: the schema must not admit what the pinned host rejects`).toBe(false); - expect(report.success, `${label}: newer-than-pin cases are rejected by the pin and accepted after it`).toBe(!pinned); + if (Object.hasOwn(newerThanPreviousPin, name)) { + expect(accepted, `${label}: the schema admits what the pinned host accepts`).toBe(true); + expect(report.success, `${label}: gated cases are rejected by the previous pin and accepted by the pin`).toBe(pinned); continue; } expect(accepted, `${label}: schema=${accepted} claude=${report.success} ${JSON.stringify(validate.errors ?? report.contents)}`) @@ -201,15 +205,16 @@ it('keeps the pinned schema descriptor and the capability evidence in step with const descriptor = claude.metadata.schemas.find((schema) => schema.name === 'hooks'); expect(descriptor?.sha256).toBe(schemaProvenance.schemas['hooks.schema.json'].sha256); expect(schemaProvenance.hooksSchemaNotes).toContain('hooks.schema.json was re-pinned (uploaded 2026-09-03'); - expect(schemaProvenance.hooksSchemaNotes).toContain('Claude Code 2.1.250'); - expect(schemaProvenance.hooksSchemaNotes).toContain('2.1.260'); + expect(schemaProvenance.hooksSchemaNotes).toContain('Claude Code 2.1.260'); + expect(schemaProvenance.hooksSchemaNotes).toContain('2.1.250'); expect(schemaProvenance.hooksSchemaNotes).toContain('PreModelSwitch'); + expect(schemaProvenance.hooksSchemaNotes).toContain('re-pin to 2.1.260'); const handlerContract = claudeCapabilityTable.hooks.handlerContract; expect(handlerContract.types).toEqual(['agent', 'command', 'http', 'mcp_tool', 'prompt']); expect(handlerContract.emitted).toEqual({ command: ['command', 'timeout', 'type'] }); expect(handlerContract.evidence.some((line) => line.startsWith('uploaded 2026-09-03:') && line.includes('hooks-2.md'))).toBe(true); - expect(handlerContract.evidence.some((line) => line.includes('Claude Code 2.1.250') && line.includes('2.1.260'))).toBe(true); + expect(handlerContract.evidence.some((line) => line.includes('Claude Code 2.1.260') && line.includes('2.1.250'))).toBe(true); for (const feature of ['timeout', 'toolMatchers'] as const) { expect(claudeCapabilityTable.hooks.features[feature].evidence.some((line) => line.includes('every handler'))).toBe(true); } diff --git a/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts b/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts index 0922ba748..12f087005 100644 --- a/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts +++ b/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts @@ -26,7 +26,7 @@ const hook = (event: NormalizedHook['event'], name: string): NormalizedHook => ( /** * The shape ScriptedAlchemy/cargo-hauler#48 shipped: SessionStart, PreToolUse, * PostToolUse, and Stop hooks beside a stdio MCP server, built for the `claude` - * target and installed through a marketplace. Claude Code 2.1.250–2.1.259 + * target and installed through a marketplace. Claude Code 2.1.250–2.1.260 * refused it while `.claude-plugin/plugin.json` named `./hooks/hooks.json` * under `hooks` (#462/#463, fixed in #470). */ @@ -130,9 +130,9 @@ it('emits the cargo-hauler shape at the default hook location with no manifest h // load without `errors` in `claude --plugin-dir plugin list --json`. The second check is the // one that matters for #462/#463: `plugin validate` accepted the duplicate-hooks manifest that Claude // Code then refused at load time, so validation alone is not proof the plugin reaches a session. -// `--json` on `plugin validate` exists from Claude Code 2.1.259; the CI host-install job runs the pinned -// 2.1.250, which rejects the flag ("unknown option '--json'"), so older binaries take the textual form -// and are held to its exit code and "Validation passed" line instead. +// `--json` on `plugin validate` exists from Claude Code 2.1.259 and is the primary path on the pinned +// 2.1.260 the CI host-install job runs; an older binary (2.1.250 rejects the flag with "unknown option +// '--json'") takes the textual form and is held to its exit code and "Validation passed" line instead. claudeIt(`passes claude plugin validate --strict and loads without errors for the emitted claude artifact${claudeAvailable ? '' : missingClaude}`, () => { const pluginRoot = join(root, 'plugin'); diff --git a/packages/agent-bundle/tests/claude-plugin-validation.test.ts b/packages/agent-bundle/tests/claude-plugin-validation.test.ts index 312b8b099..d158b0950 100644 --- a/packages/agent-bundle/tests/claude-plugin-validation.test.ts +++ b/packages/agent-bundle/tests/claude-plugin-validation.test.ts @@ -446,15 +446,50 @@ it('fails host validation when the Claude version probe cannot be spawned', asyn }); }); -it('gates the --json report on Claude Code 2.1.259 or later', () => { - expect(claudeSupportsJsonValidationReport(undefined)).toBe(false); +it('asks for the --json report first and skips it only for a version known to predate Claude Code 2.1.259', () => { expect(claudeSupportsJsonValidationReport('2.1.250')).toBe(false); expect(claudeSupportsJsonValidationReport('2.1.258')).toBe(false); expect(claudeSupportsJsonValidationReport('2.1.259')).toBe(true); + expect(claudeSupportsJsonValidationReport('2.1.260')).toBe(true); expect(claudeSupportsJsonValidationReport('2.1.300')).toBe(true); expect(claudeSupportsJsonValidationReport('2.2.0')).toBe(true); expect(claudeSupportsJsonValidationReport('3.0.0')).toBe(true); - expect(claudeSupportsJsonValidationReport('nightly')).toBe(false); + // Unknown versions take the primary path; the CLI's own `unknown option` answer selects the text fallback. + expect(claudeSupportsJsonValidationReport(undefined)).toBe(true); + expect(claudeSupportsJsonValidationReport('nightly')).toBe(true); +}); + +it('falls back to the text reporter for every run when a CLI of unknown version rejects --json', async () => { + const bundle = await emittedClaudeBundle(); + const text = await recordedReport('2.1.250-plugin-strict-findings.txt', bundle); + const calls: string[][] = []; + const report = await validateClaudePlugin({ + pluginDirectory: bundle, + run: async (request) => { + calls.push([...request.args]); + if (request.args[0] === '--version') return { exitCode: 0, signal: null, stderr: '', stdout: 'nightly (Claude Code)\n' }; + if (request.args[0] === '--plugin-dir') return { exitCode: 0, signal: null, stderr: '', stdout: loadedFixtureRow(request.args[1] ?? '') }; + if (request.args.includes('--json')) return { exitCode: 1, signal: null, stderr: "error: unknown option '--json'\n", stdout: '' }; + const target = request.args[2] ?? ''; + return target.endsWith('marketplace.json') + ? { exitCode: 0, signal: null, stderr: '', stdout: `Validating marketplace manifest: ${target}\n\n✔ Validation passed\n` } + : { exitCode: 1, signal: null, stderr: '', stdout: text }; + }, + target: 'claude', + }); + + const plugin = join(bundle, '.claude-plugin', 'plugin.json'); + const marketplace = join(bundle, '.claude-plugin', 'marketplace.json'); + expect(calls).toEqual([ + ['--version'], + ['plugin', 'validate', plugin, '--strict', '--json'], + ['plugin', 'validate', plugin, '--strict'], + ['plugin', 'validate', marketplace, '--strict'], + ['--plugin-dir', bundle, 'plugin', 'list', '--json'], + ]); + expect(report.status).toBe('failed'); + expect(report.version).toBeUndefined(); + expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(['AB6020', 'AB6020', 'AB6020', 'AB6021', 'AB6021']); }); it('validates the plugin manifest and the marketplace manifest separately with --json on 2.1.259', async () => { @@ -718,7 +753,7 @@ it('reports AB6022 when a 2.1.259 run returns no JSON report (exit 2 writes only }); }); -it('falls back to text parsing with file attribution on the CI-pinned Claude Code 2.1.250', async () => { +it('parses the text reporter with file attribution on Claude Code 2.1.250, which has no --json', async () => { const bundle = await emittedClaudeBundle(); const text = await recordedReport('2.1.250-plugin-strict-findings.txt', bundle); const fixture = runByTarget({ diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json index 1aefbf9b5..addc2cf74 100644 --- a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json @@ -280,6 +280,26 @@ ] } ], + "PreModelSwitch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PreModelSwitch" + } + ] + } + ], + "PostModelSwitch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostModelSwitch" + } + ] + } + ], "SessionEnd": [ { "hooks": [ diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json index bab52146a..7d92e8404 100644 --- a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json @@ -1,5 +1,5 @@ { - "success": true, + "success": false, "strict": true, "target": "/bundle/claude/.claude-plugin/plugin.json", "reporter": "text", @@ -10,5 +10,24 @@ "warnings": [], "notes": [] }, - "contents": [] + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.PreModelSwitch", + "message": "Invalid key in record", + "code": null + }, + { + "path": "hooks.PostModelSwitch", + "message": "Invalid key in record", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] } diff --git a/packages/agent-bundle/tests/fixtures/events/claude-post-model-switch.json b/packages/agent-bundle/tests/fixtures/events/claude-post-model-switch.json new file mode 100644 index 000000000..dfa699f45 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/events/claude-post-model-switch.json @@ -0,0 +1,15 @@ +{ + "cache_ttl": "5m", + "context_tokens": 182340, + "cwd": "/workspace", + "estimated_cache_write_usd": 1.1396, + "from_model": "claude-sonnet-5", + "hook_event_name": "PostModelSwitch", + "pricing": "catalog", + "prompt_cache_warm": true, + "requested_model": null, + "session_id": "session-claude-1", + "source": "auto", + "to_model": "claude-opus-5", + "transcript_path": "/workspace/.claude/projects/session.jsonl" +} diff --git a/packages/agent-bundle/tests/fixtures/events/claude-pre-model-switch.json b/packages/agent-bundle/tests/fixtures/events/claude-pre-model-switch.json new file mode 100644 index 000000000..cd62dae08 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/events/claude-pre-model-switch.json @@ -0,0 +1,15 @@ +{ + "cache_ttl": "5m", + "context_tokens": 182340, + "cwd": "/workspace", + "estimated_cache_write_usd": 1.1396, + "from_model": "claude-sonnet-5", + "hook_event_name": "PreModelSwitch", + "pricing": "catalog", + "prompt_cache_warm": true, + "requested_model": "opus", + "session_id": "session-claude-1", + "source": "command", + "to_model": "claude-opus-5", + "transcript_path": "/workspace/.claude/projects/session.jsonl" +} diff --git a/packages/agent-bundle/tests/host-adapters.native.test.ts b/packages/agent-bundle/tests/host-adapters.native.test.ts index d4c03ffdb..9c9945c78 100644 --- a/packages/agent-bundle/tests/host-adapters.native.test.ts +++ b/packages/agent-bundle/tests/host-adapters.native.test.ts @@ -10,9 +10,11 @@ import { codexAdapter } from '../src/adapters/codex.ts'; import { emitPlanEntries } from '../src/build/emit.ts'; import { pathTokens, type NormalizedPlugin } from '../src/core/types.ts'; import { + claudeSupportsJsonValidationReport, validateClaudePlugin, type ClaudePluginValidationReport, } from '../src/host-contracts/claude-plugin-validation.ts'; +import { parseCliVersion } from '../../../scripts/host-cli-pins.mjs'; const nativeIt = process.env.AGENT_BUNDLE_NATIVE_HOST_CONTRACTS === '1' ? it : it.skip; @@ -533,7 +535,10 @@ nativeIt('pins Claude plugin and marketplace lifecycle command help', async () = ]); expect(version.code, version.output).toBe(0); - expect(version.output).toContain('2.1.257'); + // The proof runs on whatever `claude` is on PATH (CI installs the pinned 2.1.260, the maintainer's + // daily CLI may be newer or older); the help surface below is what the re-pin verified byte-identical + // from 2.1.250 through 2.1.260 (schemas/claude/PROVENANCE.json repinNotes). + expect(version.output).toMatch(/^\d+\.\d+\.\d+ \(Claude Code\)/u); expect(pluginHelp.code, pluginHelp.output).toBe(0); for (const command of [ 'details', @@ -555,6 +560,13 @@ nativeIt('pins Claude plugin and marketplace lifecycle command help', async () = for (const command of ['add', 'list', 'remove|rm', 'update']) { expect(marketplaceHelp.output).toContain(command); } + const validateHelp = await runClaude(root, ['plugin', 'validate', '--help'], root); + expect(validateHelp.code, validateHelp.output).toBe(0); + expect(validateHelp.output).toContain('--strict'); + // `--json` landed in 2.1.259 (plugins-reference "plugin validate"); an older PATH binary lacks it. + if (claudeSupportsJsonValidationReport(parseCliVersion(version.output))) { + expect(validateHelp.output).toContain('--json Output the validation report as JSON (same exit codes)'); + } } finally { await rm(root, { force: true, recursive: true }); } @@ -677,7 +689,7 @@ nativeIt('records that plugin-mode validation warns about a symlinked skill entr // plugin-marketplaces → Validate a plugin or a directory without a manifest › Check files behind // symlinks: "A linked entry inside a skills, agents, or commands directory: Claude Code skips it // and warns, per directory, how many entries it skipped that a session would load." Observed on - // Claude Code 2.1.250 (CI pin), 2.1.251, and 2.1.260 as a `skills` directory warning that + // Claude Code 2.1.250, 2.1.251, and 2.1.260 (CI pin) as a `skills` directory warning that // --strict promotes to exit 1. The validator never names the link target, so containment stays // an install-copy check; this proof is that the plugin run reports the skipped entry at all. const validation = await validateClaudePluginRoot(root); @@ -857,6 +869,8 @@ nativeIt('records which source constraints strict Claude marketplace validation { expectedCode: 1, label: 'short github sha', + // 2.1.260 (CI pin) names the field: `plugins.0.source.sha: Must be a full 40-character lowercase + // git commit SHA`; 2.1.250 reported only `plugins.0.source: Invalid input` (re-pin evidence). output: 'full 40-character lowercase git commit SHA', source: { source: 'github', repo: 'acme/review-tools', sha: 'abc123' }, }, diff --git a/packages/agent-bundle/tests/host-adapters.test.ts b/packages/agent-bundle/tests/host-adapters.test.ts index e212c9a2c..1e8ab93a1 100644 --- a/packages/agent-bundle/tests/host-adapters.test.ts +++ b/packages/agent-bundle/tests/host-adapters.test.ts @@ -326,7 +326,7 @@ it('pins host help, capabilities, and every schema snapshot to the supported CLI // Schema snapshot hashes are pinned by adapter-metadata.test.ts's rehash // test; this test pins the observed CLI versions and the redacted help text. const hosts = { - claude: { version: '2.1.250' }, + claude: { version: '2.1.260' }, codex: { version: '0.147.0' }, } as const; diff --git a/packages/agent-bundle/tests/host-cli-pins.test.ts b/packages/agent-bundle/tests/host-cli-pins.test.ts index df2151816..e12181dc9 100644 --- a/packages/agent-bundle/tests/host-cli-pins.test.ts +++ b/packages/agent-bundle/tests/host-cli-pins.test.ts @@ -17,7 +17,7 @@ import { verifyHostCliPins, type HostCliPins, } from '../../../scripts/host-cli-pins.mjs'; -import claudeCapabilities from '../src/adapters/capabilities/claude-2.1.250.json' with { type: 'json' }; +import claudeCapabilities from '../src/adapters/capabilities/claude-2.1.260.json' with { type: 'json' }; import codexCapabilities from '../src/adapters/capabilities/codex-0.147.0.json' with { type: 'json' }; const pins: HostCliPins = Object.freeze({ diff --git a/packages/agent-bundle/tests/lifecycle-replay-service.test.ts b/packages/agent-bundle/tests/lifecycle-replay-service.test.ts index ab56b1558..e06b99763 100644 --- a/packages/agent-bundle/tests/lifecycle-replay-service.test.ts +++ b/packages/agent-bundle/tests/lifecycle-replay-service.test.ts @@ -201,7 +201,7 @@ it('mounts and reports honest receipt provenance for a Workbench replay', async actor: { reason: 'not-provided', state: 'unavailable' }, host: { source: 'receipt', state: 'available', value: { name: 'claude' } }, invocation: { - hostContractRevision: '2.1.250', + hostContractRevision: '2.1.260', kind: 'event', operationId: 'event:tool/after', surface: 'tool/after', @@ -219,7 +219,7 @@ it('mounts and reports honest receipt provenance for a Workbench replay', async actor: requestContext.actor, host: requestContext.host, invocation: { - hostContractRevision: '2.1.250', + hostContractRevision: '2.1.260', operationId: 'event:tool/after', surface: 'tool/after', }, diff --git a/packages/agent-bundle/tests/route-unit/event-project.test.ts b/packages/agent-bundle/tests/route-unit/event-project.test.ts index 80da31742..fef73f80e 100644 --- a/packages/agent-bundle/tests/route-unit/event-project.test.ts +++ b/packages/agent-bundle/tests/route-unit/event-project.test.ts @@ -1,3 +1,5 @@ +import { readFile } from 'node:fs/promises'; + import { Agent, type JsonValue } from '@agent-bundle/runtime'; import { expect, it } from '@rstest/core'; import { createElement, Suspense } from 'react'; @@ -837,6 +839,100 @@ it('projects the stage-4 Claude-only families with their documented decision cha .toThrow(/no documented additional-context channel/u); }); +it('projects the model-switch families to the PreModelSwitch decision and PostModelSwitch context channels (2.1.260 re-pin)', async () => { + const render = async (event: string, native: Record, nativeEvent: string, value?: JsonValue, context?: string) => { + const props = createCanonicalEventProps( + event as never, + native, + 'claude', + nativeEvent, + '2.1.260', + new AbortController().signal, + ); + return renderRoute({ + default: async () => createElement( + Agent.Result, + value === undefined ? null : { value }, + context === undefined ? undefined : createElement(Agent.Context, null, context), + ), + }, { + input: { canonical: props.canonical, native: props.native }, + kind: 'event-route', + routeId: `event:${event}`, + }); + }; + // hooks reference "PreModelSwitch input": the documented `/model opus` example. + const before = JSON.parse(await readFile(new URL('../fixtures/events/claude-pre-model-switch.json', import.meta.url), 'utf8')) as Record; + const after = JSON.parse(await readFile(new URL('../fixtures/events/claude-post-model-switch.json', import.meta.url), 'utf8')) as Record; + + // model-switch/before: pass-through projects nothing; allow / ask / deny are the documented permissionDecision values. + const observed = await render('model-switch/before', before, 'PreModelSwitch'); + expect(projectEventDocument(observed.document, 'model-switch/before', 'claude', 'PreModelSwitch')).toBeUndefined(); + const passThrough = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'continue' }); + expect(projectEventDocument(passThrough.document, 'model-switch/before', 'claude', 'PreModelSwitch')).toBeUndefined(); + const allowed = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'allow' }); + expect(projectEventDocument(allowed.document, 'model-switch/before', 'claude', 'PreModelSwitch')).toEqual({ + hookSpecificOutput: { hookEventName: 'PreModelSwitch', permissionDecision: 'allow' }, + }); + const asked = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'ask', reason: 'Switching re-sends about 180k tokens. Continue?' }); + expect(projectEventDocument(asked.document, 'model-switch/before', 'claude', 'PreModelSwitch')).toEqual({ + hookSpecificOutput: { + hookEventName: 'PreModelSwitch', + permissionDecision: 'ask', + permissionDecisionReason: 'Switching re-sends about 180k tokens. Continue?', + }, + }); + const denied = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'deny', reason: 'Opus is not approved for this repository.' }); + expect(projectEventDocument(denied.document, 'model-switch/before', 'claude', 'PreModelSwitch')).toEqual({ + hookSpecificOutput: { + hookEventName: 'PreModelSwitch', + permissionDecision: 'deny', + permissionDecisionReason: 'Opus is not approved for this repository.', + }, + }); + const deniedWithoutReason = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'deny' }); + expect(() => projectEventDocument(deniedWithoutReason.document, 'model-switch/before', 'claude', 'PreModelSwitch')) + .toThrow(/requires a nonempty reason/u); + const allowedWithReason = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'allow', reason: 'Fine.' }); + expect(() => projectEventDocument(allowedWithReason.document, 'model-switch/before', 'claude', 'PreModelSwitch')) + .toThrow(/reason is only valid when outcome is deny or ask/u); + const rewritten = await render('model-switch/before', before, 'PreModelSwitch', { outcome: 'allow', updatedInput: { to_model: 'claude-sonnet-5' } }); + expect(() => projectEventDocument(rewritten.document, 'model-switch/before', 'claude', 'PreModelSwitch')) + .toThrow(/accepts no updatedInput/u); + const contextual = await render('model-switch/before', before, 'PreModelSwitch', undefined, 'Context.'); + expect(() => projectEventDocument(contextual.document, 'model-switch/before', 'claude', 'PreModelSwitch')) + .toThrow(/accepts no additionalContext/u); + + // model-switch/after: observation plus additionalContext; the switch already happened. + const afterObserved = await render('model-switch/after', after, 'PostModelSwitch'); + expect(projectEventDocument(afterObserved.document, 'model-switch/after', 'claude', 'PostModelSwitch')).toBeUndefined(); + const afterContext = await render('model-switch/after', after, 'PostModelSwitch', undefined, 'On Opus, delegate implementation to subagents.'); + expect(projectEventDocument(afterContext.document, 'model-switch/after', 'claude', 'PostModelSwitch')).toEqual({ + hookSpecificOutput: { + additionalContext: 'On Opus, delegate implementation to subagents.', + hookEventName: 'PostModelSwitch', + }, + }); + const afterDenied = await render('model-switch/after', after, 'PostModelSwitch', { outcome: 'deny', reason: 'No.' }); + expect(() => projectEventDocument(afterDenied.document, 'model-switch/after', 'claude', 'PostModelSwitch')) + .toThrow(/observes a completed model switch/u); + const afterAllowed = await render('model-switch/after', after, 'PostModelSwitch', { outcome: 'allow' }); + expect(() => projectEventDocument(afterAllowed.document, 'model-switch/after', 'claude', 'PostModelSwitch')) + .toThrow(/does not accept outcome "allow"/u); + + // Envelope validation follows the documented input: PostModelSwitch alone adds the auto and resume sources. + const validation = { canonicalEvent: 'model-switch/before' as const, nativeEvent: 'PreModelSwitch', target: 'claude' }; + expect(() => validateNativeEventEnvelope({ ...before, source: 'auto' }, validation)).toThrow(/native source is invalid/u); + expect(() => validateNativeEventEnvelope({ ...before, requested_model: 7 }, validation)).toThrow(/requested_model must be a string or null/u); + expect(() => validateNativeEventEnvelope({ ...before, cache_ttl: '2h' }, validation)).toThrow(/cache_ttl is invalid/u); + expect(() => validateNativeEventEnvelope({ ...before, pricing: 'free' }, validation)).toThrow(/pricing is invalid/u); + expect(() => validateNativeEventEnvelope({ ...before, to_model: '' }, validation)).toThrow(/to_model must be a nonempty string/u); + expect(validateNativeEventEnvelope( + { ...after, source: 'resume', requested_model: 'claude-opus-5' }, + { canonicalEvent: 'model-switch/after', nativeEvent: 'PostModelSwitch', target: 'claude' }, + )).toMatchObject({ source: 'resume' }); +}); + it('projects tool/before pass-through as no decision and only explicit allow/ask/deny as one (#461)', async () => { // Native envelopes are the captured host-test PreToolUse/preToolUse // records in fixtures/host-lineage (Claude 2.1.257, Codex 0.147.0, diff --git a/website/docs/en/guide/authoring/hooks.mdx b/website/docs/en/guide/authoring/hooks.mdx index dd502525a..bf772db7c 100644 --- a/website/docs/en/guide/authoring/hooks.mdx +++ b/website/docs/en/guide/authoring/hooks.mdx @@ -204,8 +204,12 @@ throw before anything reaches the host: `session/end`, `compact/after`, `tool/fa `hookSpecificOutput.permissionDecision: 'allow' | 'ask' | 'deny'` on Claude and Codex but `{ permission: 'allow' | 'deny', … }` on Cursor, which documents `ask` as unenforced and therefore rejects it; a denied `stop` becomes `{ decision: 'block', reason }` or Cursor's -`followup_message`. `allow` and `ask` are `tool/before` decisions (`allow` also answers -`permission/request` on the user's behalf); every other family rejects them. Which families each +`followup_message`. `allow` and `ask` are `tool/before` and `model-switch/before` decisions +(`allow` also answers `permission/request` on the user's behalf); every other family rejects them. +A decided `model-switch/before` becomes `hookSpecificOutput.permissionDecision: 'allow' | 'ask' | +'deny'` on Claude Code's `PreModelSwitch` (2.1.251 or later; `ask` is a refusal outside an +interactive `/model`), and `model-switch/after` observes the completed switch with +`Agent.Context` reaching Claude as `additionalContext` on the next request. Which families each host supports is the generated [Event and hook matrix](../../reference/events.md). `updatedInput` on `tool/before` rewrites the pending call and is independent of the decision. On @@ -215,9 +219,9 @@ the rewrite or with `ask` to show the rewritten input to the user. Cursor docume `updated_input` only alongside a `permission`, so a rewrite is delivered as `permission: 'allow'` there. -Event routes reach the twenty canonical families (`session/end`, `prompt/submit`, -`compact/before`, `permission/request`, …); config-declared `hooks` cover only the seven -listed above. +Event routes reach the twenty-two canonical families (`session/end`, `prompt/submit`, +`compact/before`, `permission/request`, `model-switch/before`, …); config-declared `hooks` cover +only the seven listed above. A route or handler that **throws** produces no document and therefore no decision: the generated wrapper writes to stderr, nothing to stdout, and exits `1`, which every supported host documents as diff --git a/website/docs/en/guide/authoring/index.mdx b/website/docs/en/guide/authoring/index.mdx index b285e775c..1a7b73a4b 100644 --- a/website/docs/en/guide/authoring/index.mdx +++ b/website/docs/en/guide/authoring/index.mdx @@ -84,8 +84,8 @@ Every host key is optional and every field inside it is optional. `claude` and ` the shared `AgentBundleHostConfig`, whose single field `nativeHooks` names a project-authored, target-native hook document (`hooks.json`) the adapter validates and merges beside the compiled hooks. For Claude Code the pinned schema admits the documented handler contract: every hook event -the pinned 2.1.250 host knows (the reference's list minus `PreModelSwitch` and `PostModelSwitch`, -which require 2.1.251), the five handler types (`command`, `http`, `mcp_tool`, `prompt`, +the pinned 2.1.260 host knows (the reference's full list, including `PreModelSwitch` and +`PostModelSwitch`, which require 2.1.251), the five handler types (`command`, `http`, `mcp_tool`, `prompt`, `agent`), their per-type fields (`args`, `async`, `shell`, `url`, `headers`, `server`, `tool`, `input`, `prompt`, `model`, …), and the common `if`, `once`, `statusMessage`, and `timeout` fields; unknown keys, a field of another handler type, and a prompt/agent/http handler on an event diff --git a/website/docs/en/guide/distribution/validation.mdx b/website/docs/en/guide/distribution/validation.mdx index 1e4b5a88a..ecb5573ea 100644 --- a/website/docs/en/guide/distribution/validation.mdx +++ b/website/docs/en/guide/distribution/validation.mdx @@ -65,9 +65,10 @@ claude plugin validate /.claude-plugin/marketplace.json --strict - Host **errors** become Agent Bundle errors (`AB6021`). - Host **warnings** stay warnings (`AB6020`) unless `agent-bundle validate --strict` is set. - Each finding names the file Claude Code checked, for example - `(hooks hooks/hooks.json): hooks: hooks.postToolUse: unknown hook event`. On Claude Code 2.1.259 - or later Agent Bundle reads the CLI's `--json` report; older releases are parsed from the text - report. The marketplace run's `plugins[0] plugin.json →` copies of manifest findings the plugin + `(hooks hooks/hooks.json): hooks: hooks.postToolUse: unknown hook event`. Agent Bundle asks for + the CLI's `--json` report first (Claude Code 2.1.259 or later; the pinned 2.1.260 prints it) and + parses the text report only when the CLI rejects the flag or the probed version predates + 2.1.259. The marketplace run's `plugins[0] plugin.json →` copies of manifest findings the plugin run already reported are dropped. - A missing binary is reported as an explicit informational **skip** (`AB6019`), never as fabricated success; a probe or run that cannot complete is `AB6022`. A `build` without `claude` diff --git a/website/docs/zh/guide/authoring/hooks.mdx b/website/docs/zh/guide/authoring/hooks.mdx index 910e2b1a1..e7aa8e2ba 100644 --- a/website/docs/zh/guide/authoring/hooks.mdx +++ b/website/docs/zh/guide/authoring/hooks.mdx @@ -187,16 +187,19 @@ export default async function AfterFileEdit({ canonical, native, signal }: Agent `tool/before` 在 Claude 与 Codex 上成为 `hookSpecificOutput.permissionDecision: 'allow' | 'ask' | 'deny'`, 在 Cursor 上则是 `{ permission: 'allow' | 'deny', … }`——Cursor 文档称 `ask` 不会被强制执行,因此会拒绝它; 被拒绝的 `stop` 成为 `{ decision: 'block', reason }` 或 Cursor 的 `followup_message`。`allow` 与 `ask` 是 -`tool/before` 的决定(`allow` 也可以代表用户回答 `permission/request`);其他任何事件族都会拒绝它们。每个 -宿主支持哪些事件族,见生成的[事件与钩子矩阵](../../reference/events.md)。 +`tool/before` 与 `model-switch/before` 的决定(`allow` 也可以代表用户回答 `permission/request`);其他任何 +事件族都会拒绝它们。做出决定的 `model-switch/before` 在 Claude Code 的 `PreModelSwitch`(2.1.251 或更新)上 +成为 `hookSpecificOutput.permissionDecision: 'allow' | 'ask' | 'deny'`(在交互式 `/model` 之外,`ask` +等同于拒绝);`model-switch/after` 只观察已完成的切换,其 `Agent.Context` 会作为 `additionalContext` +随下一次请求送达 Claude。每个宿主支持哪些事件族,见生成的[事件与钩子矩阵](../../reference/events.md)。 `tool/before` 上的 `updatedInput` 改写待执行的调用,并且与决定相互独立。在 Claude 与 Codex 上,随 `continue` 返回的改写不携带 `permissionDecision`,宿主会针对改写后的输入评估其权限规则;与 `allow` 搭配可自动批准该改写, 与 `ask` 搭配则把改写后的输入展示给用户确认。Cursor 只在与 `permission` 同时出现时记载 `updated_input`,因此 改写在那里以 `permission: 'allow'` 交付。 -事件路由可以触达全部二十个规范事件族(`session/end`、`prompt/submit`、`compact/before`、 -`permission/request`……);配置声明的 `hooks` 只覆盖上面列出的七个。 +事件路由可以触达全部二十二个规范事件族(`session/end`、`prompt/submit`、`compact/before`、 +`permission/request`、`model-switch/before`……);配置声明的 `hooks` 只覆盖上面列出的七个。 **抛出**异常的路由或处理器不会产生文档,因此也没有决定:生成的包装器写到 stderr,stdout 无输出, 退出码为 `1`——所有受支持的宿主都把它记作非阻塞错误,待执行的动作会像 pass-through 之后一样继续进行。 diff --git a/website/docs/zh/guide/authoring/index.mdx b/website/docs/zh/guide/authoring/index.mdx index 653c119a7..d3d88a05e 100644 --- a/website/docs/zh/guide/authoring/index.mdx +++ b/website/docs/zh/guide/authoring/index.mdx @@ -79,7 +79,7 @@ export default defineConfig({ 每个宿主键都是可选的,其中的每个字段也都是可选的。`claude` 与 `codex` 扩展自共享的 `AgentBundleHostConfig`,它唯一的字段 `nativeHooks` 指向一份由项目编写、target 原生的钩子文档 (`hooks.json`),适配器会校验它并与编译出的钩子合并。对 Claude Code 而言,固定的 schema 接纳文档化的 -处理器契约:固定的 2.1.250 宿主认识的每个钩子事件(即参考文档列出的事件,去掉需要 2.1.251 的 +处理器契约:固定的 2.1.260 宿主认识的每个钩子事件(即参考文档列出的全部事件,包括需要 2.1.251 的 `PreModelSwitch` 与 `PostModelSwitch`)、五种处理器类型(`command`、`http`、`mcp_tool`、`prompt`、 `agent`)、各类型自己的字段(`args`、`async`、`shell`、`url`、`headers`、`server`、`tool`、`input`、 `prompt`、`model` 等)以及公共字段 `if`、`once`、`statusMessage` 与 `timeout`;未知键、属于另一种处理器 diff --git a/website/docs/zh/guide/distribution/validation.mdx b/website/docs/zh/guide/distribution/validation.mdx index a9605bc39..0c7a588ae 100644 --- a/website/docs/zh/guide/distribution/validation.mdx +++ b/website/docs/zh/guide/distribution/validation.mdx @@ -56,8 +56,9 @@ claude plugin validate /.claude-plugin/marketplace.json --strict - 宿主的 **error** 会变成 Agent Bundle 的 error(`AB6021`)。 - 宿主的 **warning** 保持为 warning(`AB6020`),除非设置了 `agent-bundle validate --strict`。 - 每条发现都会点名 Claude Code 检查的文件,例如 - `(hooks hooks/hooks.json): hooks: hooks.postToolUse: unknown hook event`。在 Claude Code 2.1.259 - 及更高版本上,Agent Bundle 读取 CLI 的 `--json` 报告;更早的版本则解析文本报告。marketplace 那次 + `(hooks hooks/hooks.json): hooks: hooks.postToolUse: unknown hook event`。Agent Bundle 会先请求 CLI 的 + `--json` 报告(Claude Code 2.1.259 及更高版本;固定的 2.1.260 会输出它),只有当 CLI 拒绝该标志或探测到的 + 版本早于 2.1.259 时才解析文本报告。marketplace 那次 运行中对插件运行已报告过的清单发现的 `plugins[0] plugin.json →` 副本会被丢弃。 - 二进制文件缺失会被报告为一次显式的信息性**跳过**(`AB6019`),绝不会伪造成功;探测或运行无法完成 时为 `AB6022`。`PATH` 上没有 `claude` 时,`build` 只会启动一次进程并只报告一次跳过,无论它构建了多少个