From 37c797fc5e6d5e1da0584e0311bfc30f6df6c7c5 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 18:53:35 +0000 Subject: [PATCH] feat(events): publish host family capabilities Bind every G10 family to pinned host evidence and fail source validation when an event route selects an unavailable target. --- .changeset/event-family-capabilities.md | 7 ++ .../adapters/capabilities/claude-2.1.250.json | 18 +++++ .../adapters/capabilities/codex-0.147.0.json | 18 +++++ .../capabilities/cursor-2026-08-28.json | 17 +++- .../adapters/capabilities/portable-1.0.0.json | 9 +++ .../src/adapters/capability-state.ts | 30 ++++++++ packages/agent-bundle/src/adapters/claude.ts | 10 ++- packages/agent-bundle/src/adapters/codex.ts | 4 +- packages/agent-bundle/src/adapters/cursor.ts | 11 ++- packages/agent-bundle/src/adapters/plugin.ts | 16 ++++ .../agent-bundle/src/adapters/portable.ts | 10 ++- packages/agent-bundle/src/config/validate.ts | 77 +++++++++++++++++++ packages/agent-bundle/src/core/types.ts | 2 + .../tests/adapter-capability-states.test.ts | 32 +++++++- .../tests/adapter-metadata.test.ts | 8 +- .../agent-bundle/tests/route-graph.test.ts | 37 +++++++++ 16 files changed, 292 insertions(+), 14 deletions(-) create mode 100644 .changeset/event-family-capabilities.md diff --git a/.changeset/event-family-capabilities.md b/.changeset/event-family-capabilities.md new file mode 100644 index 000000000..3b6a36660 --- /dev/null +++ b/.changeset/event-family-capabilities.md @@ -0,0 +1,7 @@ +--- +"agent-bundle": minor +--- + +Publish evidence-backed event-route support states for Claude, Codex, Cursor, +the unified plugin, and Portable targets, and reject unavailable routes before +packaging unless their static config restricts them to supported targets. diff --git a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json index 23e14955c..740d33654 100644 --- a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json +++ b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json @@ -8,6 +8,24 @@ "sessionStart": "SessionStart", "stop": "Stop" }, + "eventRoutes": { + "agent/start": { + "reason": "The pinned Claude Code 2.1.250 hooks contract has no subagent-start event.", + "state": "unavailable" + }, + "agent/stop": { + "reason": "The pinned Claude Code 2.1.250 hooks contract has no subagent-stop event.", + "state": "unavailable" + }, + "session/start": { "nativeEvent": "SessionStart", "state": "supported" }, + "stop": { "nativeEvent": "Stop", "state": "supported" }, + "tool/after": { "nativeEvent": "PostToolUse", "state": "supported" }, + "tool/before": { "nativeEvent": "PreToolUse", "state": "supported" }, + "workspace/open": { + "reason": "The pinned Claude Code 2.1.250 hooks contract has no workspace-open event.", + "state": "unavailable" + } + }, "matchers": { "file.read": "^Read$", "file.write": "^(?:Write|Edit)$", 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 0b814c952..c8eb29fe8 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 @@ -8,6 +8,24 @@ "sessionStart": "SessionStart", "stop": "Stop" }, + "eventRoutes": { + "agent/start": { + "reason": "The pinned Codex 0.147.0 hooks contract has no subagent-start event.", + "state": "unavailable" + }, + "agent/stop": { + "reason": "The pinned Codex 0.147.0 hooks contract has no subagent-stop event.", + "state": "unavailable" + }, + "session/start": { "nativeEvent": "SessionStart", "state": "supported" }, + "stop": { "nativeEvent": "Stop", "state": "supported" }, + "tool/after": { "nativeEvent": "PostToolUse", "state": "supported" }, + "tool/before": { "nativeEvent": "PreToolUse", "state": "supported" }, + "workspace/open": { + "reason": "The pinned Codex 0.147.0 hooks contract has no workspace-open event.", + "state": "unavailable" + } + }, "matchers": { "file.write": "^(?:apply_patch|Edit|Write)$", "mcp": "^mcp__", 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 43d231eb3..45a78f9d5 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 @@ -3,10 +3,22 @@ "hooks": { "config": "hooks/hooks.json", "events": { + "agentStart": "subagentStart", + "agentStop": "subagentStop", "afterTool": "postToolUse", "beforeTool": "preToolUse", "sessionStart": "sessionStart", - "stop": "stop" + "stop": "stop", + "workspaceOpen": "workspaceOpen" + }, + "eventRoutes": { + "agent/start": { "nativeEvent": "subagentStart", "state": "supported" }, + "agent/stop": { "nativeEvent": "subagentStop", "state": "supported" }, + "session/start": { "nativeEvent": "sessionStart", "state": "supported" }, + "stop": { "nativeEvent": "stop", "state": "supported" }, + "tool/after": { "nativeEvent": "postToolUse", "state": "supported" }, + "tool/before": { "nativeEvent": "preToolUse", "state": "supported" }, + "workspace/open": { "nativeEvent": "workspaceOpen", "state": "supported" } }, "matchers": { "agent": "^Task$", @@ -61,7 +73,8 @@ "Known-loading physical ~/.cursor/plugins/local/tracedecay uses .cursor-plugin/plugin.json, root mcp.json, and hooks/hooks.json.", "Installed cursor-agent-exec loader candidates: .cursor-plugin/plugin.json, .claude-plugin/plugin.json, plugin.json.", "Installed loader substitutes CURSOR_PLUGIN_ROOT in MCP command, args, env, and cwd fields and in hook commands.", - "Local-plugin symlinks are realpath checked and rejected when their targets escape ~/.cursor/plugins/local." + "Local-plugin symlinks are realpath checked and rejected when their targets escape ~/.cursor/plugins/local.", + "The pinned Cursor hooks schema admits subagentStart, subagentStop, and workspaceOpen as first-class hook arrays; the public Cursor hooks reference documents workspaceOpen and subagent lifecycle payloads." ] } } 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 2d2ae7ebc..c29e940f9 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 @@ -1,4 +1,13 @@ { + "eventRoutes": { + "agent/start": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" }, + "agent/stop": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" }, + "session/start": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" }, + "stop": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" }, + "tool/after": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" }, + "tool/before": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" }, + "workspace/open": { "reason": "Agent Plugins 1.0.0 does not define hooks.", "state": "unavailable" } + }, "host": "portable", "mcp": { "pathTokens": { diff --git a/packages/agent-bundle/src/adapters/capability-state.ts b/packages/agent-bundle/src/adapters/capability-state.ts index 0f91def3a..3f46befa7 100644 --- a/packages/agent-bundle/src/adapters/capability-state.ts +++ b/packages/agent-bundle/src/adapters/capability-state.ts @@ -24,6 +24,36 @@ export const unavailableCapability = (reason: string): CapabilityState => Object state: 'unavailable', }); +export interface EventRouteCapabilityTableEntry { + readonly nativeEvent?: string; + readonly reason?: string; + /** JSON imports widen literals; unsupported table states fail closed below. */ + readonly state: string; +} + +/** + * Converts a pinned host table's semantic-event rows into the shared + * capability-state namespace consumed by route validation and inspect. + */ +export const eventRouteCapabilitiesFrom = ( + routes: Readonly>, + evidence: CapabilityEvidence, +): Readonly> => Object.freeze(Object.fromEntries( + Object.entries(routes).sort(([left], [right]) => left.localeCompare(right)).map(([event, capability]) => { + switch (capability.state) { + case 'supported': + return [`event:${event}`, supportedCapability(evidence)]; + case 'unavailable': + return [ + `event:${event}`, + unavailableCapability(capability.reason ?? `The pinned ${evidence.target} contract does not support ${event}.`), + ]; + default: + throw new TypeError(`Unsupported event-route capability state ${JSON.stringify(capability.state)} for ${event}.`); + } + }), +)); + export const capabilityStateFromSupport = ( supported: boolean, evidence: CapabilityEvidence, diff --git a/packages/agent-bundle/src/adapters/claude.ts b/packages/agent-bundle/src/adapters/claude.ts index 67766a14c..88fb14555 100644 --- a/packages/agent-bundle/src/adapters/claude.ts +++ b/packages/agent-bundle/src/adapters/claude.ts @@ -14,7 +14,12 @@ import { standardMcpPathTokens, } from '../services/mcp-path-tokens.ts'; import { createTargetMcpRuntime } from '../services/mcp-runtime.ts'; -import { capabilityEvidence, capabilityStateFromSupport, supportedCapability } from './capability-state.ts'; +import { + capabilityEvidence, + capabilityStateFromSupport, + eventRouteCapabilitiesFrom, + supportedCapability, +} from './capability-state.ts'; import capabilityTable from './capabilities/claude-2.1.250.json' with { type: 'json' }; import { mergeHookDocuments, @@ -125,7 +130,7 @@ const hookContract = Object.freeze({ const metadata = Object.freeze({ adapterRevision: '1.2.0', capabilityRevision: capabilityTable.observedCliVersion, - capabilitySha256: '952788d759db5152e8bcb7128ba778bb74f51fac79403011f669eecdcb1f45f3', + capabilitySha256: '5beb395c075c22a290a70f76b1670fab82b16d933af37cda89da850dbd8d483c', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); @@ -489,6 +494,7 @@ export const claudeAdapter: TargetAdapter = Object.freeze({ artifactValidation, artifactLayout: standardArtifactLayout, capabilities: Object.freeze({ + ...eventRouteCapabilitiesFrom(capabilityTable.hooks.eventRoutes, evidence), marketplace: supportedCapability(evidence), hooks: supportedCapability(evidence), lsp: capabilityStateFromSupport( diff --git a/packages/agent-bundle/src/adapters/codex.ts b/packages/agent-bundle/src/adapters/codex.ts index 80e78be71..0385a19d7 100644 --- a/packages/agent-bundle/src/adapters/codex.ts +++ b/packages/agent-bundle/src/adapters/codex.ts @@ -17,6 +17,7 @@ import { createTargetMcpRuntime, resolveTargetRelativeStdioArgument } from '../s import { capabilityEvidence, capabilityStateFromSupport, + eventRouteCapabilitiesFrom, supportedCapability, unavailableCapability, } from './capability-state.ts'; @@ -116,7 +117,7 @@ const hookContract = Object.freeze({ const metadata = Object.freeze({ adapterRevision: '1.1.0', capabilityRevision: capabilityTable.observedCliVersion, - capabilitySha256: '4b08c8820ace59ca068677dcb1863a9fd4cb730b7e00733b994b0076958beaf0', + capabilitySha256: 'f2c7109e3572ebde8739e08f6fdfa7a7b5d7817e3e216406b9c855f1570e2bd9', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); @@ -405,6 +406,7 @@ export const codexAdapter: TargetAdapter = Object.freeze({ artifactValidation, artifactLayout: standardArtifactLayout, capabilities: Object.freeze({ + ...eventRouteCapabilitiesFrom(capabilityTable.hooks.eventRoutes, evidence), marketplace: supportedCapability(evidence), hooks: supportedCapability(evidence), // The pinned Codex plugin contract documents no LSP surface at all, so diff --git a/packages/agent-bundle/src/adapters/cursor.ts b/packages/agent-bundle/src/adapters/cursor.ts index c22d590a9..92ca646f6 100644 --- a/packages/agent-bundle/src/adapters/cursor.ts +++ b/packages/agent-bundle/src/adapters/cursor.ts @@ -13,7 +13,13 @@ import { standardMcpPathTokens, } from '../services/mcp-path-tokens.ts'; import { createTargetMcpRuntime } from '../services/mcp-runtime.ts'; -import { capabilityEvidence, capabilityStateFromSupport, supportedCapability, unavailableCapability } from './capability-state.ts'; +import { + capabilityEvidence, + capabilityStateFromSupport, + eventRouteCapabilitiesFrom, + supportedCapability, + unavailableCapability, +} from './capability-state.ts'; import capabilityTable from './capabilities/cursor-2026-08-28.json' with { type: 'json' }; import { cursorHookWrapperSource, @@ -246,7 +252,7 @@ export const cursorManifest = ( const metadata = Object.freeze({ adapterRevision: '1.3.0', capabilityRevision: capabilityTable.observedCliVersion, - capabilitySha256: '234920e63508664ae79db4e1a5422c1022d93ad572fae345a179bfd774f6f6d7', + capabilitySha256: '20fc70ad5ba67d984826c3ac917fca66f28e61a8c74edb65dace53c29cc67279', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); @@ -358,6 +364,7 @@ export const cursorAdapter: TargetAdapter = Object.freeze({ artifactValidation, artifactLayout: standardArtifactLayout, capabilities: Object.freeze({ + ...eventRouteCapabilitiesFrom(capabilityTable.hooks.eventRoutes, evidence), hooks: supportedCapability(evidence), marketplace: unavailableCapability('The pinned Cursor Plugin contract does not define a marketplace document.'), mcp: capabilityStateFromSupport( diff --git a/packages/agent-bundle/src/adapters/plugin.ts b/packages/agent-bundle/src/adapters/plugin.ts index 3ef3f0371..9f1f65047 100644 --- a/packages/agent-bundle/src/adapters/plugin.ts +++ b/packages/agent-bundle/src/adapters/plugin.ts @@ -436,10 +436,26 @@ const plan = (model: NormalizedPlugin): TargetArtifactPlan => { }); }; +const compositeEventCapabilities = Object.freeze(Object.fromEntries( + Object.keys(claudeCapabilityTable.hooks.eventRoutes) + .sort((left, right) => left.localeCompare(right)) + .map((event) => { + const capability = `event:${event}`; + return [ + capability, + intersectCapabilityStates( + claudeAdapter.capabilities[capability]!, + codexAdapter.capabilities[capability]!, + ), + ]; + }), +)); + export const pluginAdapter: TargetAdapter = Object.freeze({ artifactValidation, artifactLayout, capabilities: Object.freeze({ + ...compositeEventCapabilities, marketplace: intersectCapabilityStates(claudeAdapter.capabilities.marketplace!, codexAdapter.capabilities.marketplace!), hooks: intersectCapabilityStates(claudeAdapter.capabilities.hooks!, codexAdapter.capabilities.hooks!), // Claude supports LSP and Codex has no LSP surface, so the intersection diff --git a/packages/agent-bundle/src/adapters/portable.ts b/packages/agent-bundle/src/adapters/portable.ts index 6b01f1350..23dbfafc4 100644 --- a/packages/agent-bundle/src/adapters/portable.ts +++ b/packages/agent-bundle/src/adapters/portable.ts @@ -14,7 +14,12 @@ import { standardMcpPathTokens, } from '../services/mcp-path-tokens.ts'; import { createTargetMcpRuntime } from '../services/mcp-runtime.ts'; -import { capabilityEvidence, capabilityStateFromSupport, unavailableCapability } from './capability-state.ts'; +import { + capabilityEvidence, + capabilityStateFromSupport, + eventRouteCapabilitiesFrom, + unavailableCapability, +} from './capability-state.ts'; import capabilityTable from './capabilities/portable-1.0.0.json' with { type: 'json' }; import schemaProvenance from './schemas/portable/PROVENANCE.json' with { type: 'json' }; import mcpSchema from './schemas/portable/mcp.schema.json' with { type: 'json' }; @@ -52,7 +57,7 @@ const validateMcp = schemaValidator.compile(mcpSchema); const metadata = Object.freeze({ adapterRevision: '1.1.0', capabilityRevision: capabilityTable.observedSpecificationVersion, - capabilitySha256: '642da9f921374a4d0143da21ed4b4b2260a2375a5eb33c4cbb4ef531f2bb7352', + capabilitySha256: '99a27bd327f2afdb0a71fa869e9fad27438f07d843c5e61062d2dc3f978dce9a', observedVersion: capabilityTable.observedSpecificationVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.version), }); @@ -322,6 +327,7 @@ export const portableAdapter: TargetAdapter = Object.freeze({ skills: 'skills', }), capabilities: Object.freeze({ + ...eventRouteCapabilitiesFrom(capabilityTable.eventRoutes, evidence), hooks: unavailableCapability('Agent Plugins 1.0.0 does not define a hooks component.'), marketplace: unavailableCapability('Agent Plugins 1.0.0 does not define a marketplace document.'), mcp: capabilityStateFromSupport( diff --git a/packages/agent-bundle/src/config/validate.ts b/packages/agent-bundle/src/config/validate.ts index 9ad6111a2..f73b86588 100644 --- a/packages/agent-bundle/src/config/validate.ts +++ b/packages/agent-bundle/src/config/validate.ts @@ -933,6 +933,82 @@ const validateBin = (loaded: LoadedConfig): Diagnostic[] => { return diagnostics; }; +const validateEventRoutes = ( + loaded: LoadedConfig, + discovered: DiscoveredProject, + registry: NormalizationTargetRegistry, +): Diagnostic[] => { + const selectedTargets = loaded.context.selectedTargets.length > 0 + ? loaded.context.selectedTargets + : (loaded.config.targets ?? registry.defaultTargetNames()); + const diagnostics: Diagnostic[] = []; + + for (const route of discovered.routeGraph?.events ?? []) { + const declaredTargets = route.config['targets']; + if ( + declaredTargets !== undefined && + (!Array.isArray(declaredTargets) || + declaredTargets.length === 0 || + declaredTargets.some((target) => typeof target !== 'string' || target.trim().length === 0)) + ) { + diagnostics.push(sourceDiagnostic( + 'AB4815', + `Event route ${route.provenance.relativePath} config.targets must be a nonempty array of target names.`, + route.source, + )); + continue; + } + const targets = declaredTargets === undefined + ? selectedTargets + : [...new Set(declaredTargets as readonly string[])]; + for (const target of targets) { + if (!registry.has(target)) { + diagnostics.push({ + code: 'AB4814', + message: `Event route ${route.provenance.relativePath} selects unknown target ${JSON.stringify(target)}.`, + severity: 'error', + sourcePath: route.source, + target, + }); + continue; + } + const capabilityName = `event:${route.event}`; + const capability = registry.capabilityState?.(target, capabilityName); + if (capability === undefined) { + if (registry.supports(target, capabilityName)) continue; + diagnostics.push({ + code: 'AB4814', + message: `Event route ${route.provenance.relativePath} requires ${capabilityName}, unavailable on ${target}.`, + severity: 'error', + sourcePath: route.source, + target, + }); + continue; + } + switch (capability.state) { + case 'supported': + case 'degraded': + break; + case 'unavailable': + case 'prohibited': + diagnostics.push({ + code: 'AB4814', + message: `Event route ${route.provenance.relativePath} requires ${capabilityName}, ${capability.state} on ${target}: ${capability.reason}`, + severity: 'error', + sourcePath: route.source, + target, + }); + break; + default: { + const exhaustive: never = capability; + return exhaustive; + } + } + } + } + return diagnostics; +}; + const validateLib = (loaded: LoadedConfig): Diagnostic[] => { const lib = loaded.config.lib; if (lib === undefined || lib === false) return []; @@ -1521,6 +1597,7 @@ export const validateSource = ( // Route-graph collisions (AB4800-AB4804) are compiled during discovery; // they are project-source errors, so they gate inspect and build here. diagnostics.push(...(discovered.routeGraph?.diagnostics ?? [])); + diagnostics.push(...validateEventRoutes(loaded, discovered, registry)); // The stage-1 gate for conventional script routes rides beside the graph's // own collisions: rendered, nested, and config-conflicting script routes // stay hard errors until later #102 stages ship them. diff --git a/packages/agent-bundle/src/core/types.ts b/packages/agent-bundle/src/core/types.ts index ad44a900e..f0f9c9fbf 100644 --- a/packages/agent-bundle/src/core/types.ts +++ b/packages/agent-bundle/src/core/types.ts @@ -1,6 +1,7 @@ import type { EnvironmentConfig } from '@rsbuild/core'; import type { CompiledAgentRoute } from '../routes/types.ts'; +import type { CapabilityState } from './capabilities.ts'; export interface AgentBundlePluginConfig { description?: string; @@ -494,6 +495,7 @@ export type NormalizationNativeHookSource = | NormalizationNativeHookSourceError; export interface NormalizationTargetRegistry { + capabilityState?(name: string, capability: string): CapabilityState | undefined; configExtensions(): readonly NormalizationConfigExtension[]; defaultTargetNames(): readonly string[]; has(name: string): boolean; diff --git a/packages/agent-bundle/tests/adapter-capability-states.test.ts b/packages/agent-bundle/tests/adapter-capability-states.test.ts index cf044177e..7dfd51510 100644 --- a/packages/agent-bundle/tests/adapter-capability-states.test.ts +++ b/packages/agent-bundle/tests/adapter-capability-states.test.ts @@ -181,9 +181,39 @@ it('surfaces built-in adapter metadata as immutable capability evidence', () => if (cursor.capabilities.mcp?.state !== 'supported') throw new Error('Expected Cursor MCP support evidence.'); expect(cursor.capabilities.mcp.evidence).toEqual({ capabilityRevision: '2026-08-28', - capabilitySha256: '234920e63508664ae79db4e1a5422c1022d93ad572fae345a179bfd774f6f6d7', + capabilitySha256: '20fc70ad5ba67d984826c3ac917fca66f28e61a8c74edb65dace53c29cc67279', observedVersion: '2026-08-28', target: 'cursor', }); expect(Object.isFrozen(cursor.capabilities.mcp.evidence)).toBe(true); }); + +it('reports the evidence-backed G10 event family matrix without inferred support', () => { + const registry = createDefaultRegistry(); + const existing = ['event:session/start', 'event:tool/before', 'event:tool/after', 'event:stop']; + const cursorOnly = ['event:agent/start', 'event:agent/stop', 'event:workspace/open']; + + for (const capability of [...existing, ...cursorOnly]) { + expect(registry.get('cursor').capabilities[capability]).toMatchObject({ + evidence: { observedVersion: '2026-08-28', target: 'cursor' }, + state: 'supported', + }); + } + for (const target of ['claude', 'codex'] as const) { + for (const capability of existing) { + expect(registry.get(target).capabilities[capability]).toMatchObject({ + evidence: { target }, + state: 'supported', + }); + } + for (const capability of cursorOnly) { + expect(registry.get(target).capabilities[capability]).toMatchObject({ + reason: expect.stringContaining('pinned'), + state: 'unavailable', + }); + } + } + expect(registry.get('plugin').capabilities['event:workspace/open']).toMatchObject({ + state: 'unavailable', + }); +}); diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index f597a120e..45745a924 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -56,7 +56,7 @@ it('records exact immutable metadata for every built-in target', () => { expect(registryMetadata(registry, 'portable')).toEqual({ adapterRevision: '1.1.0', capabilityRevision: '1.0.0', - capabilitySha256: '642da9f921374a4d0143da21ed4b4b2260a2375a5eb33c4cbb4ef531f2bb7352', + capabilitySha256: '99a27bd327f2afdb0a71fa869e9fad27438f07d843c5e61062d2dc3f978dce9a', observedVersion: '1.0.0', schemas: [ { @@ -74,7 +74,7 @@ it('records exact immutable metadata for every built-in target', () => { expect(registryMetadata(registry, 'codex')).toEqual({ adapterRevision: '1.1.0', capabilityRevision: '0.147.0', - capabilitySha256: '4b08c8820ace59ca068677dcb1863a9fd4cb730b7e00733b994b0076958beaf0', + capabilitySha256: 'f2c7109e3572ebde8739e08f6fdfa7a7b5d7817e3e216406b9c855f1570e2bd9', observedVersion: '0.147.0', schemas: [ { @@ -102,7 +102,7 @@ it('records exact immutable metadata for every built-in target', () => { expect(registryMetadata(registry, 'claude')).toEqual({ adapterRevision: '1.2.0', capabilityRevision: '2.1.250', - capabilitySha256: '952788d759db5152e8bcb7128ba778bb74f51fac79403011f669eecdcb1f45f3', + capabilitySha256: '5beb395c075c22a290a70f76b1670fab82b16d933af37cda89da850dbd8d483c', observedVersion: '2.1.250', schemas: [ { @@ -135,7 +135,7 @@ it('records exact immutable metadata for every built-in target', () => { expect(registryMetadata(registry, 'cursor')).toEqual({ adapterRevision: '1.3.0', capabilityRevision: '2026-08-28', - capabilitySha256: '234920e63508664ae79db4e1a5422c1022d93ad572fae345a179bfd774f6f6d7', + capabilitySha256: '20fc70ad5ba67d984826c3ac917fca66f28e61a8c74edb65dace53c29cc67279', observedVersion: '2026-08-28', schemas: [ { diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index 6e3478af7..bd917ca7f 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -567,3 +567,40 @@ it('discovers only the seven v1 event families and validates their component con expect(graph.diagnostics[0]?.sourcePath).toBe(join(root, 'src/events/prompt/submit.tsx')); expect(graph.diagnostics[1]?.sourcePath).toBe(join(root, 'src/events/tool/before.tsx')); }); + +it('fails unavailable event routes before packaging unless they are target-restricted', async () => { + const eventSource = 'export default async function WorkspaceOpen() { return undefined; }\n'; + const configSource = [ + 'export default {', + " plugin: { name: 'event-capability-fixture', version: '1.0.0' },", + " targets: ['claude', 'cursor'],", + '};', + '', + ].join('\n'); + const unrestrictedRoot = await createRoot(); + await writeTree(unrestrictedRoot, { + 'agent-bundle.config.ts': configSource, + 'package.json': '{"type":"module"}\n', + 'src/events/workspace/open.tsx': eventSource, + }); + + const unrestricted = await inspect({ root: unrestrictedRoot }); + expect(unrestricted.state).toBe('invalid'); + expect(unrestricted.diagnostics).toContainEqual(expect.objectContaining({ + code: 'AB4814', + target: 'claude', + })); + + const restrictedRoot = await createRoot(); + await writeTree(restrictedRoot, { + 'agent-bundle.config.ts': configSource, + 'package.json': '{"type":"module"}\n', + 'src/events/workspace/open.tsx': [ + "export const config = { targets: ['cursor'] };", + eventSource, + ].join('\n'), + }); + + const restricted = await inspect({ root: restrictedRoot }); + expect(restricted.state).toBe('ready'); +});