| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- /**
- * CapabilityMatrix - Feature compatibility matrix across platforms
- *
- * Provides a centralized registry of what features each platform supports,
- * enabling pre-conversion validation and compatibility reporting.
- *
- * @example
- * ```ts
- * const compatibility = analyzeCompatibility(agent, 'cursor');
- * // => { compatible: false, warnings: [...], blockers: [...] }
- * ```
- */
- import type { OpenAgent, ToolCapabilities } from "../types.js";
- /**
- * Re-export only. The ordered-rule collapse lives in `Capabilities.ts`, which owns
- * last-match-wins; this file is a static per-platform support matrix and must not grow a
- * second, divergent resolver. Callers reach `degradeToBinary` here because a per-capability
- * grant is the matrix's own question ("what survives on this platform?"), answered by the
- * resolver rather than duplicated against it.
- *
- * ## Ownership (settled by subtask 07; subtask 04 flagged the overlap)
- *
- * `Capabilities.ts` is the sole IMPLEMENTATION — one resolver, one fail-closed projection.
- * This module is a re-export SURFACE and nothing more: no logic, no wrapper, no defaults.
- * The re-export is load-bearing rather than convenience —
- * `tests/unit/build/permission-ordering.test.ts` imports `degradeToBinary` from this path by
- * name and pins it to subtask 07, so deleting it would break a green gate. New code should
- * prefer importing from `./Capabilities.js` directly; adapters that need the flat-list
- * projection want `projectToFlatTools`, which is only exported there.
- */
- export { degradeToBinary } from "./Capabilities.js";
- export type { BinaryProjection } from "./Capabilities.js";
- // ============================================================================
- // Types
- // ============================================================================
- export type Platform = "oac" | "claude" | "cursor" | "windsurf";
- /**
- * Feature categories for the capability matrix
- */
- export type FeatureCategory =
- | "agents"
- | "permissions"
- | "tools"
- | "context"
- | "model"
- | "advanced";
- /**
- * Support level for a feature
- */
- export type SupportLevel = "full" | "partial" | "none";
- /**
- * Feature definition in the capability matrix
- */
- export interface FeatureDefinition {
- name: string;
- category: FeatureCategory;
- description: string;
- support: Record<Platform, SupportLevel>;
- notes?: Partial<Record<Platform, string>>;
- }
- /**
- * Compatibility analysis result
- */
- export interface CompatibilityResult {
- /** Overall compatibility assessment */
- compatible: boolean;
- /** Score from 0-100 representing compatibility percentage */
- score: number;
- /** Warnings about features that will be degraded */
- warnings: string[];
- /** Blocking issues that prevent conversion */
- blockers: string[];
- /** Features that will be fully preserved */
- preserved: string[];
- /** Features that will be partially preserved */
- degraded: string[];
- /** Features that will be lost */
- lost: string[];
- }
- // ============================================================================
- // Capability Matrix
- // ============================================================================
- /**
- * Complete feature capability matrix
- */
- const CAPABILITY_MATRIX: FeatureDefinition[] = [
- // Agent Features
- {
- name: "multipleAgents",
- category: "agents",
- description: "Support for multiple agent definitions",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
- notes: { cursor: "Single .cursorrules file only - agents will be merged" },
- },
- {
- name: "agentModes",
- category: "agents",
- description: "Primary/subagent mode distinction",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
- notes: { windsurf: "Limited mode support" },
- },
- {
- name: "agentCategories",
- category: "agents",
- description: "Agent categorization (core, development, etc.)",
- // Claude Code agent frontmatter has no category field — the canonical `oac.category`
- // survives only as the directory an author happens to file the source under, and is not
- // carried into the emitted agent at all.
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
- notes: { claude: "No category field in agent frontmatter — dropped on emit" },
- },
- // Permission Features
- {
- name: "granularPermissions",
- category: "permissions",
- description: "Fine-grained allow/deny/ask patterns",
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
- notes: {
- claude:
- "tools/disallowedTools are flat name lists — a capability is wholly granted or " +
- "wholly denied, so anything scoped degrades fail-closed to disallowedTools",
- cursor: "Binary on/off only",
- windsurf: "Binary on/off only",
- },
- },
- {
- name: "orderedPermissionRules",
- category: "permissions",
- description: "Ordered rule lists resolved last-match-wins (deny-all-then-allowlist)",
- // The central fact of the canonical refactor, and previously unrepresented here: the
- // shipped agents' security posture IS the rule order (`bash: {"*": deny, "git log*":
- // allow}`). A target scoring "none" cannot carry that shape at all, which is why
- // Capabilities.degradeToBinary refuses it rather than picking a winning rule.
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
- notes: {
- claude: "No rule ordering concept — the allowlist cannot be carried, so Bash is denied",
- },
- },
- {
- name: "askPermissions",
- category: "permissions",
- description: "Interactive permission requests",
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
- notes: {
- claude: "No 'ask' in agent frontmatter — degrades to deny, never to allow",
- },
- },
- {
- name: "pathPatterns",
- category: "permissions",
- description: "Glob patterns for file permissions",
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
- notes: {
- claude: "Tool grants carry no path scope — secret-file denies cannot be expressed",
- },
- },
- // Tool Features
- {
- name: "binaryToolGrants",
- category: "tools",
- description: "Flat allow/deny lists of tool names (tools / disallowedTools)",
- // What Claude Code DOES support, stated positively — this is the entire target surface
- // ClaudeAdapter emits into, and the matrix previously described only what was missing.
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
- },
- {
- name: "taskDelegation",
- category: "tools",
- description: "Agent-to-agent task delegation",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
- notes: { cursor: "No delegation support" },
- },
- {
- name: "bashExecution",
- category: "tools",
- description: "Shell command execution",
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
- },
- {
- name: "fileOperations",
- category: "tools",
- description: "Read/write/edit file operations",
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
- },
- {
- name: "searchOperations",
- category: "tools",
- description: "Grep/glob search operations",
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
- },
- // Context Features
- {
- name: "externalContext",
- category: "context",
- description: "External context file references",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
- notes: { cursor: "Context must be inline in .cursorrules" },
- },
- {
- name: "contextPriority",
- category: "context",
- description: "Priority levels for context loading",
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
- },
- {
- name: "contextSubdirs",
- category: "context",
- description: "Nested context directory structure",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
- },
- {
- name: "skillsSystem",
- category: "context",
- description: "Loadable skill modules",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
- },
- // Model Features
- {
- name: "modelSelection",
- category: "model",
- description: "Custom model selection",
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
- },
- {
- name: "temperatureControl",
- category: "model",
- description: "Temperature parameter control",
- support: { oac: "full", claude: "none", cursor: "partial", windsurf: "partial" },
- notes: {
- claude: "Temperature not configurable",
- cursor: "Limited range",
- windsurf: "Maps to creativity setting",
- },
- },
- {
- name: "maxSteps",
- category: "model",
- description: "Maximum execution steps limit",
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
- },
- // Advanced Features
- {
- name: "hooks",
- category: "advanced",
- description: "Event hooks (PreToolUse, PostToolUse, etc.)",
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "none" },
- notes: { cursor: "No hook support", windsurf: "No hook support" },
- },
- {
- name: "dependencies",
- category: "advanced",
- description: "Agent dependency declarations",
- // Claude Code agent frontmatter accepts name/description/tools/disallowedTools/model and
- // nothing else. `oac.dependencies` is resolved at build time and then dropped; the
- // emitted agent declares no dependencies, so "full" overstated this.
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
- notes: { claude: "Dependencies resolve at build time; not carried in agent frontmatter" },
- },
- {
- name: "priorityLevels",
- category: "advanced",
- description: "Task priority levels",
- // "2 levels" was not a Claude Code feature — nothing in agent frontmatter or the plugin
- // format expresses task priority.
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
- notes: { oac: "4 levels", claude: "No task priority concept", windsurf: "2 levels" },
- },
- ];
- // ============================================================================
- // Core Functions
- // ============================================================================
- /**
- * Get the full capability matrix.
- *
- * @returns Array of all feature definitions
- */
- export function getCapabilityMatrix(): FeatureDefinition[] {
- return [...CAPABILITY_MATRIX];
- }
- /**
- * Get features by category.
- *
- * @param category - Feature category to filter by
- * @returns Array of features in that category
- */
- export function getFeaturesByCategory(
- category: FeatureCategory
- ): FeatureDefinition[] {
- return CAPABILITY_MATRIX.filter((f) => f.category === category);
- }
- /**
- * Get the support level for a specific feature on a platform.
- *
- * @param featureName - Name of the feature
- * @param platform - Target platform
- * @returns Support level or undefined if feature not found
- */
- export function getFeatureSupport(
- featureName: string,
- platform: Platform
- ): SupportLevel | undefined {
- const feature = CAPABILITY_MATRIX.find((f) => f.name === featureName);
- return feature?.support[platform];
- }
- /**
- * Check if a feature is fully supported on a platform.
- *
- * @param featureName - Name of the feature
- * @param platform - Target platform
- * @returns True if fully supported
- */
- export function isFeatureSupported(
- featureName: string,
- platform: Platform
- ): boolean {
- return getFeatureSupport(featureName, platform) === "full";
- }
- // ============================================================================
- // Compatibility Analysis
- // ============================================================================
- /**
- * Analyze compatibility of an OpenAgent with a target platform.
- *
- * @param agent - The OpenAgent to analyze
- * @param targetPlatform - Target platform for conversion
- * @returns Detailed compatibility analysis
- */
- export function analyzeCompatibility(
- agent: OpenAgent,
- targetPlatform: Exclude<Platform, "oac">
- ): CompatibilityResult {
- const warnings: string[] = [];
- const blockers: string[] = [];
- const preserved: string[] = [];
- const degraded: string[] = [];
- const lost: string[] = [];
- // Check agent mode
- if (agent.frontmatter.mode === "subagent") {
- const modeSupport = getFeatureSupport("agentModes", targetPlatform);
- if (modeSupport === "none") {
- warnings.push(`Agent mode 'subagent' not supported by ${targetPlatform}`);
- degraded.push("agentModes");
- } else if (modeSupport === "partial") {
- warnings.push(`Agent mode may have limited support on ${targetPlatform}`);
- degraded.push("agentModes");
- } else {
- preserved.push("agentModes");
- }
- }
- // Check temperature
- if (agent.frontmatter.temperature !== undefined) {
- const tempSupport = getFeatureSupport("temperatureControl", targetPlatform);
- if (tempSupport === "none") {
- warnings.push(
- `Temperature setting (${agent.frontmatter.temperature}) will be ignored by ${targetPlatform}`
- );
- lost.push("temperatureControl");
- } else if (tempSupport === "partial") {
- warnings.push(`Temperature will be approximated on ${targetPlatform}`);
- degraded.push("temperatureControl");
- } else {
- preserved.push("temperatureControl");
- }
- }
- // Check hooks
- if (agent.frontmatter.hooks && agent.frontmatter.hooks.length > 0) {
- const hookSupport = getFeatureSupport("hooks", targetPlatform);
- if (hookSupport === "none") {
- blockers.push(
- `Hooks are not supported by ${targetPlatform} - ${agent.frontmatter.hooks.length} hook(s) will be lost`
- );
- lost.push("hooks");
- } else {
- preserved.push("hooks");
- }
- }
- // Check skills
- if (agent.frontmatter.skills && agent.frontmatter.skills.length > 0) {
- const skillSupport = getFeatureSupport("skillsSystem", targetPlatform);
- if (skillSupport === "none") {
- warnings.push(
- `Skills system not supported by ${targetPlatform} - skills will be converted to inline context`
- );
- degraded.push("skillsSystem");
- } else if (skillSupport === "partial") {
- warnings.push(`Skills may have limited functionality on ${targetPlatform}`);
- degraded.push("skillsSystem");
- } else {
- preserved.push("skillsSystem");
- }
- }
- // Check granular permissions
- if (agent.frontmatter.permission) {
- const hasGranular = Object.values(agent.frontmatter.permission).some(
- (rule) => typeof rule === "object" && rule !== null
- );
- if (hasGranular) {
- const permSupport = getFeatureSupport("granularPermissions", targetPlatform);
- if (permSupport === "none") {
- warnings.push(
- `Granular permissions will be simplified to binary allow/deny for ${targetPlatform}`
- );
- degraded.push("granularPermissions");
- }
- }
- }
- // Check contexts
- if (agent.contexts && agent.contexts.length > 0) {
- const contextSupport = getFeatureSupport("externalContext", targetPlatform);
- if (contextSupport === "none") {
- warnings.push(
- `External context files not supported by ${targetPlatform} - content must be inline`
- );
- degraded.push("externalContext");
- } else {
- preserved.push("externalContext");
- }
- // Check priority
- const hasPriority = agent.contexts.some((c) => c.priority);
- if (hasPriority) {
- const prioritySupport = getFeatureSupport("contextPriority", targetPlatform);
- if (prioritySupport === "none") {
- warnings.push(`Context priority metadata will be ignored by ${targetPlatform}`);
- lost.push("contextPriority");
- }
- }
- }
- // Check maxSteps
- if (agent.frontmatter.maxSteps !== undefined) {
- const stepsSupport = getFeatureSupport("maxSteps", targetPlatform);
- if (stepsSupport === "none") {
- warnings.push(`maxSteps setting will be ignored by ${targetPlatform}`);
- lost.push("maxSteps");
- }
- }
- // Calculate compatibility score
- const totalFeatures = preserved.length + degraded.length + lost.length;
- const score =
- totalFeatures > 0
- ? Math.round(
- ((preserved.length + degraded.length * 0.5) / totalFeatures) * 100
- )
- : 100;
- return {
- compatible: blockers.length === 0,
- score,
- warnings,
- blockers,
- preserved,
- degraded,
- lost,
- };
- }
- // ============================================================================
- // ToolCapabilities Generation
- // ============================================================================
- /**
- * Generate a ToolCapabilities object for a platform.
- *
- * @param platform - Target platform
- * @returns ToolCapabilities object
- */
- export function getToolCapabilities(
- platform: Exclude<Platform, "oac">
- ): ToolCapabilities {
- const displayNames: Record<Exclude<Platform, "oac">, string> = {
- claude: "Claude Code",
- cursor: "Cursor IDE",
- windsurf: "Windsurf",
- };
- const configFormats: Record<Exclude<Platform, "oac">, ToolCapabilities["configFormat"]> = {
- // Claude Code agents are markdown files with YAML frontmatter
- // (`plugins/claude-code/agents/<id>.md`). This row previously read "json" on the
- // reasoning that `settings.json` is JSON — but settings.json is not what any adapter
- // emits, and ClaudeAdapter.getCapabilities() simultaneously reported "markdown". One
- // platform cannot have two answers about itself; the emitted artifact decides.
- claude: "markdown",
- cursor: "plain",
- windsurf: "json",
- };
- const outputStructures: Record<
- Exclude<Platform, "oac">,
- ToolCapabilities["outputStructure"]
- > = {
- claude: "directory",
- cursor: "single-file",
- windsurf: "directory",
- };
- return {
- name: platform,
- displayName: displayNames[platform],
- supportsMultipleAgents: isFeatureSupported("multipleAgents", platform),
- supportsSkills: getFeatureSupport("skillsSystem", platform) !== "none",
- supportsHooks: isFeatureSupported("hooks", platform),
- supportsGranularPermissions: isFeatureSupported("granularPermissions", platform),
- supportsContexts: getFeatureSupport("externalContext", platform) !== "none",
- supportsCustomModels: isFeatureSupported("modelSelection", platform),
- supportsTemperature: getFeatureSupport("temperatureControl", platform) !== "none",
- supportsMaxSteps: isFeatureSupported("maxSteps", platform),
- configFormat: configFormats[platform],
- outputStructure: outputStructures[platform],
- };
- }
- // ============================================================================
- // Comparison Utilities
- // ============================================================================
- /**
- * Compare two platforms' capabilities.
- *
- * @param platformA - First platform
- * @param platformB - Second platform
- * @returns Comparison showing which features differ
- */
- export function comparePlatforms(
- platformA: Platform,
- platformB: Platform
- ): {
- identical: string[];
- betterInA: string[];
- betterInB: string[];
- different: string[];
- } {
- const identical: string[] = [];
- const betterInA: string[] = [];
- const betterInB: string[] = [];
- const different: string[] = [];
- const supportOrder: Record<SupportLevel, number> = {
- full: 2,
- partial: 1,
- none: 0,
- };
- for (const feature of CAPABILITY_MATRIX) {
- const supportA = feature.support[platformA];
- const supportB = feature.support[platformB];
- if (supportA === supportB) {
- identical.push(feature.name);
- } else {
- different.push(feature.name);
- if (supportOrder[supportA] > supportOrder[supportB]) {
- betterInA.push(feature.name);
- } else {
- betterInB.push(feature.name);
- }
- }
- }
- return { identical, betterInA, betterInB, different };
- }
- /**
- * Get a summary of what will happen during conversion.
- *
- * @param sourcePlatform - Source platform
- * @param targetPlatform - Target platform
- * @returns Human-readable summary
- */
- export function getConversionSummary(
- sourcePlatform: Platform,
- targetPlatform: Platform
- ): string[] {
- const comparison = comparePlatforms(sourcePlatform, targetPlatform);
- const summary: string[] = [];
- if (comparison.betterInA.length > 0) {
- summary.push(
- `Features that may be degraded: ${comparison.betterInA.join(", ")}`
- );
- }
- if (comparison.betterInB.length > 0) {
- summary.push(
- `Features that may be enhanced: ${comparison.betterInB.join(", ")}`
- );
- }
- if (comparison.identical.length === CAPABILITY_MATRIX.length) {
- summary.push("Full feature parity - no degradation expected");
- }
- return summary;
- }
|