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
- Agent dispatch
- Desired end state
- Patterns to follow
- The capability matrix
- Gap analysis
- Decisions made
- What #57 builds against
- Antigravity CLI
- OpenCode
- Out of scope
- Edge cases
- Open questions (deferred to the port epic)
- Risks
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:
- The Markdown bodies of every agent and skill. They are plain prose and use no Claude Code APIs.
- The
.mjshook logic. It uses the Node stdlib only, sonode:fs/promises,node:child_process,node:path, andnode:url, with zero npm deps. - The artifact file I/O under
docs/plans/<id>/. - The agent→orchestrator JSON-envelope convention.
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:
- 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. - Host path env vars.
${CLAUDE_PLUGIN_ROOT}, interpolated into every hook command (plugin.json:18,30,41,52), andCLAUDE_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. - Agent/Task tool dispatch, plus
SendMessageresume and depth/parallel nesting semantics. - 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:
- Every specialist ships as
agents/<name>.md. The body is a complete role prompt; the frontmatter is host metadata. - A host that resolves Team agents by name (Claude Code) dispatches the named agent, so the host applies the frontmatter’s tool and permission restrictions.
- Any other host reads the definition, strips frontmatter, and spawns a fresh generic subagent with the body as its role instructions. No host-side agent registration is required.
- A producer may run inline only when no subagent facility exists; a reviewer never runs inline, because a reviewer sharing the author’s context cannot judge it.
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
- Runtime vs. development split (
CLAUDE.md,docs/architecture.md). Only the distributed set ports:agents/,skills/*/SKILL.md+registry.json+ the bundled skill scripts (supports-nesting.mjs,ste-lint.mjs,external-review.mjs,resolve-transcript.mjs,write-target.mjs,discover-topic.sh, andpr-screenshots’scripts/splice.mjsplus itsscripts/*.sh),hooks/*.mjs,.claude-plugin/. The entire.claude/tree,docs/,.github/never ship and are out of every port’s scope. - A bundled skill script names its own directory, never a host variable.
${CLAUDE_PLUGIN_ROOT}exists on Claude Code alone, so a SKILL.md that interpolates it into a runnable command breaks on Codex, where the value is empty and the path resolves to/skills/.... Document the command with a<skill-dir>placeholder the caller substitutes, the pattern Codex’s own bundled skills use, and keep the script free of relative imports and of environment reads that resolve its own location, so it runs from any install path (ste-lint.mjsdoes both). Reading the environment for something other than the script’s own path is fine and sometimes required — the host a session is running on is knowable no other way, which is howresolve-transcript.mjstells a Claude Code session from a Codex one.skills/team/references/agent-dispatch.mdstill interpolates the variable directly. That command is Claude-Code-specific, but the pipeline it serves is not: nested dispatch degrades to its documented inline fallback on every other host (skills/team/references/agent-dispatch.md, “Optimization, never a dependency”). - Hooks already isolate portable logic from host contract. Each
.mjsreads stdin, does Node-only work, then writes a host-shaped JSON result (session-start-recover.mjs:236-244,post-write-validate.mjs:29-37). The scan and git logic is the reusable core. Only the stdin field names and the result envelope are the binding. The shim layer mirrors this seam. - Agent definition format is already near-universal. Claude
agents/*.md, which is Markdown with YAML frontmatter, carries the same system-prompt body Codex reads through its TOML agent roles. The body ports. The frontmatter and TOML binding does not. - The JSON-envelope convention is host-agnostic by construction
(
skills/agent-open-questions/SKILL.md). It layers on whatever result channel the host gives: final-text on Claude,--output-schemaon Codex.
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.yaml — documented 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:
-
Codex does not expose MCP prompts as slash commands (hard gap). Codex MCP supports tools and resources (
read_mcp_resourceandlist_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). -
Codex lists every skill in the
$picker, souser-invocable: falseis a Claude-Code-only guarantee (resolved by the playbook refactor). Before the refactor, Team’s registered methodology andprinciple-*skills were reference material an agent loaded, never something a human ran. On Claude Code,user-invocable: falsekept 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 ofskills/into ordinary references and playbooks read by path, so no methodology registration remains for Codex to list. The principle tier is returning as guardeddisable-model-invocationskills, kept out of Codex’s implicit catalog byallow_implicit_invocation: false; the$picker shows the 27 commands. See the divergence note for the historical evidence. -
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
-
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.
-
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.
-
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.
-
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.
-
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.
-
Model configuration is limited to model selection. Optional
.team/config.jsonoverrides 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
- Bodies port as-is. Agent roles → TOML in
.codex/agents/with the same system-prompt body. - Skills port natively, through Codex’s own plugin install:
.codex-plugin/plugin.jsonplus.agents/plugins/marketplace.jsonmake the checkout installable withcodex plugin add team@team-dev, and Codex reads every skill fromskills/<name>/SKILL.mdunder the installed plugin root. They arrive namespaced asteam:<name>, with description-matched implicit invocation. Each skill also shipsagents/openai.yaml, which names it in the catalog; the guarded skills declarepolicy.allow_implicit_invocation: falsethere to opt out of that matching, which is this host’s documented equivalent ofdisable-model-invocation. - Do not also link the checkout’s
skills/into~/.agents/skills/. That collection link registers every skill a second time, under both roots, and Codex truncates each description to roughly a quarter of its length to fit the doubled catalog. Verified on codex-cli 0.153.4, 2026-09-09. - The install is a copy, and the manifest version is its cache key. Codex
copies the marketplace root to
~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/, so an edited skill does not reach it until the plugin is installed again. Codex’splugin-creatorreference prescribes a<base>+codex.<cachebuster>version suffix to force the re-copy rather than a version bump;script/dev-install-codexstamps one, reinstalls, restores the manifest, and prunes the previous copy. - Claude Code installs the same way, and takes the same loop.
claude plugin installcopies the marketplace root to~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/, andclaude plugin updatereports “already at the latest version” and copies nothing when the version has not moved, soscript/dev-install-claudestamps<base>+claude.<cachebuster>onto.claude-plugin/plugin.jsonand both version strings in.claude-plugin/marketplace.json— the catalog is a snapshot taken when the marketplace is added or updated, so the stamp has to land before the refresh. Two details differ from Codex: the cache directory is named with+rewritten to-while the reported version keeps the+, so the served path comes from whatclaude plugin listreports rather than from the version string; and there is no legacy collection link to migrate. Verified on 2026-09-10. - Do not replace the cached copy with a symlink to the checkout. It makes edits take effect without a reinstall, and it decouples the served content from the version Claude reports: the directory name is fixed at install time while the contents follow the checkout, so a branch that bumps the version runs the new code under the old number (#355).
- Codex’s plugin validator rejects
disable-model-invocation.plugin-creator’svalidate_plugin.pyrequires the key to be absent orfalse, and Team’s five guarded skills set ittruebecause Claude Code needs it. The runtime does not enforce the rule, and this host’s own equivalent —policy.allow_implicit_invocation: falsein each skill’sagents/openai.yaml— keeps all five out of the implicit catalog. The divergence is deliberate and the validator finding is expected. - Codex ignores
user-invocable: falsefor the retired methodology registrations. The playbook refactor removed those registrations, so only the 27 entry commands remain in the picker. Principles are guardeddisable-model-invocationskills read by installed path, so they add no implicit-invocation entries. Historical probe evidence: the$picker was fed by theskills/listapp-server method, which returned all 100 Team skills withenabled: true,team:principle-fix-root-causesamong them. ItsSkillMetadatapayload carries nine fields —dependencies,description,enabled,interface,name,path,pluginId,scope,shortDescription— and none of them expresses invocability. The stringsuser-invocableanddisable-model-invocationdo appear in the Codex binary, but only inside its embedded skill-authoring prompt andplugin-creator’s Python validator, never in the Rust loader. The three things that look like levers are not:policy.allow_implicit_invocation: falsegoverns model-context injection only. Codex’s own docs for the field say the skill “can still be invoked explicitly via$skill”.- Deleting a skill’s
agents/openai.yamldrops its display name, blurb, and default prompt, but the row stays: with the file moved aside,skills/liststill returned the skill, falling back to the SKILL.mddescription. - Nesting a skill deeper to hide it from discovery fails on both hosts. A
probe at
skills/probe-nest/inner-probe/SKILL.mdwas invisible to Codex’sskills/listand toclaude plugin details, while its flat sibling appeared in both. Each host walks exactly one level.
Verified on codex-cli 0.153.4, 2026-09-10. The only real fix is a Codex feature (honoring
user-invocable, or apolicy.allow_explicit_invocation). - Hooks: three host-native duplicates (
hooks/codex/session-start-recover.mjs,hooks/codex/pre-compact-anchor.mjs,hooks/codex/post-write-validate.mjs) plus the canonicalhooks/validate-team-config.mjsreused unchanged. No shim: Codex’sHooksFileenvelope andapply_patchstdin differ enough that a readable duplicate beats schema translation. Codex blocks with exit 2.PLUGIN_ROOT/PLUGIN_DATA(and theirCLAUDE_aliases) are injected into a plugin hook command’s environment, though no Team hook depends on them. - Slash entry points → Codex Skills, not MCP (gap 1).
- Env: resolve through
.codex/trust + config.toml. - Models: use the installed resolver and optional
.team/config.jsonoverrides. Apply its explicit model and effort to fresh children; verify runtime metadata. Parallelism stays in native host configuration. - Known hazards to track:
- codex#15250 (open). Custom agents are not always reachable from tool-backed sessions. Full subagent parity must verify dispatch works in Team’s tool-heavy flows and track the issue.
- codex#15451 (fixed April
2026). Silent
--output-schemadrop under active tools, resolved upstream (full detail in the risk register). Guard on a pre-fix Codex pin: validate output shape, fall back to text-envelope parse. - MCP prompts are a hard gap (tools and resources are native), so keep all prompt/slash workflows on Skills.
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.
plugin.jsonhas to sit at the plugin root, andskills/,agents/,commands/,mcpServers/, andhooks/all resolve beside that manifest. So.claude-plugin/and.codex-plugin/both validate and then discover nothing, and a manifest cannot redirect its component paths. That is why Team’s Antigravity manifest is at the repo root rather than in a directory beside the other two.- Given a root manifest,
agyprocessed every skill and all 13 agents. - Skill discovery descends the tree but stops at any directory that owns a
SKILL.md, so a skill cannot nest another skill. - A symlinked skill folder is followed at plugin scope and at global scope.
- A symlinked plugin root is discovered with no registration step. A single
link at
~/.gemini/config/plugins/teampointing at a checkout put 52 Team skills in the agent’s own skill list, with no entry inimport_manifest.json(a probe from the 54-skill era; the tree has grown since). Team’s dev install is that one link. A directory holding a hand-writtenplugin.jsonbeside symlinkedskills/andagents/works too; linking the root is preferred because the checkout already carries the manifest, so nothing has to be generated or kept in sync. hooks/is not discovered: this host registers hooks through a roothooks.json.agents/is discovered, and discovery is not dispatch — whetheragycan dispatch an agent, and whether a structured return survives, has not been tested, which is why Team claims no pipeline support here.- Only
PreInvocationhas a Team binding. It cannot block, so the.team/config.jsonguard injects an ephemeral message and the prompt proceeds. There is noSessionStart,PreCompact, or blockingPostToolUse, so three of the four hooks are named gaps in hooks-portability.md.
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.
- The global plugin root is
~/.gemini/config/plugins/. The global skill scope is~/.gemini/config/skills/; Team writes to neither by hand except through its dev install, which owns one directory under the former.~/.gemini/is this host’s own config root, so that spelling is the host’s fact, not a leftover to rename. - The catalog name comes from a skill’s frontmatter
name:, never from the directory or link name. A link namedteam-probe-gitcommitwas reported asgit-commit. So this host applies noteam:prefix and gives Team no namespace at the skill level, unlike Codex — even though the files themselves sit namespaced under a plugin directory. - The
~/.agents/skills/directory is invisible toagy. About fifty unrelated skills sat there on the probe machine and none appeared in the agent’s list. - From inside a Team checkout,
agy plugin listprinted “No imported plugins.” and only the two built-in skills reached the agent’s list. Plugin discovery keys on aplugin.jsonmarker, so Codex’s.agents/plugins/marketplace.jsonis not read as a workspace plugin and no collision exists between the two.
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
- Writing any of the port code. #56 and #57 own the implementation. This is the study they build against.
- Building the shim generator and build tooling. The epic chooses if it generates the shims or hand-writes them (decision 1 permits both).
- Porting the dev-only tree (
.claude/,docs/,.github/), which is never distributed and never ported. - Adopting MCP as a transport. Documented as fallback only (decision 4).
- Reduced-MVP parity. Explicitly rejected: full parity is the target.
- Full OpenCode parity. Its native installation/discovery adapter is covered separately above; the pipeline portability matrix does not certify OpenCode execution, agent permissions, or hooks.
- Guaranteeing host API stability. The young-API recency risk is surfaced and assigned to the shim layer plus version pinning, not eliminated.
Edge cases
These are the boundary conditions the strategy and the downstream epic must handle.
- Boundary: zero portable change in a body. A host with an identical body format needs no transform. The shim is pure binding. The strategy must not force a transpile pass where copy suffices.
- Boundary: a primitive with no host facility at all. The one hard gap (Codex MCP prompts) has an explicit documented detour. Any new primitive Team adds must be matrix-checked before assuming it ports.
- Invalid: host manifest schema drift. A host changes its hook stdin schema.
Chosen behavior: the schema adapter lives in the shim only. The
.mjscore is untouched. This is the central reason for the hybrid boundary. - Failure: Codex pre-April-2026 silent schema drop. On a Codex build before
the codex#15451 fix,
--output-schemais ignored under active tools. Behavior: validate the returned shape and fall back to text-envelope parsing. It is fixed on current Codex, so this is a version-pin caveat. - Concurrency: nested-subagent depth mismatch. Codex (
max_depth=1) cannot match Claude’s depth-2. Behavior: the orchestrator flattens or sequences nested work per host. This is a parity item rather than a silent capability drop. - Authorization: Codex
.codex/trust gate. Project-local hooks and agents load only when the directory is trusted. Behavior: the port’s install docs must state the trust requirement. An untrusted directory skips hooks without warning. Skills bypass the gate entirely. Codex gates “project-local config, hooks, and exec policies” (config/src/loader/mod.rs:912) and skills are absent from that set, so a user-scope skill install is exposed to every session with no prompt. - Resource limit: Codex
agents.max_threads=6. Team’s 5-reviewer parallel dispatch must fit the host’s thread ceiling. Behavior: cap or batch reviewer dispatch per host.
Open questions (deferred to the port epic)
- Shim generation vs. hand-authoring. Decision 1 permits both. Which to use is a structure-phase choice for #57.
- Host version pinning policy. Which exact Codex version the port certifies against (recency risk) is an implementation detail for the port epic.
- Posture on the one open host issue (codex#15250). The port epic makes the maintenance-posture call: upstream a fix, or only design around it. The other cited issue, codex#15451, is already resolved upstream.
- Model assignment quality. The mappings preserve distinct selections. Per-role quality and cost evaluation still determine the best host assignments.
Risks
- Young-host-API risk (moderate). Codex’s hooks and multi-agent rolled out March to May 2026 (v0.114-v0.129, latest v0.142.3) and its contracts may still move. Mitigation: bindings isolated in shims, pin host versions, re-validate on upgrade. (Capabilities verified against the host repos 2026-06-27. Issue statuses 2026-06-25.)
- codex#15250 (open, moderate). Custom agents are not always reachable from tool sessions, which hits Team’s tool-heavy dispatch directly. It is the one live host bug, tracked for #57.
- codex#15451 (fixed April 2026,
low). Silent
--output-schemadrop under active tools, resolved upstream. It is a risk only on a pre-fix Codex pin, covered by shape validation plus a text fallback. - Hidden Claude Code assumptions (low to moderate). Some agent prose may assume Claude-specific tool names or behaviors that the layer analysis did not catch. The port epic should audit bodies for host-specific references during structure.
See also
- Architecture: the full plugin design these primitives are drawn from.
- #50: the source issue this study delivers.
- #57 Codex port: the epic that executes this matrix for Codex CLI.
- #56 Antigravity backend: the epic that runs the pipeline against the Antigravity CLI as an alternate model backend (formerly the Gemini CLI port).
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.