CapabilityMatrix.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. /**
  2. * CapabilityMatrix - Feature compatibility matrix across platforms
  3. *
  4. * Provides a centralized registry of what features each platform supports,
  5. * enabling pre-conversion validation and compatibility reporting.
  6. *
  7. * @example
  8. * ```ts
  9. * const compatibility = analyzeCompatibility(agent, 'cursor');
  10. * // => { compatible: false, warnings: [...], blockers: [...] }
  11. * ```
  12. */
  13. import type { OpenAgent, ToolCapabilities } from "../types.js";
  14. /**
  15. * Re-export only. The ordered-rule collapse lives in `Capabilities.ts`, which owns
  16. * last-match-wins; this file is a static per-platform support matrix and must not grow a
  17. * second, divergent resolver. Callers reach `degradeToBinary` here because a per-capability
  18. * grant is the matrix's own question ("what survives on this platform?"), answered by the
  19. * resolver rather than duplicated against it.
  20. *
  21. * ## Ownership (settled by subtask 07; subtask 04 flagged the overlap)
  22. *
  23. * `Capabilities.ts` is the sole IMPLEMENTATION — one resolver, one fail-closed projection.
  24. * This module is a re-export SURFACE and nothing more: no logic, no wrapper, no defaults.
  25. * The re-export is load-bearing rather than convenience —
  26. * `tests/unit/build/permission-ordering.test.ts` imports `degradeToBinary` from this path by
  27. * name and pins it to subtask 07, so deleting it would break a green gate. New code should
  28. * prefer importing from `./Capabilities.js` directly; adapters that need the flat-list
  29. * projection want `projectToFlatTools`, which is only exported there.
  30. */
  31. export { degradeToBinary } from "./Capabilities.js";
  32. export type { BinaryProjection } from "./Capabilities.js";
  33. // ============================================================================
  34. // Types
  35. // ============================================================================
  36. export type Platform = "oac" | "claude" | "cursor" | "windsurf";
  37. /**
  38. * Feature categories for the capability matrix
  39. */
  40. export type FeatureCategory =
  41. | "agents"
  42. | "permissions"
  43. | "tools"
  44. | "context"
  45. | "model"
  46. | "advanced";
  47. /**
  48. * Support level for a feature
  49. */
  50. export type SupportLevel = "full" | "partial" | "none";
  51. /**
  52. * Feature definition in the capability matrix
  53. */
  54. export interface FeatureDefinition {
  55. name: string;
  56. category: FeatureCategory;
  57. description: string;
  58. support: Record<Platform, SupportLevel>;
  59. notes?: Partial<Record<Platform, string>>;
  60. }
  61. /**
  62. * Compatibility analysis result
  63. */
  64. export interface CompatibilityResult {
  65. /** Overall compatibility assessment */
  66. compatible: boolean;
  67. /** Score from 0-100 representing compatibility percentage */
  68. score: number;
  69. /** Warnings about features that will be degraded */
  70. warnings: string[];
  71. /** Blocking issues that prevent conversion */
  72. blockers: string[];
  73. /** Features that will be fully preserved */
  74. preserved: string[];
  75. /** Features that will be partially preserved */
  76. degraded: string[];
  77. /** Features that will be lost */
  78. lost: string[];
  79. }
  80. // ============================================================================
  81. // Capability Matrix
  82. // ============================================================================
  83. /**
  84. * Complete feature capability matrix
  85. */
  86. const CAPABILITY_MATRIX: FeatureDefinition[] = [
  87. // Agent Features
  88. {
  89. name: "multipleAgents",
  90. category: "agents",
  91. description: "Support for multiple agent definitions",
  92. support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
  93. notes: { cursor: "Single .cursorrules file only - agents will be merged" },
  94. },
  95. {
  96. name: "agentModes",
  97. category: "agents",
  98. description: "Primary/subagent mode distinction",
  99. support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
  100. notes: { windsurf: "Limited mode support" },
  101. },
  102. {
  103. name: "agentCategories",
  104. category: "agents",
  105. description: "Agent categorization (core, development, etc.)",
  106. // Claude Code agent frontmatter has no category field — the canonical `oac.category`
  107. // survives only as the directory an author happens to file the source under, and is not
  108. // carried into the emitted agent at all.
  109. support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
  110. notes: { claude: "No category field in agent frontmatter — dropped on emit" },
  111. },
  112. // Permission Features
  113. {
  114. name: "granularPermissions",
  115. category: "permissions",
  116. description: "Fine-grained allow/deny/ask patterns",
  117. support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
  118. notes: {
  119. claude:
  120. "tools/disallowedTools are flat name lists — a capability is wholly granted or " +
  121. "wholly denied, so anything scoped degrades fail-closed to disallowedTools",
  122. cursor: "Binary on/off only",
  123. windsurf: "Binary on/off only",
  124. },
  125. },
  126. {
  127. name: "orderedPermissionRules",
  128. category: "permissions",
  129. description: "Ordered rule lists resolved last-match-wins (deny-all-then-allowlist)",
  130. // The central fact of the canonical refactor, and previously unrepresented here: the
  131. // shipped agents' security posture IS the rule order (`bash: {"*": deny, "git log*":
  132. // allow}`). A target scoring "none" cannot carry that shape at all, which is why
  133. // Capabilities.degradeToBinary refuses it rather than picking a winning rule.
  134. support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
  135. notes: {
  136. claude: "No rule ordering concept — the allowlist cannot be carried, so Bash is denied",
  137. },
  138. },
  139. {
  140. name: "askPermissions",
  141. category: "permissions",
  142. description: "Interactive permission requests",
  143. support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
  144. notes: {
  145. claude: "No 'ask' in agent frontmatter — degrades to deny, never to allow",
  146. },
  147. },
  148. {
  149. name: "pathPatterns",
  150. category: "permissions",
  151. description: "Glob patterns for file permissions",
  152. support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
  153. notes: {
  154. claude: "Tool grants carry no path scope — secret-file denies cannot be expressed",
  155. },
  156. },
  157. // Tool Features
  158. {
  159. name: "binaryToolGrants",
  160. category: "tools",
  161. description: "Flat allow/deny lists of tool names (tools / disallowedTools)",
  162. // What Claude Code DOES support, stated positively — this is the entire target surface
  163. // ClaudeAdapter emits into, and the matrix previously described only what was missing.
  164. support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
  165. },
  166. {
  167. name: "taskDelegation",
  168. category: "tools",
  169. description: "Agent-to-agent task delegation",
  170. support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
  171. notes: { cursor: "No delegation support" },
  172. },
  173. {
  174. name: "bashExecution",
  175. category: "tools",
  176. description: "Shell command execution",
  177. support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
  178. },
  179. {
  180. name: "fileOperations",
  181. category: "tools",
  182. description: "Read/write/edit file operations",
  183. support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
  184. },
  185. {
  186. name: "searchOperations",
  187. category: "tools",
  188. description: "Grep/glob search operations",
  189. support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
  190. },
  191. // Context Features
  192. {
  193. name: "externalContext",
  194. category: "context",
  195. description: "External context file references",
  196. support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
  197. notes: { cursor: "Context must be inline in .cursorrules" },
  198. },
  199. {
  200. name: "contextPriority",
  201. category: "context",
  202. description: "Priority levels for context loading",
  203. support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
  204. },
  205. {
  206. name: "contextSubdirs",
  207. category: "context",
  208. description: "Nested context directory structure",
  209. support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
  210. },
  211. {
  212. name: "skillsSystem",
  213. category: "context",
  214. description: "Loadable skill modules",
  215. support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
  216. },
  217. // Model Features
  218. {
  219. name: "modelSelection",
  220. category: "model",
  221. description: "Custom model selection",
  222. support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
  223. },
  224. {
  225. name: "temperatureControl",
  226. category: "model",
  227. description: "Temperature parameter control",
  228. support: { oac: "full", claude: "none", cursor: "partial", windsurf: "partial" },
  229. notes: {
  230. claude: "Temperature not configurable",
  231. cursor: "Limited range",
  232. windsurf: "Maps to creativity setting",
  233. },
  234. },
  235. {
  236. name: "maxSteps",
  237. category: "model",
  238. description: "Maximum execution steps limit",
  239. support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
  240. },
  241. // Advanced Features
  242. {
  243. name: "hooks",
  244. category: "advanced",
  245. description: "Event hooks (PreToolUse, PostToolUse, etc.)",
  246. support: { oac: "full", claude: "full", cursor: "none", windsurf: "none" },
  247. notes: { cursor: "No hook support", windsurf: "No hook support" },
  248. },
  249. {
  250. name: "dependencies",
  251. category: "advanced",
  252. description: "Agent dependency declarations",
  253. // Claude Code agent frontmatter accepts name/description/tools/disallowedTools/model and
  254. // nothing else. `oac.dependencies` is resolved at build time and then dropped; the
  255. // emitted agent declares no dependencies, so "full" overstated this.
  256. support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
  257. notes: { claude: "Dependencies resolve at build time; not carried in agent frontmatter" },
  258. },
  259. {
  260. name: "priorityLevels",
  261. category: "advanced",
  262. description: "Task priority levels",
  263. // "2 levels" was not a Claude Code feature — nothing in agent frontmatter or the plugin
  264. // format expresses task priority.
  265. support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
  266. notes: { oac: "4 levels", claude: "No task priority concept", windsurf: "2 levels" },
  267. },
  268. ];
  269. // ============================================================================
  270. // Core Functions
  271. // ============================================================================
  272. /**
  273. * Get the full capability matrix.
  274. *
  275. * @returns Array of all feature definitions
  276. */
  277. export function getCapabilityMatrix(): FeatureDefinition[] {
  278. return [...CAPABILITY_MATRIX];
  279. }
  280. /**
  281. * Get features by category.
  282. *
  283. * @param category - Feature category to filter by
  284. * @returns Array of features in that category
  285. */
  286. export function getFeaturesByCategory(
  287. category: FeatureCategory
  288. ): FeatureDefinition[] {
  289. return CAPABILITY_MATRIX.filter((f) => f.category === category);
  290. }
  291. /**
  292. * Get the support level for a specific feature on a platform.
  293. *
  294. * @param featureName - Name of the feature
  295. * @param platform - Target platform
  296. * @returns Support level or undefined if feature not found
  297. */
  298. export function getFeatureSupport(
  299. featureName: string,
  300. platform: Platform
  301. ): SupportLevel | undefined {
  302. const feature = CAPABILITY_MATRIX.find((f) => f.name === featureName);
  303. return feature?.support[platform];
  304. }
  305. /**
  306. * Check if a feature is fully supported on a platform.
  307. *
  308. * @param featureName - Name of the feature
  309. * @param platform - Target platform
  310. * @returns True if fully supported
  311. */
  312. export function isFeatureSupported(
  313. featureName: string,
  314. platform: Platform
  315. ): boolean {
  316. return getFeatureSupport(featureName, platform) === "full";
  317. }
  318. // ============================================================================
  319. // Compatibility Analysis
  320. // ============================================================================
  321. /**
  322. * Analyze compatibility of an OpenAgent with a target platform.
  323. *
  324. * @param agent - The OpenAgent to analyze
  325. * @param targetPlatform - Target platform for conversion
  326. * @returns Detailed compatibility analysis
  327. */
  328. export function analyzeCompatibility(
  329. agent: OpenAgent,
  330. targetPlatform: Exclude<Platform, "oac">
  331. ): CompatibilityResult {
  332. const warnings: string[] = [];
  333. const blockers: string[] = [];
  334. const preserved: string[] = [];
  335. const degraded: string[] = [];
  336. const lost: string[] = [];
  337. // Check agent mode
  338. if (agent.frontmatter.mode === "subagent") {
  339. const modeSupport = getFeatureSupport("agentModes", targetPlatform);
  340. if (modeSupport === "none") {
  341. warnings.push(`Agent mode 'subagent' not supported by ${targetPlatform}`);
  342. degraded.push("agentModes");
  343. } else if (modeSupport === "partial") {
  344. warnings.push(`Agent mode may have limited support on ${targetPlatform}`);
  345. degraded.push("agentModes");
  346. } else {
  347. preserved.push("agentModes");
  348. }
  349. }
  350. // Check temperature
  351. if (agent.frontmatter.temperature !== undefined) {
  352. const tempSupport = getFeatureSupport("temperatureControl", targetPlatform);
  353. if (tempSupport === "none") {
  354. warnings.push(
  355. `Temperature setting (${agent.frontmatter.temperature}) will be ignored by ${targetPlatform}`
  356. );
  357. lost.push("temperatureControl");
  358. } else if (tempSupport === "partial") {
  359. warnings.push(`Temperature will be approximated on ${targetPlatform}`);
  360. degraded.push("temperatureControl");
  361. } else {
  362. preserved.push("temperatureControl");
  363. }
  364. }
  365. // Check hooks
  366. if (agent.frontmatter.hooks && agent.frontmatter.hooks.length > 0) {
  367. const hookSupport = getFeatureSupport("hooks", targetPlatform);
  368. if (hookSupport === "none") {
  369. blockers.push(
  370. `Hooks are not supported by ${targetPlatform} - ${agent.frontmatter.hooks.length} hook(s) will be lost`
  371. );
  372. lost.push("hooks");
  373. } else {
  374. preserved.push("hooks");
  375. }
  376. }
  377. // Check skills
  378. if (agent.frontmatter.skills && agent.frontmatter.skills.length > 0) {
  379. const skillSupport = getFeatureSupport("skillsSystem", targetPlatform);
  380. if (skillSupport === "none") {
  381. warnings.push(
  382. `Skills system not supported by ${targetPlatform} - skills will be converted to inline context`
  383. );
  384. degraded.push("skillsSystem");
  385. } else if (skillSupport === "partial") {
  386. warnings.push(`Skills may have limited functionality on ${targetPlatform}`);
  387. degraded.push("skillsSystem");
  388. } else {
  389. preserved.push("skillsSystem");
  390. }
  391. }
  392. // Check granular permissions
  393. if (agent.frontmatter.permission) {
  394. const hasGranular = Object.values(agent.frontmatter.permission).some(
  395. (rule) => typeof rule === "object" && rule !== null
  396. );
  397. if (hasGranular) {
  398. const permSupport = getFeatureSupport("granularPermissions", targetPlatform);
  399. if (permSupport === "none") {
  400. warnings.push(
  401. `Granular permissions will be simplified to binary allow/deny for ${targetPlatform}`
  402. );
  403. degraded.push("granularPermissions");
  404. }
  405. }
  406. }
  407. // Check contexts
  408. if (agent.contexts && agent.contexts.length > 0) {
  409. const contextSupport = getFeatureSupport("externalContext", targetPlatform);
  410. if (contextSupport === "none") {
  411. warnings.push(
  412. `External context files not supported by ${targetPlatform} - content must be inline`
  413. );
  414. degraded.push("externalContext");
  415. } else {
  416. preserved.push("externalContext");
  417. }
  418. // Check priority
  419. const hasPriority = agent.contexts.some((c) => c.priority);
  420. if (hasPriority) {
  421. const prioritySupport = getFeatureSupport("contextPriority", targetPlatform);
  422. if (prioritySupport === "none") {
  423. warnings.push(`Context priority metadata will be ignored by ${targetPlatform}`);
  424. lost.push("contextPriority");
  425. }
  426. }
  427. }
  428. // Check maxSteps
  429. if (agent.frontmatter.maxSteps !== undefined) {
  430. const stepsSupport = getFeatureSupport("maxSteps", targetPlatform);
  431. if (stepsSupport === "none") {
  432. warnings.push(`maxSteps setting will be ignored by ${targetPlatform}`);
  433. lost.push("maxSteps");
  434. }
  435. }
  436. // Calculate compatibility score
  437. const totalFeatures = preserved.length + degraded.length + lost.length;
  438. const score =
  439. totalFeatures > 0
  440. ? Math.round(
  441. ((preserved.length + degraded.length * 0.5) / totalFeatures) * 100
  442. )
  443. : 100;
  444. return {
  445. compatible: blockers.length === 0,
  446. score,
  447. warnings,
  448. blockers,
  449. preserved,
  450. degraded,
  451. lost,
  452. };
  453. }
  454. // ============================================================================
  455. // ToolCapabilities Generation
  456. // ============================================================================
  457. /**
  458. * Generate a ToolCapabilities object for a platform.
  459. *
  460. * @param platform - Target platform
  461. * @returns ToolCapabilities object
  462. */
  463. export function getToolCapabilities(
  464. platform: Exclude<Platform, "oac">
  465. ): ToolCapabilities {
  466. const displayNames: Record<Exclude<Platform, "oac">, string> = {
  467. claude: "Claude Code",
  468. cursor: "Cursor IDE",
  469. windsurf: "Windsurf",
  470. };
  471. const configFormats: Record<Exclude<Platform, "oac">, ToolCapabilities["configFormat"]> = {
  472. // Claude Code agents are markdown files with YAML frontmatter
  473. // (`plugins/claude-code/agents/<id>.md`). This row previously read "json" on the
  474. // reasoning that `settings.json` is JSON — but settings.json is not what any adapter
  475. // emits, and ClaudeAdapter.getCapabilities() simultaneously reported "markdown". One
  476. // platform cannot have two answers about itself; the emitted artifact decides.
  477. claude: "markdown",
  478. cursor: "plain",
  479. windsurf: "json",
  480. };
  481. const outputStructures: Record<
  482. Exclude<Platform, "oac">,
  483. ToolCapabilities["outputStructure"]
  484. > = {
  485. claude: "directory",
  486. cursor: "single-file",
  487. windsurf: "directory",
  488. };
  489. return {
  490. name: platform,
  491. displayName: displayNames[platform],
  492. supportsMultipleAgents: isFeatureSupported("multipleAgents", platform),
  493. supportsSkills: getFeatureSupport("skillsSystem", platform) !== "none",
  494. supportsHooks: isFeatureSupported("hooks", platform),
  495. supportsGranularPermissions: isFeatureSupported("granularPermissions", platform),
  496. supportsContexts: getFeatureSupport("externalContext", platform) !== "none",
  497. supportsCustomModels: isFeatureSupported("modelSelection", platform),
  498. supportsTemperature: getFeatureSupport("temperatureControl", platform) !== "none",
  499. supportsMaxSteps: isFeatureSupported("maxSteps", platform),
  500. configFormat: configFormats[platform],
  501. outputStructure: outputStructures[platform],
  502. };
  503. }
  504. // ============================================================================
  505. // Comparison Utilities
  506. // ============================================================================
  507. /**
  508. * Compare two platforms' capabilities.
  509. *
  510. * @param platformA - First platform
  511. * @param platformB - Second platform
  512. * @returns Comparison showing which features differ
  513. */
  514. export function comparePlatforms(
  515. platformA: Platform,
  516. platformB: Platform
  517. ): {
  518. identical: string[];
  519. betterInA: string[];
  520. betterInB: string[];
  521. different: string[];
  522. } {
  523. const identical: string[] = [];
  524. const betterInA: string[] = [];
  525. const betterInB: string[] = [];
  526. const different: string[] = [];
  527. const supportOrder: Record<SupportLevel, number> = {
  528. full: 2,
  529. partial: 1,
  530. none: 0,
  531. };
  532. for (const feature of CAPABILITY_MATRIX) {
  533. const supportA = feature.support[platformA];
  534. const supportB = feature.support[platformB];
  535. if (supportA === supportB) {
  536. identical.push(feature.name);
  537. } else {
  538. different.push(feature.name);
  539. if (supportOrder[supportA] > supportOrder[supportB]) {
  540. betterInA.push(feature.name);
  541. } else {
  542. betterInB.push(feature.name);
  543. }
  544. }
  545. }
  546. return { identical, betterInA, betterInB, different };
  547. }
  548. /**
  549. * Get a summary of what will happen during conversion.
  550. *
  551. * @param sourcePlatform - Source platform
  552. * @param targetPlatform - Target platform
  553. * @returns Human-readable summary
  554. */
  555. export function getConversionSummary(
  556. sourcePlatform: Platform,
  557. targetPlatform: Platform
  558. ): string[] {
  559. const comparison = comparePlatforms(sourcePlatform, targetPlatform);
  560. const summary: string[] = [];
  561. if (comparison.betterInA.length > 0) {
  562. summary.push(
  563. `Features that may be degraded: ${comparison.betterInA.join(", ")}`
  564. );
  565. }
  566. if (comparison.betterInB.length > 0) {
  567. summary.push(
  568. `Features that may be enhanced: ${comparison.betterInB.join(", ")}`
  569. );
  570. }
  571. if (comparison.identical.length === CAPABILITY_MATRIX.length) {
  572. summary.push("Full feature parity - no degradation expected");
  573. }
  574. return summary;
  575. }