integrations/codex/plugins/swarm/README.md

swarm — Codex CLI plugin

Lifecycle bridge between Codex CLI sessions and the swarm-mcp coordinator, mirroring the hermes plugin and the Claude Code plugin inside the constraints of the Codex plugin model.

This is the behavioural layer that complements the other artifacts in this repo:

  • MCP server (src/index.ts) — gives the agent the swarm tools.
  • Skill (skills/swarm-mcp/) — gives the agent the role doctrine.
  • Plugin (this directory) — eliminates lifecycle boilerplate the agent should not have to remember.

For the broader adapter contract, see docs/control-plane.md. For design parallels, see integrations/hermes/SPEC.md and integrations/claude-code/SPEC.md. Backend selection and workspace identity conventions are centralized in docs/backend-configuration.md.

What it does (v0.2.0)

ResponsibilityMechanism
Auto-register on session startSessionStart hook → swarm-mcp register, stores instance_id in hook scratch metadata
Auto-deregister on session endStop hook → swarm-mcp deregister
Enforce peer-declared locks on apply_patchPreToolUse (matcher: apply_patch) → parses the patch envelope, reads swarm-mcp locks --json, emits permissionDecision: deny when a peer holds any of the patch's files. Never acquires.
Publish and cleanup workspace identitySessionStart / Stop hooks → publish/delete current workspace handle when HERDR_PANE_ID is present
Publish configured work trackerSessionStart hook reads tracker config and writes config/work_tracker/<identity> KV
Gateway conductor modeSWARM_CODEX_ROLE=gateway registers as role:planner; make easy edits locally, use the MCP dispatch tool for medium/large task/spawn routing
Gateway SOUL primingSessionStart appends this repo's SOUL.md for gateway/lead sessions
/swarm slash command (status / instances / tasks / kv / messages)Markdown command shelling to the swarm-mcp CLI

Worker-mode coordination failures are swallowed — coordination is opt-in convenience for ordinary sessions, never critical path. Gateway mode can handle trivial, low-risk edits locally, but medium or large implementation work should create/reuse a swarm task and route it through the MCP dispatch tool, not native subagents. The pre-tool hook is check-only: it inspects existing locks and denies on peer-held conflicts, but never acquires on the agent's behalf. Agents declare wider critical sections themselves via lock_file when they want peers to wait.

Codex specifics

Codex's only file-write tool is apply_patch, whose tool input is a *** Begin Patch ... *** End Patch envelope rather than a JSON file_path. The plugin parses the envelope to recover affected paths from *** Update File:, *** Add File:, *** Delete File:, and *** Move File: directives, then checks each one against the active swarm locks. exec_command and write_stdin are not checked — shell-mediated writes are out of scope for v0.1.

Install

codex plugin marketplace add accepts a local marketplace root. This repo's root is the marketplace root via .agents/plugins/marketplace.json, which exposes the swarm plugin from integrations/codex/plugins/swarm/. That matches the Claude Code setup: install the repo-level marketplace once, then let the host load the plugin and its hooks from the repo checkout.

# personal profile
CODEX_HOME=~/.codex-personal codex plugin marketplace add \
    /Users/james.volpe/volpestyle/swarm-mcp

# or work profile
codex plugin marketplace add \
    /Users/james.volpe/volpestyle/swarm-mcp

Plugin hooks must be enabled for the lifecycle bridge to run:

CODEX_HOME=~/.codex-personal codex features enable hooks
CODEX_HOME=~/.codex-personal codex features enable plugin_hooks

Restart codex so it picks up the plugin's hooks.json and commands/. On the first launch after installing or changing hooks, review and approve the new hook entries in /hooks; until they are approved, Codex will not run the SessionStart auto-registration hook.

plugin_hooks is under-development — install the GA-hooks fallback

Codex flags plugin_hooks as an under-development feature, and in observed sessions its SessionStart dispatch silently no-ops, leaving the agent unregistered with no error visible to the operator. The symptom is that swarm-mcp instances does not show the session even though the plugin is installed, trusted, and version-pinned in the cache.

Until plugin_hooks stabilizes, mirror the same session_start.py invocation from the GA hooks surface in your per-identity ~/.codex*/hooks.json. That file is read directly by Codex without going through the plugin_hooks dispatcher.

Since the file is user-local, point straight at the repo checkout — there is no need to repeat the plugin's cache-discovery dance (the plugin only needs that because Codex doesn't expose a plugin-root env var to hook subprocesses):

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/swarm-mcp/integrations/codex/plugins/swarm/hooks/session_start.py",
            "statusMessage": "Registering with swarm-mcp..."
          }
        ]
      }
    ]
  }
}

Codex will surface an approval prompt on next launch because the hook hash changed; accept it once. The script no-ops cleanly if SWARM_MCP_BIN / swarm-mcp is unreachable — coordination is fail-open for workers.

Once Codex's plugin_hooks SessionStart dispatch is observed to fire reliably, remove the fallback to avoid double-registration. There is no idempotency guard between the two surfaces today.

Codex runs hook commands from the session cwd, not the plugin root, and does not currently expose a plugin-root environment variable to hook subprocesses. For that reason, this plugin's hook commands locate the installed plugin under CODEX_HOME / ~/.codex* before executing the Python hook scripts.

For local plugin development, Codex discovers installed plugin versions from real cache directories. If you want repo edits to flow into the installed plugin without reinstalling, keep the version directory real and symlink the plugin contents inside it, for example:

mkdir -p ~/.codex-personal/plugins/cache/swarm-mcp/swarm/0.2.0/.codex-plugin
ln -s /Users/james.volpe/volpestyle/swarm-mcp/integrations/codex/plugins/swarm/.codex-plugin/plugin.json \
  ~/.codex-personal/plugins/cache/swarm-mcp/swarm/0.2.0/.codex-plugin/plugin.json
ln -s /Users/james.volpe/volpestyle/swarm-mcp/integrations/codex/plugins/swarm/hooks.json \
  ~/.codex-personal/plugins/cache/swarm-mcp/swarm/0.2.0/hooks.json
ln -s /Users/james.volpe/volpestyle/swarm-mcp/integrations/codex/plugins/swarm/hooks \
  ~/.codex-personal/plugins/cache/swarm-mcp/swarm/0.2.0/hooks
ln -s /Users/james.volpe/volpestyle/swarm-mcp/integrations/codex/plugins/swarm/commands \
  ~/.codex-personal/plugins/cache/swarm-mcp/swarm/0.2.0/commands

Do not leave backup version directories such as 0.2.0.cached under the same plugin cache path; Codex may select them instead of the intended version.

Make sure the swarm MCP server is mounted

This plugin expects the swarm MCP server to already be available inside the session — it does not bundle a .mcp.json. The simplest path is to add it once to your codex config:

# ~/.codex-personal/config.toml
[mcp_servers.swarm]
command = "bun"
args = ["run", "/Users/james.volpe/volpestyle/swarm-mcp/src/index.ts"]

For global installs and non-codex hosts, see docs/install-skill.md.

CLI resolution

The hooks shell to the swarm-mcp CLI without relying on shell aliases (codex hooks run as direct subprocesses):

  1. SWARM_MCP_BIN as a real command — e.g.
    export SWARM_MCP_BIN='bun run /path/to/swarm-mcp/src/cli.ts'
    
  2. swarm-mcp on $PATH.
  3. The repo checkout's src/cli.ts under bun, then dist/cli.js under node.

Do not use a shell alias for SWARM_MCP_BIN; subprocesses do not expand aliases.

Identity, label, scope, role

Hooks pick up the same env knobs as the hermes / Claude Code plugins, with SWARM_CODEX_* taking priority for codex-specific overrides:

VariablePurpose
SWARM_CODEX_IDENTITY / AGENT_IDENTITY / SWARM_IDENTITYAuto-derives the identity:<work|personal> label token.
SWARM_CODEX_LABEL / SWARM_HERMES_LABELOverride the full label. If it omits identity:, the derived token is prepended.
SWARM_CODEX_SCOPE / SWARM_HERMES_SCOPE / SWARM_MCP_SCOPEOverride the coordination scope. Default: git root of cwd.
SWARM_CODEX_FILE_ROOT / SWARM_HERMES_FILE_ROOT / SWARM_MCP_FILE_ROOTOverride the file root passed to register.
SWARM_CODEX_AGENT_ROLE / SWARM_AGENT_ROLEAdds a role:<name> token to the derived label. Accepts planner, implementer, reviewer, researcher, generalist, or worker (the default; emits no token).
SWARM_CODEX_ROLE / SWARM_ROLEworker by default. Set gateway for planner/conductor behavior.
SWARM_CODEX_LEASE_SECONDSCLI registration lease for hook-managed sessions. Defaults to 86400; Stop deregisters normally.
SWARM_CODEX_WORK_TRACKER / SWARM_WORK_TRACKERJSON tracker config to publish at config/work_tracker/<identity>; use this for Linear/Jira/GitHub policy, not credentials.
HERDR_PANE_ID, HERDR_SOCKET_PATH, HERDR_WORKSPACE_IDWhen present, SessionStart publishes workspace identity for peer wakes and reports pane.report_agent state=idle; Stop releases that herdr agent authority. Missing env/socket failures fall back to herdr heuristics. See backend-configuration.md.

Repo-wide role default — .swarm-role file. If SWARM_CODEX_AGENT_ROLE is unset, the hook walks up from cwd to the coordination scope looking for a .swarm-role file. The first non-blank, non-comment line is read as the role token. Drop one at the repo root to make every codex session in that workspace register as e.g. implementer without env-var ceremony:

echo implementer > .swarm-role

Resolution order: SWARM_CODEX_AGENT_ROLE.swarm-role file → SWARM_AGENT_ROLErole:planner when SWARM_CODEX_ROLE=gateway. The literal value worker explicitly suppresses the role token.

If no source supplies a role, the SessionStart context appends a one-line nudge to the agent telling it how to set the token before its first peer interaction. Skill doctrine in skills/swarm-mcp/SKILL.md is the source of truth for which role to pick.

Default label format: identity:<id> codex platform:cli [mode:gateway] [role:<name>] origin:codex session:<id-prefix>. Gateway mode adds mode:gateway and defaults the routing role to role:planner.

Verify

In a fresh project with the swarm MCP server mounted:

  1. Start codex (cdx or codex). The first turn should include a SessionStart system block saying the session is already registered with an instance_id.
  2. Confirm registration: swarm-mcp instances from another terminal should show your codex session.
  3. With a second peer registered in the same scope, ask the agent to apply a patch on a file the peer has locked (swarm-mcp lock <file> --note "..." from peer terminal). The apply_patch tool should be denied with swarm lock blocked apply_patch for <file>: held by <8-char-prefix> (...).
  4. Run /swarm inside the session — should print a compact status summary.

If the deny message never appears, the most common causes are:

  • The agent never registered (skill not loaded, or register was skipped). Check swarm-mcp instances.
  • swarm-mcp CLI is not resolvable from the hook subprocess. Set SWARM_MCP_BIN to a real command.
  • The peer is not actually holding a lock, or held it on a different path than the agent is editing. swarm-mcp locks should list it.
  • The hook's session has no cached instance_id (registration failed earlier in the session), so it fails open and can't tell own vs peer.

Roadmap

v0.1 — Lifecycle bridge ✓

  • SessionStart additionalContext priming registration with derived args
  • Pre-tool peer-lock check with deny-on-conflict, fail-open elsewhere
  • /swarm slash command
  • Best-effort identity KV cleanup on Stop

v0.2 — Autonomous lifecycle + gateway mode ✓ (this version)

  • swarm-mcp register / deregister / list-instances
  • SessionStart/Stop hooks call lifecycle commands directly
  • Gateway-mode planner labels, local-small/dispatch-large routing, and MCP dispatch

v0.3 — Verify hook payload contract

  • Empirically confirm codex's PreToolUse / PostToolUse stdin schema and matcher semantics; tighten path extraction once the contract is stable.

v0.4 — Peer prompt express lane

  • Use the adapter-neutral prompt_peer MCP tool or swarm-mcp prompt-peer CLI. No plugin-local tool surface is needed.

v0.5 — Ambient peer context

  • SessionStart additionalContext carries the current peer/lock/message snapshot so the agent starts a turn already aware of the coordination state.

File layout

integrations/_shared/
└── swarm_hook_core.py           -- runtime-agnostic HookCore class (shared with claude-code)

integrations/codex/plugins/swarm/
├── README.md
├── SPEC.md
├── .codex-plugin/
│   └── plugin.json              -- codex plugin manifest
├── hooks.json                   -- hook registration
├── hooks/
│   ├── _common.py               -- codex RuntimeConfig + apply_patch path extractor
│   ├── session_start.py         -- 12-line stub: core.run_session_start_hook
│   ├── session_end.py           -- 12-line stub: core.run_session_end_hook
│   ├── pre_tool_use.py          -- check-only peer-lock inspection (denies on peer-held conflict)
│   └── post_tool_use.py         -- no-op back-compat shim for installs that still wire PostToolUse
└── commands/
    └── swarm.md                 -- /swarm slash command

The hook lifecycle methods (lock-conflict detection, peer scan, identity registration, scratch-dir bookkeeping, herdr identity publication, etc.) live in the shared core. This plugin's _common.py only carries codex-specific bits: the apply_patch envelope parser, the codex label token, the SWARM_CODEX_* env-var prefix, and the swarm-codex scratch namespace.