|
|
@@ -37,7 +37,19 @@ export type { BinaryProjection } from "./Capabilities.js";
|
|
|
// Types
|
|
|
// ============================================================================
|
|
|
|
|
|
-export type Platform = "oac" | "claude" | "cursor" | "windsurf";
|
|
|
+/**
|
|
|
+ * Every platform this matrix describes.
|
|
|
+ *
|
|
|
+ * `oac` is the IR itself — the canonical source every other column is measured against — not
|
|
|
+ * an emitted target; it has no adapter. The rest map 1:1 onto `src/adapters/*Adapter.ts`.
|
|
|
+ *
|
|
|
+ * **`opencode` was absent from this union until 2026-07-15**, despite OpenCode being one of
|
|
|
+ * the two first-class targets (`07-EXECUTION-PLAN.md`) and `OpenCodeAdapter` shipping since
|
|
|
+ * `d100ccd`. The consequence was not cosmetic: with nowhere to be described, OpenCodeAdapter
|
|
|
+ * hand-wrote its own `getCapabilities()` while ClaudeAdapter derived from this table — two
|
|
|
+ * patterns, and the drift that follows. See {@link getToolCapabilities}.
|
|
|
+ */
|
|
|
+export type Platform = "oac" | "claude" | "cursor" | "windsurf" | "opencode";
|
|
|
|
|
|
/**
|
|
|
* Feature categories for the capability matrix
|
|
|
@@ -99,14 +111,14 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
name: "multipleAgents",
|
|
|
category: "agents",
|
|
|
description: "Support for multiple agent definitions",
|
|
|
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "full", opencode: "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" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
notes: { windsurf: "Limited mode support" },
|
|
|
},
|
|
|
{
|
|
|
@@ -116,16 +128,29 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
// 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" },
|
|
|
+ //
|
|
|
+ // OpenCode strips the `oac:` block too, so `category` is likewise not a frontmatter field
|
|
|
+ // there — but its agents are emitted to `.opencode/agent/<category>/<name>.md`, mirroring
|
|
|
+ // content/agents/, so the category survives structurally and round-trips via the path.
|
|
|
+ // Claude's are flat (`plugins/claude-code/agents/<id>.md`), so it is lost outright.
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
+ notes: {
|
|
|
+ claude: "No category field in agent frontmatter — dropped on emit",
|
|
|
+ opencode: "Carried as the agent's directory (.opencode/agent/<category>/), not a field",
|
|
|
+ },
|
|
|
},
|
|
|
|
|
|
// Permission Features
|
|
|
+ //
|
|
|
+ // OpenCode scores "full" across this whole category for one reason: the canonical
|
|
|
+ // `permission:` block IS OpenCode's own field. There is no projection on that path —
|
|
|
+ // OpenCodeAdapter emits the rules verbatim and OpenCode resolves them last-match-wins.
|
|
|
+ // Every other target is measured by how much of that shape it loses.
|
|
|
{
|
|
|
name: "granularPermissions",
|
|
|
category: "permissions",
|
|
|
description: "Fine-grained allow/deny/ask patterns",
|
|
|
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "none", opencode: "full" },
|
|
|
notes: {
|
|
|
claude:
|
|
|
"tools/disallowedTools are flat name lists — a capability is wholly granted or " +
|
|
|
@@ -142,16 +167,17 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
// 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" },
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "none", opencode: "full" },
|
|
|
notes: {
|
|
|
claude: "No rule ordering concept — the allowlist cannot be carried, so Bash is denied",
|
|
|
+ opencode: "Rules keep their authored order; OpenCode resolves them last-match-wins",
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: "askPermissions",
|
|
|
category: "permissions",
|
|
|
description: "Interactive permission requests",
|
|
|
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "none", opencode: "full" },
|
|
|
notes: {
|
|
|
claude: "No 'ask' in agent frontmatter — degrades to deny, never to allow",
|
|
|
},
|
|
|
@@ -160,7 +186,7 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
name: "pathPatterns",
|
|
|
category: "permissions",
|
|
|
description: "Glob patterns for file permissions",
|
|
|
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial" },
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
notes: {
|
|
|
claude: "Tool grants carry no path scope — secret-file denies cannot be expressed",
|
|
|
},
|
|
|
@@ -173,32 +199,32 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
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" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
},
|
|
|
{
|
|
|
name: "taskDelegation",
|
|
|
category: "tools",
|
|
|
description: "Agent-to-agent task delegation",
|
|
|
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
notes: { cursor: "No delegation support" },
|
|
|
},
|
|
|
{
|
|
|
name: "bashExecution",
|
|
|
category: "tools",
|
|
|
description: "Shell command execution",
|
|
|
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "full", windsurf: "full", opencode: "full" },
|
|
|
},
|
|
|
{
|
|
|
name: "fileOperations",
|
|
|
category: "tools",
|
|
|
description: "Read/write/edit file operations",
|
|
|
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "full", windsurf: "full", opencode: "full" },
|
|
|
},
|
|
|
{
|
|
|
name: "searchOperations",
|
|
|
category: "tools",
|
|
|
description: "Grep/glob search operations",
|
|
|
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "full", windsurf: "full", opencode: "full" },
|
|
|
},
|
|
|
|
|
|
// Context Features
|
|
|
@@ -206,26 +232,36 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
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" },
|
|
|
+ // Cursor is "none", and the adapter used to disagree — `CursorAdapter.getCapabilities()`
|
|
|
+ // hand-wrote `supportsContexts: true // ✅ Can inline context`, which was the same class of
|
|
|
+ // bug as the old json/markdown split (one platform, two answers) and survived because the
|
|
|
+ // agreement test covered only claude.configFormat. Ruled 2026-07-15: the feature is
|
|
|
+ // external *references*. Cursor cannot reference; pasting the bytes into .cursorrules
|
|
|
+ // discards the reference, the file boundary and the priority. That is degradation, not
|
|
|
+ // support — and calling it support would hide real loss from every compatibility report.
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "full", opencode: "full" },
|
|
|
+ notes: { cursor: "Context must be inline in .cursorrules — reference and priority are lost" },
|
|
|
},
|
|
|
{
|
|
|
name: "contextPriority",
|
|
|
category: "context",
|
|
|
description: "Priority levels for context loading",
|
|
|
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
|
|
|
+ // OpenCode installs context files byte-for-byte, so the MVI header that carries the level
|
|
|
+ // (`<!-- Context: … | Priority: critical | … -->`) survives verbatim in .opencode/context/.
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "none", opencode: "full" },
|
|
|
+ notes: { opencode: "MVI priority header survives verbatim in the installed context file" },
|
|
|
},
|
|
|
{
|
|
|
name: "contextSubdirs",
|
|
|
category: "context",
|
|
|
description: "Nested context directory structure",
|
|
|
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "full" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "full", opencode: "full" },
|
|
|
},
|
|
|
{
|
|
|
name: "skillsSystem",
|
|
|
category: "context",
|
|
|
description: "Loadable skill modules",
|
|
|
- support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
},
|
|
|
|
|
|
// Model Features
|
|
|
@@ -233,13 +269,15 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
name: "modelSelection",
|
|
|
category: "model",
|
|
|
description: "Custom model selection",
|
|
|
- support: { oac: "full", claude: "full", cursor: "full", windsurf: "full" },
|
|
|
+ support: { oac: "full", claude: "full", cursor: "full", windsurf: "full", opencode: "full" },
|
|
|
},
|
|
|
{
|
|
|
name: "temperatureControl",
|
|
|
category: "model",
|
|
|
description: "Temperature parameter control",
|
|
|
- support: { oac: "full", claude: "none", cursor: "partial", windsurf: "partial" },
|
|
|
+ // opencode "full" verified against disk: real agents carry the field
|
|
|
+ // (.opencode/agent/eval-runner.md, subagents/system-builder/command-creator.md).
|
|
|
+ support: { oac: "full", claude: "none", cursor: "partial", windsurf: "partial", opencode: "full" },
|
|
|
notes: {
|
|
|
claude: "Temperature not configurable",
|
|
|
cursor: "Limited range",
|
|
|
@@ -250,7 +288,8 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
name: "maxSteps",
|
|
|
category: "model",
|
|
|
description: "Maximum execution steps limit",
|
|
|
- support: { oac: "full", claude: "none", cursor: "none", windsurf: "none" },
|
|
|
+ // opencode "full" verified against disk — same agents as temperatureControl.
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "none", opencode: "full" },
|
|
|
},
|
|
|
|
|
|
// Advanced Features
|
|
|
@@ -258,8 +297,15 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
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" },
|
|
|
+ // opencode "none": .opencode/plugin/ holds plugins (agent-validator.ts), which are a
|
|
|
+ // different mechanism — there is no PreToolUse/PostToolUse event surface to emit into.
|
|
|
+ // Matches OpenCodeAdapter's own long-standing `supportsHooks: false`.
|
|
|
+ support: { oac: "full", claude: "full", cursor: "none", windsurf: "none", opencode: "none" },
|
|
|
+ notes: {
|
|
|
+ cursor: "No hook support",
|
|
|
+ windsurf: "No hook support",
|
|
|
+ opencode: "Plugins (.opencode/plugin/) are a different mechanism, not event hooks",
|
|
|
+ },
|
|
|
},
|
|
|
{
|
|
|
name: "dependencies",
|
|
|
@@ -268,8 +314,16 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
// 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" },
|
|
|
+ //
|
|
|
+ // OpenCode is "none" for the same reason, which is easy to miss because OpenCode is the
|
|
|
+ // canonical target: `dependencies` lives in the `oac:` block, and OpenCodeAdapter strips
|
|
|
+ // that block on emit. Being lossless about OpenCode's OWN fields does not make it
|
|
|
+ // lossless about OAC's.
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial", opencode: "none" },
|
|
|
+ notes: {
|
|
|
+ claude: "Dependencies resolve at build time; not carried in agent frontmatter",
|
|
|
+ opencode: "Declared in the oac: block, which is stripped on emit; resolved at build time",
|
|
|
+ },
|
|
|
},
|
|
|
{
|
|
|
name: "priorityLevels",
|
|
|
@@ -277,8 +331,20 @@ const CAPABILITY_MATRIX: FeatureDefinition[] = [
|
|
|
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" },
|
|
|
+ //
|
|
|
+ // FIXME(2026-07-15): this row describes a concept that does not exist. `types.ts` has only
|
|
|
+ // ContextPrioritySchema — CONTEXT priority — whose four levels (critical/high/medium/low)
|
|
|
+ // are almost certainly the "4 levels" the oac note means. There is no task-priority field
|
|
|
+ // anywhere in the canonical schema, which makes this row a mis-named duplicate of
|
|
|
+ // `contextPriority`. Left as-is deliberately: deciding whether to delete it or rename it is
|
|
|
+ // a separate call, and folding that into an opencode-gap fix would bury it.
|
|
|
+ support: { oac: "full", claude: "none", cursor: "none", windsurf: "partial", opencode: "full" },
|
|
|
+ notes: {
|
|
|
+ oac: "4 levels",
|
|
|
+ claude: "No task priority concept",
|
|
|
+ windsurf: "2 levels",
|
|
|
+ opencode: "Tracks contextPriority — see the FIXME on this row",
|
|
|
+ },
|
|
|
},
|
|
|
];
|
|
|
|
|
|
@@ -501,6 +567,7 @@ export function getToolCapabilities(
|
|
|
claude: "Claude Code",
|
|
|
cursor: "Cursor IDE",
|
|
|
windsurf: "Windsurf",
|
|
|
+ opencode: "OpenCode",
|
|
|
};
|
|
|
|
|
|
const configFormats: Record<Exclude<Platform, "oac">, ToolCapabilities["configFormat"]> = {
|
|
|
@@ -512,6 +579,9 @@ export function getToolCapabilities(
|
|
|
claude: "markdown",
|
|
|
cursor: "plain",
|
|
|
windsurf: "json",
|
|
|
+ // Agent files are markdown with YAML frontmatter, emitted to
|
|
|
+ // .opencode/agent/<category>/<name>.md.
|
|
|
+ opencode: "markdown",
|
|
|
};
|
|
|
|
|
|
const outputStructures: Record<
|
|
|
@@ -521,6 +591,7 @@ export function getToolCapabilities(
|
|
|
claude: "directory",
|
|
|
cursor: "single-file",
|
|
|
windsurf: "directory",
|
|
|
+ opencode: "directory",
|
|
|
};
|
|
|
|
|
|
return {
|