team

Cross-host portability

What this is. A portability study. It shows how Team’s Claude Code plugin primitives map onto Codex CLI, and gives the strategy we chose to support that host alongside Claude Code. Team also runs on Antigravity CLI, which needed no porting strategy: it installs Team from a local checkout through a manifest of its own, like the other hosts. It is a decision document, not a code change. The source issue is #50. Two epics consume it: the #57 Codex port, which builds against the matrix, the gap analysis, and the “what #57 builds against” section below, and the #56 Antigravity backend, which builds against the Antigravity CLI host facts.

Deprecated host (2026-08-13). An earlier revision of this study scored a third host and handed it a port epic of its own. That Gemini CLI port was scored on 2026-06-27 and then dropped in favor of Antigravity CLI, which ships today. The port epic, #56, was retargeted at the Antigravity CLI as an alternate model backend rather than closed. The matrix below scores the surviving hosts only.

Contents

Current state

Team is a Claude Code-native plugin. It ships 13 agents (agents/*.md), skills (skills/*/SKILL.md + registry.json), and 8 hook programs (the four canonical hooks/*.mjs plus the hooks/codex/ and hooks/antigravity/ copies) plus three opencode/team.js adapters. Claude registers through .claude-plugin/plugin.json, Codex through hooks/hooks.json, and Antigravity through the root hooks.json; see hooks-portability.md. The orchestrator walks the QRSPI phase table (skills/team/SKILL.md). It persists state as artifact files under docs/plans/<id>/. It coordinates agents through the Task tool and SendMessage resume.

The portability surface splits cleanly. Four layers are already host-neutral:

These layers move to any host unchanged.

The portability-blocking surface is the set of Claude Code-specific contracts. There are four non-portable bindings:

  1. Hook event names and the stdin/stdout JSON contract. The stdin schema (tool_name, tool_input, cwd), the stdout/stderr envelope (hookSpecificOutput.{permissionDecision, additionalContext}, systemMessage), and exit-code semantics.
  2. Host path env vars. ${CLAUDE_PLUGIN_ROOT}, interpolated into every hook command (plugin.json:18,30,41,52), and CLAUDE_PROJECT_DIR, read from the environment inside the hook bodies (pre-compact-anchor.mjs:27, session-start-recover.mjs:31, post-write-validate.mjs:103). The two use different mechanisms: manifest interpolation and runtime env lookup.
  3. Agent/Task tool dispatch, plus SendMessage resume and depth/parallel nesting semantics.
  4. SKILL.md slash-command auto-registration, plus user-invocable.

The host also interprets agent frontmatter: name, model, tools, skills, effort, and permissionMode. Claude’s native model aliases stay in the agent files. Body-loaded dispatch on Codex and Antigravity translates those aliases through a model-selection map, then applies the host’s actual spawn arguments. See Model selection.

Agent dispatch

Of the four blocking bindings, the third — Agent/Task dispatch — needs no per-host agent registration. Team resolves it in the orchestrator itself, through the portable definition contract in skills/team/references/15-host-dispatch.md:

The consequence for this study: the “agent dispatch” primitive is reachable on every host that can spawn a subagent, without a per-host shim. The remaining per-host work includes hook registration and live verification of the permission and execution contracts. Model selection is defined below.

Desired end state

Team shares agent and skill Markdown and Node hook logic across hosts. Claude, Codex, and Antigravity already install through native manifests; no generated core or binding-shim layer was needed for those installs. Body-loaded dispatch uses the shared agent body. OpenCode separately ships its discovery adapter. Hook and permission parity remain host work owned by #56 and #57.

The earlier proposal required one .team/config.json for models, host selection, parallelism, and repositories. Model resolution does not justify that combined configuration. Only optional model overrides ship, in .team/config.json, consumed by the installed resolver before body-loaded dispatch. Agent frontmatter stays native to Claude. The remaining parity target covers the four runtime hooks, parallel and nested subagents, and structured returns.

Model selection

The installed procedure and resolver define the config and validation contract. Bundled selections:

Agent model: Codex model ID Antigravity invocation tier
opus gpt-6-astra pro
sonnet gpt-5.6-sol flash
haiku gpt-5.6-luna flash_lite

Codex preserves each agent’s effort unless an override specifies reasoning_effort. Antigravity’s invocation tier selects its effort; there is no separate effort argument. The optional <home-project>/.team/config.json replaces individual selections, for example:

{
  "codex": {
    "sonnet": { "model": "gpt-5.6-terra", "reasoning_effort": "medium" }
  }
}

The resolver rejects unknown config fields, unavailable selections, and unsupported Codex effort using capability data from the running host. It does not query a provider or spawn an agent. The orchestrator supplies that data and applies the returned arguments. Missing overrides use bundled selections; invalid overrides fail. Claude named-agent dispatch never reads this config. The .team/ ignore entry reserves local overrides; pipeline state still lives under docs/plans/<id>/.

Evidence, 2026-09-15: live probes on Codex CLI 0.154.0 and Antigravity CLI 1.2.1 used completed child responses and host runtime metadata, not model self-identification. Confidence: high for these observed selections.

Probe Observed result
Both CLIs, --model opus, sonnet, or haiku Rejected; no native CLI alias resolution
Codex, three fresh children without overrides All selected gpt-6-astra, effort medium
Codex, explicit child override gpt-5.6-luna, effort low
Antigravity, Model: inherit with a Pro parent gemini-3.1-pro-low
Antigravity, Model: pro gemini-3.1-pro-low
Antigravity, Model: flash gemini-3.8-flash-tiered
Antigravity, Model: flash_lite gemini-3.5-flash-lite

Antigravity’s invocation schema accepts inherit, flash_lite, flash, and pro; explicit opus and sonnet invocations failed validation. The native frontmatter fixture was not discovered, so its model resolution was not proven. The CLI returned exit 0 after a tool validation error, making successful child completion a separate check. Codex child turn contexts and Antigravity child gen_metadata/executor_metadata supplied the selected IDs above.

After wiring the resolver, a Codex procedure probe ran all three selections: child metadata confirmed Astra/high, Sol/medium, and Luna/low. The corresponding Antigravity procedure probe stopped at a headless file-read permission denial, including when retried from the repository workspace. Its new resolver outputs are covered by deterministic tests; the native tier calls above were verified separately. Neither probe establishes full QRSPI or cross-provider quality parity.

The Codex contract describes model/effort inheritance and explicit overrides. The Antigravity contract describes native agent tiers; its live invocation schema also exposed flash_lite during the probe.

Decision: preserve distinct tiers with explicit host selections and report requested and observed values separately. Confidence: moderate; routing is verified, but quality equivalence and the best per-host assignments are not. These defaults are initial policy choices, not a claim that another provider reproduces Claude’s quality or its security-reviewer model pin. Concrete IDs behind Antigravity tiers may change. See the installed procedure for evidence reporting, mismatch handling, and unsupported-host limits.

Patterns to follow

The capability matrix

The matrix maps each Team primitive against each host. Each cell holds one of three values. native means a direct host equivalent. workaround means a documented alternate mechanism reaches it. hard gap means the host has no facility, so the design must work around it.

Team primitive Claude Code Codex CLI
Agent/skill Markdown bodies native (loaded as-is) native (system-prompt body)
Custom slash entry points native (SKILL.md auto-register) native (built-ins and Skills. Prompts are deprecated in favor of Skills.)
On-demand SKILL.md injection native (skills: + auto-load) native (skills/<name>/SKILL.md under an installed plugin, description-matched implicit invocation). A skill opts out through policy.allow_implicit_invocation: false in its agents/openai.yamldocumented to block implicit invocation while leaving $skill working
Hide a skill from the user’s menu native (user-invocable: false keeps it out of /) hard gap: every discovered skill is listed in the $ picker; no frontmatter or manifest field suppresses one
Subagent dispatch (parallel) native (Agent/Task tool) native (spawn_agent/wait_agent…, features.multi_agent)
Nested subagents native (depth 2, ≤4, read-only) workaround: max_depth=1, nesting capped one level
Structured agent→caller output native (final-text JSON envelope) native and strongest (--output-schema JSON Schema). A silent-drop bug under tools (codex#15451) was fixed April 2026
PreToolUse hook native native (PreToolUse)
PostToolUse hook native native (PostToolUse)
SessionStart hook native native (SessionStart)
PreCompact hook native native (PreCompact, + PostCompact)
Hook stdin/stdout JSON contract native (Claude schema) workaround: own schema. A plugin hook file is a HooksFile ({"hooks": {…}}, deny_unknown_fields); events mirror Claude and exit 2 blocks. Team ships hooks/codex/ duplicates plus one reused canonical guard
Plugin-root / project-dir env vars native (${CLAUDE_PLUGIN_ROOT}, CLAUDE_PROJECT_DIR) workaround: no equivalent, so resolve through .codex/ trust + config
Always-on project context native (CLAUDE.md) native (AGENTS.md)
MCP tools native native (stdio/HTTP, OAuth, per-tool approval)
MCP prompts-as-slash-commands native hard gap: MCP prompts unsupported client-side, so route through Skills
MCP resources native native (read_mcp_resource/list_mcp_resources)
Manifest / binding format .claude-plugin/plugin.json .codex-plugin/plugin.json + .agents/plugins/marketplace.json for the package; config.toml/hooks.json + .codex/ for hooks and agents
Model-selection overrides Native agent frontmatter; no Team config needed Optional .team/config.json
Agent tier → host model Native Claude alias Installed resolver emits explicit model ID and effort

Antigravity CLI is not a third column. Only some of these rows are settled for it — manifest layout, skill and agent discovery, naming, hooks — while the MCP group and the hook JSON contract are not, and a column that said “unknown” two thirds of the way down would look scored without being scored. What is settled is in its own section; the rest sits in open questions.

Reading the matrix: every row that Team’s behavior depends on is native or workaround on Codex CLI. There is no hook-event gap. All four events map natively, and on-demand skills, subagents, MCP tools, and MCP resources are native. The two remaining hard gaps are narrow. Codex does not surface MCP prompts as slash commands — its MCP tools and resources are fine, and it has a clean detour: route slash entry through Codex Skills, below. And Codex offers no way to hide a skill from the user’s $ picker, which costs presentation rather than behavior.

The landscape is recent. As of mid-2026 Codex CLI ships a full hooks system, parallel subagents, custom slash commands, on-demand skills, MCP, and structured headless output. Earlier (2025) write-ups that treated these as hard gaps are stale. Codex’s hooks and multi-agent are young, rolled out March to May 2026 across v0.114-v0.129 (latest v0.142.3). See the recency risk in the gap analysis.

Gap analysis

After verifying every capability against the host repos (2026-06-27), the gap picture is narrower than the earlier draft assumed. One hard gap remains, one was resolved by the playbook refactor, plus a cross-cutting recency caveat:

  1. Codex does not expose MCP prompts as slash commands (hard gap). Codex MCP supports tools and resources (read_mcp_resource and list_mcp_resources). It does not support MCP prompts. “MCP-prompts-as-slash-commands” thus does not work on Codex. The workaround for #57 is to route every slash-style entry point through Codex Skills, the documented successor to deprecated custom prompts, and not through MCP. This is why the chosen strategy does not depend on MCP (decision 4).

  2. Codex lists every skill in the $ picker, so user-invocable: false is a Claude-Code-only guarantee (resolved by the playbook refactor). Before the refactor, Team’s registered methodology and principle-* skills were reference material an agent loaded, never something a human ran. On Claude Code, user-invocable: false kept them out of the / menu. Codex had no equivalent, so they all appeared under $ and a user could invoke any of them directly. The playbook refactor moved that content out of skills/ into ordinary references and playbooks read by path, so no methodology registration remains for Codex to list. The principle tier is returning as guarded disable-model-invocation skills, kept out of Codex’s implicit catalog by allow_implicit_invocation: false; the $ picker shows the 27 commands. See the divergence note for the historical evidence.

  3. Recency risk. This is cross-cutting rather than a primitive gap. Codex’s hooks and multi-agent are young. They rolled out from March to May 2026 across v0.114-v0.129 (latest v0.142.3). Treat its contracts as moving targets. The shim layer (decision 1) absorbs breaking changes in one place. The mitigation and version-pinning policy are tracked in the risk register.

Decisions made

  1. Shared definitions with native host bindings. The Markdown bodies, Node hook logic, artifact I/O, and envelope convention are maintained once. Claude, Codex, and Antigravity install through native manifests. The proposed generated core and shim layer were not needed for those installs. Add adapters only for demonstrated host differences, such as OpenCode discovery or the model selection described above. Hook parity remains port work.

  2. Rejected: single source of truth plus a full transpile/build. One canonical set. A build step emits a complete package per host. Why rejected: it forces the build to fully model three divergent manifest/agent/command formats — Claude Code’s, Codex’s, and Antigravity’s — the youngest of which (Codex’s) are still moving. The upfront modeling cost is high and the build itself becomes the highest-churn artifact, since every host API change breaks the transpiler. The hybrid keeps the same DRY core without committing to a total-coverage transpiler. Shims can stay hand-written where generation does not pay. The hybrid can generate shims later where it pays, making it a strict superset of this option’s value with less risk.

  3. Rejected: per-host maintained adapters (parallel hand-maintained trees). Why rejected: it costs 3× the maintenance across 13 agents, every skill, and 3 hooks, one tree per shipped host — Claude Code, Codex CLI, and Antigravity CLI. It also guarantees drift, because someone must apply a fix to an agent body three times by hand. It throws away the fact that the bodies are already portable. The hybrid keeps most of its only advantage, a fully idiomatic host, because host idiom lives in the shim layer anyway.

  4. MCP is documented as a bridge, not adopted as the strategy’s mechanism. The matrix records MCP’s reach on Codex: tools and resources, but not prompts-as-slash. The chosen path is native per-host bindings, and MCP is a documented fallback to revisit only if a native binding proves insufficient. Why: Codex MCP carries tools and resources but not prompts, so MCP can never be the uniform slash-command layer. Leaning on it would force a split path anyway while adding a server dependency. Keeping it as fallback preserves the option without coupling the strategy to it.

  5. Parity target for #57: full hook and subagent parity, not MVP-first. The epic targets all four runtime hooks, parallel and nested subagents, and structured returns before it declares the work done. This raises the bar against the young-API and open-bug risk. The design thus confronts those risks directly rather than defer them by a cut in scope. See the risks and “what #57 builds against” below.

  6. Model configuration is limited to model selection. Optional .team/config.json overrides bundled host mappings. The installed resolver validates them against the active host’s capabilities; dispatch applies the resulting arguments. Claude aliases and native dispatch stay unchanged. Host selection, concurrency limits, and repository lists are not part of this file. This replaces the earlier combined-config proposal after the live model resolution probes for #55.

What #57 builds against

The epic completes the remaining Codex bindings, targeting full parity. It starts from the matrix and works around the named gaps.

#57. Codex port

Antigravity CLI

Everything here is from agy 1.1.12 on macOS. The Codex CLI section comes from vendor docs and host repos read on 2026-06-27, since that binary is not installed here.

This host installs Team natively, through its own manifest. Team ships plugin.json at the repo root, which is Antigravity’s plugin marker, with skills/ and agents/ beside it where this host resolves components. agy plugin install /path/to/team then copies all skills and all 13 agents into ~/.gemini/config/plugins/team/, and import_manifest.json records the source as antigravity — its native path, the same local-checkout form the Claude Code and Codex installs use.

The root manifest is what buys that. Without it, this host falls back to recognizing .claude-plugin/ and records the source as claude-code, importing Team as a Claude Code plugin. That fallback works, but it makes Team’s presence here contingent on another host’s manifest, and it is not the path this project relies on. The manifest also cannot move into a directory of its own the way .claude-plugin/ and .codex-plugin/ do, because components resolve as siblings of the manifest — which is why Team carries a sixth version string at the repo root.

A local install can fail on a git fsmonitor socket. The install copies the whole tree, .git included, so a checkout with a running fsmonitor daemon fails on .git/fsmonitor--daemon.ipc. A worktree is unaffected, because its .git is a file, and installing from a URL clones fresh so the socket never exists.

Discovery.

disable-model-invocation is honored. With the plugin installed, the probe (taken when the plugin shipped 54 skills, two of which set the key) had the agent list 52 of them. The two missing ones were exactly pr-rebase and pr-watch-as-reviewer — the skills that set the key as of that probe. The guarded set has since grown to four: retro and no-comments set it too, so this host withholds both as well. This host therefore keeps every guarded skill out of the model’s reach on its own, and it is why Team’s install for this host withholds nothing.

Codex reaches the same end through its own key rather than this one: no-comments, pr-rebase, pr-watch-as-reviewer, and retro each declare policy.allow_implicit_invocation: false in their agents/openai.yaml. OpenAI documents that key as blocking implicit invocation while leaving explicit $skill invocation working, which is what disable-model-invocation buys on the other two hosts. The difference is in evidence, not in outcome: this host’s behavior was observed on a live probe, Codex’s rests on the documented contract.

Paths and naming.

Name collisions still resolve by precedence. Bare names mean a skill of the user’s own can carry a Team name, and the host picks a winner silently. Team no longer scans for that: it writes into its own plugin directory rather than into the shared global skill directory, so it has nothing to warn about and no authority over which copy wins. Built-in skills live inside the agy binary, so no disk scan could enumerate them anyway. Whether a project’s .agents/skills/ outranks the global scope has not been confirmed — the nearest evidence points the other way, since about fifty skills in ~/.agents/skills/ were invisible to agy.

Scope. Antigravity installs every skill and every agent, and the dev install keeps a checkout’s edits live. Dispatch is resolved host-neutrally, not by a per-host agent registration: the orchestrator reads each specialist’s portable definition and dispatches it through the host’s subagent facility (see Agent dispatch). Hooks, commands, and rules remain unported on this host. That work stays with #56.

OpenCode

OpenCode loads the native opencode/team.js plugin through one symlink in its configuration directory. script/dev-install opencode and script/dev-uninstall opencode manage that exact-owned registration; see installation for prerequisites, overrides, restart, worktrees, conflicts, dangling targets, and lock recovery. Installation validates the checkout without reading user JSON/JSONC or invoking OpenCode. Its success says registered. Malformed native configuration can still prevent loading afterward.

opencode/catalog.mjs owns catalog validation for both installation and plugin initialization. The entry resolves its real file before importing the helper, so checkout aliases and linked worktrees resolve to canonical file/base paths. Each immediate real skill directory contributes one regular SKILL.md. The validator walks its tree once without following symlinks. It rejects linked entries, nested SKILL.md, duplicate names, ambiguous consumed frontmatter, and empty catalogs. Ordinary references and scripts stay available. Headers are read once. Bodies stay on disk. Canonical paths containing $, backticks, or @ are rejected before registration or config contribution. Spaces and Unicode are supported. Existing command collisions or wrong consumed config types reject the whole contribution before paths or commands change.

Every skill gets a native configured command with its description and a quoted absolute file/base pointer. The template declares explicit invocation, requests a filesystem read, resolves relative references against the canonical base, and supplies $ARGUMENTS. It embeds no skill body and sets no model or agent override. This preserves literal shell examples and argument references inside canonical skill content. Even the guarded disable-model-invocation: true skills appear in the command menu. Commands include /retro, whose transcript resolver supports Claude Code, Codex, and OpenCode.

disable-model-invocation: true excludes a directory from Team’s added skills.paths only. Other paths retain their order and exact duplicates are removed. Team does not rewrite user-owned skill sources. An external source can expose its own guarded copies or win resolution of a duplicate skill name. Inspect the resolved skill’s location separately from the command template: Team’s configured commands retain canonical pointers despite duplicate skill sources. Later plugins or MCP commands can still collide; the initialization collision check cannot guarantee ownership after other contributors run.

Command permissions and preprocessing

A Team command requests a filesystem read subject to native read and external_directory rules. An unguarded skill-tool call instead uses skill permission and returns content cached by native discovery. read: deny does not deny that cached route. Guarded skills remain available as explicit command-file read requests. Team leaves existing read, external_directory, skill, and bash rules unchanged and adds no permission overrides.

OpenCode preprocesses supplied command arguments. For example, the native syntax below can run printf before any model call:

/team-question !`printf example`

This shell substitution runs outside model-tool permission checks, including bash: deny. Native file references and placeholders also retain their native argument behavior. Canonical file pointers protect skill body text, not untrusted arguments. Team adds no argument escaping or expansion adapter.

Lifecycle and support limits

The shared Node lifecycle helper creates the plugin parent only for installation, canonicalizes it, then atomically acquires plugins/team.js.lock before inspecting or changing the target. Config aliases therefore share a lock. Matching absolute symlinks converge on reinstall/removal, including a missing runtime target on uninstall. Foreign links and non-link targets fail unchanged. Cleanup removes only the acquired empty lock. A busy/stale lock requires manual recovery after checking no lifecycle process remains. Config, credentials, other plugins, and existing parents remain untouched.

Concurrent checkout edits during catalog loading and external programs replacing files without the lifecycle lock are unsupported. A new OpenCode process reads current checkout content. No generated catalog, cache copy, provider call, or persistent Team process is introduced.

Native discovery was observed on OpenCode 1.18.20. Support covers registration, skill/command discovery, and the developer lifecycle. Three runtime hooks are bound in opencode/team.js — recovery, compaction, and write validation — and their program contracts are probed; the prompt-block .team/config.json guard is a named gap. Full QRSPI execution, specialist/nested-agent dispatch, translated reviewer permissions, and live host-firing of the adapters remain unverified. No provider, credentials, model-tier translation, or model-quality guarantee is installed. /retro resolves OpenCode sessions from the host’s SQLite store.

Out of scope

Edge cases

These are the boundary conditions the strategy and the downstream epic must handle.

Open questions (deferred to the port epic)

Risks

See also

Installed contract resources

Artifact schemas and shell rules are ordinary files under skills/team/references/: artifacts.md and external-data.md. They add no skill registrations. Read conditional artifact templates only when their operation applies.

Both named and body-loaded dispatches supply the installed root, agent definition, and applicable resource paths before work. Standalone agents resolve their installed definition or receive its path from the dispatcher. Skill links resolve from the loaded SKILL.md; agent links resolve from the installed agents/<name>.md. Claude uses its plugin root. Codex and Antigravity use the supplied installed path. OpenCode keeps its canonical realpath base. A missing resource stops its consuming step with the resolved path. No source-checkout fallback or recursive loading applies.

Filesystem fixture results establish path and byte delivery. Native receiving-agent read traces establish instruction consumption separately.