Claude Skill

agent-relay

Use when you need Codex to coordinate multiple agents through Agent Relay for peer-to-peer messaging, lead/worker handoffs, or shared status tracking across sub-agents and terminals.

LLM Mart · 0 points · 25 views 35 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agentworkforce-relay-plugins_codex-relay-skill-a8d2cab.zip · 14 KB
Part of agentworkforce/relay — 57 skills

Install

skills CLI npx skills add https://github.com/AgentWorkforce/relay/tree/main/plugins/codex-relay-skill
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agentworkforce-relay@llmmart
Git git clone https://github.com/AgentWorkforce/relay.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agentworkforce/relay collection as a plugin from our marketplace. Git is the plain clone.

README

Codex Relay Skill

Codex-native multi-agent coordination via Relaycast.

What it does

This package gives Codex a reusable relay coordination layer so sub-agents can communicate through Relaycast instead of staying limited to parent-only result collection.

It includes:

  • a Codex skill that teaches lead and worker messaging protocol
  • an Agent Relay MCP dependency declaration
  • a template Agent Relay MCP config block for .codex/config.toml
  • a relay-worker custom agent template for .codex/agents/

With these pieces installed, Codex can:

  • coordinate teams through direct messages, channels, and threads
  • require ACK/DONE signaling from workers
  • let workers send peer-to-peer updates through Relaycast
  • reuse the same relay workflow across project-scoped and user-scoped setups

Installation

mkdir -p .agents/skills
cp -R plugins/codex-relay-skill .agents/skills/agent-relay

That's it. Everything else is automatic.

On first use, the skill self-installs by running scripts/setup.sh, which:

  • adds the Agent Relay MCP server to .codex/config.toml
  • enables features.codex_hooks = true
  • writes .codex/hooks.json with SessionStart, UserPromptSubmit, and Stop hooks
  • installs .codex/agents/relay-worker.toml

All of this is idempotent — safe to run multiple times, and it merges with existing config rather than overwriting.

For user-wide availability, install to $HOME/.agents/skills/agent-relay instead.

Optional: join an existing workspace

Set RELAY_API_KEY before launching Codex to join a specific Relaycast workspace:

export RELAY_API_KEY="rk_live_your_key_here"

If unset, a new workspace is auto-created on the first session.

Manual setup (advanced)

If you prefer to configure everything manually instead of using the auto-installer:

  1. Add to .codex/config.toml:
features.codex_hooks = true

[mcp_servers.agent-relay]
command = "npx"
args = ["-y", "agent-relay", "mcp"]
env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://cast.agentrelay.com", RELAY_AGENT_TYPE = "agent" }
  1. Copy hooks config:
cp .agents/skills/agent-relay/hooks/hooks.json .codex/hooks.json
  1. Install the worker agent:
mkdir -p .codex/agents
cp .agents/skills/agent-relay/codex-config/relay-worker.toml .codex/agents/relay-worker.toml

Usage

Use the skill directly

Invoke the skill explicitly:

$agent-relay Coordinate this refactor with two workers and keep all status updates in Relaycast.

Or describe the task naturally and let Codex match the skill from its description.

Spawn relay workers

Once relay-worker.toml is installed, delegate bounded tasks to the relay-worker custom agent and include:

  • the worker relay name
  • the lead relay name
  • the workspace-key source
  • exact task scope
  • completion criteria

Example:

Spawn a relay-worker named api-worker.
Have it check Relaycast, ACK me, update the API route tests only, send STATUS after the first green test run, and send DONE with evidence before exit.

Coordinate a team

Use Relaycast when workers need to message each other directly, not only the lead. Good fits:

  • parallel implementation across separate subsystems
  • lead/worker review loops
  • shared channel updates for longer-running tasks
  • cross-terminal or cross-machine collaboration

Environment variables

Variable Required Default Description
RELAY_API_KEY No "" in the template config Relaycast workspace key
RELAY_BASE_URL No https://cast.agentrelay.com Relaycast API base URL
RELAY_AGENT_TYPE No agent Default Relaycast agent type
RELAY_AGENT_NAME No unset Optional stable relay identity when your workflow wants a fixed name

Plugin structure

codex-relay-skill/
  SKILL.md                    # Codex skill manifest and workflow instructions
  README.md                   # Installation and usage docs
  agents/
    openai.yaml              # Agent Relay MCP dependency metadata
  codex-config/
    config.toml              # Template MCP server config for .codex/config.toml
    relay-worker.toml        # Template custom worker agent for .codex/agents/
  scripts/
    setup.sh                 # Auto-installer (runs on first skill activation)
  hooks/
    hooks.json               # Hook definitions (SessionStart, Stop, UserPromptSubmit)
    session-start.sh         # Auto-connect and state persistence
    stop-inbox.sh            # Block exit while unread messages exist
    prompt-inbox.sh          # Rate-limited inbox polling and context injection

Installed layout in a project typically looks like:

.agents/skills/agent-relay/   # Skill directory Codex scans
.codex/config.toml            # Runtime Agent Relay MCP server + features.codex_hooks
.codex/hooks.json             # Hook wiring (copied from skill)
.codex/agents/relay-worker.toml

Skill manifest

Agent Relay

Use this skill when Codex needs real-time coordination across multiple agents. It gives Codex a repeatable workflow for:

  • connecting to an Agent Relay workspace
  • spawning relay-aware workers
  • sending direct messages, channel updates, and thread replies
  • keeping lead and worker state synchronized through ACK, STATUS, BLOCKED, and DONE signals

Relay fills the peer-to-peer gap in Codex sub-agent workflows. Codex can spawn and collect worker results, but Agent Relay gives those workers a shared message bus so they can talk to the lead and to each other.

Auto-setup

On first activation, this skill auto-configures Codex by running scripts/setup.sh. This adds the Agent Relay MCP server to .codex/config.toml, enables hooks, installs hooks.json, and copies the relay-worker.toml agent definition. No manual setup is required after installing the skill.

Startup protocol

Every relay-connected Codex agent must complete these steps IN ORDER before substantive work:

  1. Set up a workspace.

    • If RELAY_WORKSPACE_KEY is set in the environment, call set_workspace_key with that key.
    • If only the legacy RELAY_API_KEY alias is set, treat it as the same workspace key.
    • If no key is available, call create_workspace to auto-create one. This returns a workspace key — save it for workers.
  2. Register as an agent. Call register_agent with your agent name and type: "agent". Use RELAY_AGENT_NAME from the environment if set, otherwise derive a name from the task context (e.g., lead, auth-worker).

  3. Keep workspace credentials out of output. Never print the workspace key or construct an observer URL from it — it is an administrative credential. When the user asks to follow the conversation, run agent-relay observer and print the URL it returns. That mints a scoped, read-only token (ot_live_...) that expires in 24 hours and excludes agent DMs, so the link is safe to share. Narrow it further with --channels, widen it with --include-dms or --expires, and revoke it with agent-relay observer revoke <id>.

  4. Check the relay inbox. Call check_inbox to see if there are any pending messages or task assignments.

  5. Send an ACK. If you received a task assignment, send ACK: <one-sentence understanding> to your lead via send_dm. If the assignment is unclear, send BLOCKED: <question> instead of guessing.

  6. When the task is complete, send DONE: <summary with evidence> before stopping.

If workspace creation or registration fails, retry once, then report the failure to the user — do not proceed without a relay connection.

Critical rule

Do not assume the current MCP session already has an active Agent Relay workspace. Always call set_workspace_key or create_workspace before registering.

Working rules

  • Include as: "<agent-name>" on relay calls that support explicit attribution.
  • Keep the relay identity stable for the whole task. Do not switch names mid-task.
  • Check the inbox again after meaningful milestones, before long-running work, and before stopping.
  • Prefer direct messages for lead/worker coordination. Use channels only when multiple agents need the same update.
  • Keep status messages short, factual, and scoped to the assigned work.
  • Do not spawn additional relay workers unless the lead explicitly asks for more delegation.
  • If the lead updates the task, follow the newest explicit instruction.

Message templates

  • ACK: I understand the assignment and I am starting work on <scope>.
  • STATUS: Finished <milestone>; next I am doing <next-step>.
  • BLOCKED: I cannot continue because <blocker>.
  • DONE: Completed <scope>. Evidence: <files changed, commands run, tests, or decisions>.

Worker patterns

There are two current ways to involve more agents. Use the right one for the job.

Registered workspace identities

Use register_agent for an agent process that is already running and only needs a Relay identity. Registration does not start a new model runtime.

Lead steps:

  1. Ensure workspace exists (set_workspace_key or create_workspace).
  2. Register the lead (register_agent).
  3. Give the other running process the workspace key and tell it to call register_agent with a stable name.
  4. Send the assignment via send_dm(to: "worker-name", text: "...").
  5. Poll lead inbox for ACK (check_inbox).

Worker steps:

  1. Call set_workspace_key with the shared key.
  2. Register with register_agent.
  3. Check inbox (check_inbox).
  4. Send ACK to lead via send_dm.
  5. Perform the assigned scope.
  6. Send DONE to lead via send_dm.

Relay-spawned workers

Use add_agent when the lead should ask Relay to start a provider-backed worker. The current tool requires name, cli, and task; optional fields include channel, persona, and model.

Lead steps:

  1. Ensure workspace exists and lead is registered.
  2. Spawn the worker with add_agent(name: "worker-name", cli: "codex", task: "...").
  3. Include https://agentrelay.com/skill, the lead name, exact scope, and completion criteria in the task prompt.
  4. Poll lead inbox for ACK (check_inbox).
  5. Release the worker with remove_agent after the work is accepted.

Worker steps:

  1. Follow the using-agent-relay role from https://agentrelay.com/skill.
  2. Check inbox, send ACK, do the assigned work, and send DONE.

Codex sub-agents

If your Codex surface has a sub-agent spawn capability, use the bundled relay-worker agent definition for code-heavy work that needs a separate Codex runtime with file access and tools. Include the workspace key, relay name, lead name, exact scope, and completion criteria in the sub-agent prompt. If that spawn capability is not available, use add_agent instead.

Worker ACK fallback

If a worker does not ACK within 30 seconds:

  1. Check whether the worker appears in list_agents.
  2. If this is a running process, have it call register_agent.
  3. If this should be a spawned worker, call add_agent with name, cli, and task.
  4. Send (or re-send) the assignment via send_dm.
  5. Poll the lead inbox again for ACK.
  6. If still no ACK after a second attempt, report the exact failed step to the user.

Handoff template

Worker: api-worker
Type: relay-spawned worker (use add_agent with name, cli, and task)
Lead: lead
Scope: check the Agent Relay inbox and confirm connectivity
Protocol:
  1. Check inbox
  2. DM lead with ACK
  3. Perform scope
  4. DM lead with DONE

For code-heavy tasks, change the type line to:

Type: Codex sub-agent (use relay-worker if your Codex surface provides sub-agent spawning)
Files (relay)
  • agents
    • openai.yaml 224 B
      dependencies:
        tools:
          - type: 'mcp'
            value: 'agent-relay'
            description: 'Agent Relay real-time messaging for multi-agent coordination'
            transport: 'stdio'
            command: 'agent-relay'
            args: ['mcp']
      
  • codex-config
    • config.toml 358 B
      # Add this block to .codex/config.toml or ~/.codex/config.toml.
      
      # Enable the hooks engine (required for auto-connect, inbox polling, stop guard)
      features.codex_hooks = true
      
      [mcp_servers.agent-relay]
      command = "npx"
      args = ["-y", "agent-relay", "mcp"]
      env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://cast.agentrelay.com", RELAY_AGENT_TYPE = "agent" }
      
    • relay-worker.toml 1.1 KB
      name = "relay-worker"
      description = "Executes relay-coordinated tasks with ACK/DONE signaling"
      developer_instructions = """
      You are a relay-connected Codex worker.
      
      Before doing substantive work:
      1. Set the Relaycast workspace key from your assignment or environment. Do not print the key.
      2. Register with Relaycast using your assigned relay name and type "agent" unless you were explicitly pre-registered.
      3. Check your inbox with the same relay identity to load your task and lead context.
      4. Send ACK to your lead with a one-sentence understanding of the assignment.
      5. If you are blocked or the task is ambiguous, send BLOCKED instead of guessing.
      
      Working rules:
      - Include as: "<agent-name>" on relay calls that support explicit attribution.
      - Stay within the assigned scope and do not spawn more workers unless the lead explicitly directs you to.
      - Check inbox again after meaningful milestones, during long-running work, and before exit.
      - Keep status updates concise and factual.
      
      Completion:
      - Send DONE before exiting.
      - Include evidence when relevant: files changed, commands run, tests executed, or decisions made.
      """
      mcp_servers = ["agent-relay"]
      
  • hooks
    • hooks.json 887 B
      {
        "hooks": {
          "SessionStart": [
            {
              "hooks": [
                {
                  "type": "command",
                  "command": "bash .agents/skills/agent-relay/hooks/session-start.sh",
                  "timeoutSec": 15,
                  "statusMessage": "Connecting Relaycast"
                }
              ]
            }
          ],
          "UserPromptSubmit": [
            {
              "hooks": [
                {
                  "type": "command",
                  "command": "bash .agents/skills/agent-relay/hooks/prompt-inbox.sh",
                  "timeoutSec": 5,
                  "statusMessage": "Checking relay inbox"
                }
              ]
            }
          ],
          "Stop": [
            {
              "hooks": [
                {
                  "type": "command",
                  "command": "bash .agents/skills/agent-relay/hooks/stop-inbox.sh",
                  "timeoutSec": 5,
                  "statusMessage": "Verifying relay inbox"
                }
              ]
            }
          ]
        }
      }
      
    • prompt-inbox.sh 4.1 KB
      #!/usr/bin/env bash
      
      set -euo pipefail
      
      SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
      PLUGIN_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
      ENV_FILE="${PLUGIN_DIR}/.env"
      BASE_RELAY_DIR="${HOME}/.relay"
      # Per-agent namespacing: use RELAY_AGENT_NAME to avoid concurrent agents
      # overwriting each other's state files
      _agent_ns=$(printf '%s' "${RELAY_AGENT_NAME:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64)
      if [ -n "$_agent_ns" ]; then
        RELAY_DIR="${BASE_RELAY_DIR}/agents/${_agent_ns}"
      else
        RELAY_DIR="${BASE_RELAY_DIR}"
      fi
      TOKEN_FILE="${RELAY_DIR}/token"
      STATE_FILE="${RELAY_DIR}/codex-session.json"
      LAST_POLL_FILE="${RELAY_DIR}/last-poll"
      DEFAULT_BASE_URL="https://cast.agentrelay.com"
      EMPTY_OUTPUT='{}'
      MAX_RENDERED_MESSAGES=20
      MIN_POLL_INTERVAL=3
      
      load_env() {
        if [ -f "$ENV_FILE" ]; then
          set -a
          # shellcheck disable=SC1090
          . "$ENV_FILE"
          set +a
        fi
      }
      
      command_exists() {
        command -v "$1" >/dev/null 2>&1
      }
      
      trim() {
        printf '%s' "${1:-}" | awk '{$1=$1;print}'
      }
      
      normalize_base_url() {
        local value
        value=$(trim "${1:-}")
        if [ -z "$value" ] && [ -f "$STATE_FILE" ] && command_exists jq; then
          value=$(jq -r '.baseUrl // empty' "$STATE_FILE" 2>/dev/null || true)
        fi
        value=$(trim "${value:-$DEFAULT_BASE_URL}")
        value=${value%/}
        printf '%s' "${value:-$DEFAULT_BASE_URL}"
      }
      
      read_token() {
        local token
        token=$(trim "${RELAY_TOKEN:-}")
        if [ -n "$token" ]; then
          printf '%s' "$token"
          return
        fi
        if [ -s "$TOKEN_FILE" ]; then
          trim "$(cat "$TOKEN_FILE" 2>/dev/null || true)"
          return
        fi
        printf ''
      }
      
      should_skip_poll() {
        local now last elapsed
        now=$(date +%s)
        if [ -f "$LAST_POLL_FILE" ]; then
          last=$(cat "$LAST_POLL_FILE" 2>/dev/null || printf '0')
          case "$last" in
            ''|*[!0-9]*) last=0 ;;
          esac
          elapsed=$((now - last))
          if [ "$elapsed" -lt "$MIN_POLL_INTERVAL" ]; then
            return 0
          fi
        fi
        mkdir -p "$RELAY_DIR"
        printf '%s\n' "$now" > "$LAST_POLL_FILE"
        return 1
      }
      
      main() {
        local token base_url messages count formatted overflow context
      
        load_env
      
        if ! command_exists curl || ! command_exists jq; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        if should_skip_poll; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        token=$(read_token)
        if [ -z "$token" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        base_url=$(normalize_base_url "${RELAY_BASE_URL:-}")
        messages=$(
          curl -fsS \
            -X POST \
            -H "Authorization: Bearer ${token}" \
            -H 'Content-Type: application/json' \
            -d '{}' \
            "${base_url}/v1/inbox/check" 2>/dev/null || true
        )
      
        if [ -z "$messages" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        count=$(printf '%s' "$messages" | jq -r '(.messages // []) | length' 2>/dev/null || printf '0')
        case "$count" in
          ''|*[!0-9]*) count=0 ;;
        esac
      
        if [ "$count" -eq 0 ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        formatted=$(
          printf '%s' "$messages" | jq -r --argjson limit "$MAX_RENDERED_MESSAGES" '
            (.messages // [])
            | .[:$limit]
            | map(
                if ((.channel // "") | length) > 0 then
                  "Relay message from \(.from // "unknown") in #\(.channel)\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
                else
                  "Relay message from \(.from // "unknown")\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
                end
              )
            | join("\n")
          ' 2>/dev/null || true
        )
      
        if [ -z "$formatted" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        overflow=""
        if [ "$count" -gt "$MAX_RENDERED_MESSAGES" ]; then
          overflow=$(printf '\n... and %s more unread relay message(s).' "$((count - MAX_RENDERED_MESSAGES))")
        fi
      
        context=$(printf 'Relay inbox update (%s unread):\n%s%s\nRead and respond to any messages that affect the current task.' "$count" "$formatted" "$overflow")
        jq -nc \
          --arg context "$context" \
          '{
            hookSpecificOutput: {
              hookEventName: "UserPromptSubmit",
              additionalContext: $context
            }
          }'
      }
      
      main "$@"
      
    • session-start.sh 7.3 KB
      #!/usr/bin/env bash
      
      set -euo pipefail
      
      SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
      PLUGIN_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
      ENV_FILE="${PLUGIN_DIR}/.env"
      BASE_RELAY_DIR="${HOME}/.relay"
      # Per-agent namespacing: use RELAY_AGENT_NAME to avoid concurrent agents
      # overwriting each other's state files
      _agent_ns=$(printf '%s' "${RELAY_AGENT_NAME:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64)
      if [ -n "$_agent_ns" ]; then
        RELAY_DIR="${BASE_RELAY_DIR}/agents/${_agent_ns}"
      else
        RELAY_DIR="${BASE_RELAY_DIR}"
      fi
      KEY_FILE="${RELAY_DIR}/workspace-key"
      TOKEN_FILE="${RELAY_DIR}/token"
      STATE_FILE="${RELAY_DIR}/codex-session.json"
      DEFAULT_BASE_URL="https://cast.agentrelay.com"
      
      load_env() {
        if [ -f "$ENV_FILE" ]; then
          set -a
          # shellcheck disable=SC1090
          . "$ENV_FILE"
          set +a
        fi
      }
      
      run_local_setup() {
        if [ -x "${PLUGIN_DIR}/scripts/setup.sh" ]; then
          "${PLUGIN_DIR}/scripts/setup.sh" "$PWD" >/dev/null 2>&1 || true
        fi
      }
      
      command_exists() {
        command -v "$1" >/dev/null 2>&1
      }
      
      trim() {
        printf '%s' "${1:-}" | awk '{$1=$1;print}'
      }
      
      normalize_base_url() {
        local value
        value=$(trim "${1:-$DEFAULT_BASE_URL}")
        value=${value%/}
        printf '%s' "${value:-$DEFAULT_BASE_URL}"
      }
      
      json_value() {
        local payload="$1"
        local query="$2"
        printf '%s' "$payload" | jq -r "$query // empty" 2>/dev/null || true
      }
      
      sanitize() {
        printf '%s' "${1:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64
      }
      
      derive_workspace_name() {
        local user host suffix
        user=$(sanitize "${USER:-${USERNAME:-codex}}")
        host=$(hostname 2>/dev/null | cut -d '.' -f 1 | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-20)
        suffix=$(date +%s)
        printf 'codex-%s-%s-%s' "${user:-codex}" "${host:-local}" "$suffix" | cut -c1-64
      }
      
      read_existing_agent_name() {
        if [ -f "$STATE_FILE" ] && command_exists jq; then
          jq -r '.agentName // empty' "$STATE_FILE" 2>/dev/null || true
        fi
      }
      
      derive_agent_name() {
        local explicit existing user host suffix
        explicit=$(trim "${RELAY_AGENT_NAME:-}")
        if [ -n "$explicit" ]; then
          sanitize "$explicit"
          return
        fi
      
        existing=$(trim "$(read_existing_agent_name)")
        if [ -n "$existing" ]; then
          sanitize "$existing"
          return
        fi
      
        user=$(sanitize "${USER:-${USERNAME:-codex}}")
        host=$(hostname 2>/dev/null | cut -d '.' -f 1 | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-20)
        suffix=$(date +%s | tail -c 7)
        printf 'codex-%s-%s-%s' "${user:-codex}" "${host:-local}" "$suffix" | cut -c1-64
      }
      
      write_file() {
        local path="$1"
        local content="$2"
        printf '%s' "$content" > "$path"
      }
      
      create_workspace() {
        local base_url="$1"
        local name="$2"
        curl -fsS \
          -X POST \
          -H 'Content-Type: application/json' \
          -d "$(jq -nc --arg name "$name" '{name: $name}')" \
          "${base_url}/v1/workspaces" 2>/dev/null || true
      }
      
      register_v1_agents() {
        local base_url="$1"
        local workspace_key="$2"
        local agent_name="$3"
        curl -fsS \
          -X POST \
          -H "Authorization: Bearer ${workspace_key}" \
          -H 'Content-Type: application/json' \
          -d "$(jq -nc --arg name "$agent_name" '{name: $name, agent_type: "agent", type: "agent"}')" \
          "${base_url}/v1/agents" 2>/dev/null || true
      }
      
      register_v1_register() {
        local base_url="$1"
        local workspace_key="$2"
        local agent_name="$3"
        curl -fsS \
          -X POST \
          -H 'Content-Type: application/json' \
          -d "$(jq -nc --arg workspace "$workspace_key" --arg name "$agent_name" '{workspace: $workspace, name: $name, cli: "codex", type: "agent"}')" \
          "${base_url}/v1/register" 2>/dev/null || true
      }
      
      persist_state() {
        local base_url="$1"
        local workspace_key="$2"
        local workspace_id="$3"
        local agent_id="$4"
        local agent_name="$5"
        local token="$6"
      
        write_file "$KEY_FILE" "$workspace_key"
        chmod 600 "$KEY_FILE"
        write_file "$TOKEN_FILE" "$token"
        chmod 600 "$TOKEN_FILE"
        jq -nc \
          --arg baseUrl "$base_url" \
          --arg workspaceKey "$workspace_key" \
          --arg workspaceId "$workspace_id" \
          --arg agentId "$agent_id" \
          --arg agentName "$agent_name" \
          --arg token "$token" \
          --arg updatedAt "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
          '{
            baseUrl: $baseUrl,
            workspaceKey: $workspaceKey,
            workspaceId: $workspaceId,
            agentId: $agentId,
            agentName: $agentName,
            token: $token,
            cli: "codex",
            updatedAt: $updatedAt
          }' > "$STATE_FILE"
        chmod 600 "$STATE_FILE"
      }
      
      main() {
        run_local_setup
        load_env
      
        if ! command_exists curl || ! command_exists jq; then
          exit 0
        fi
      
        mkdir -p "$RELAY_DIR"
      
        local base_url workspace_key workspace_id agent_name registration token registered_name agent_id
        base_url=$(normalize_base_url "${RELAY_BASE_URL:-}")
        workspace_key=$(trim "${RELAY_API_KEY:-}")
        workspace_id=""
      
        if [ -z "$workspace_key" ] && [ -s "$KEY_FILE" ]; then
          workspace_key=$(trim "$(cat "$KEY_FILE" 2>/dev/null || true)")
        fi
      
        if [ -z "$workspace_key" ]; then
          local created
          created=$(create_workspace "$base_url" "$(derive_workspace_name)")
          workspace_key=$(json_value "$created" '.api_key')
          [ -z "$workspace_key" ] && workspace_key=$(json_value "$created" '.apiKey')
          [ -z "$workspace_key" ] && workspace_key=$(json_value "$created" '.data.api_key')
          [ -z "$workspace_key" ] && workspace_key=$(json_value "$created" '.data.apiKey')
          workspace_id=$(json_value "$created" '.workspace_id')
          [ -z "$workspace_id" ] && workspace_id=$(json_value "$created" '.workspaceId')
          [ -z "$workspace_id" ] && workspace_id=$(json_value "$created" '.data.workspace_id')
          [ -z "$workspace_id" ] && workspace_id=$(json_value "$created" '.data.workspaceId')
        fi
      
        [ -z "$workspace_key" ] && exit 0
      
        agent_name=$(derive_agent_name)
      
        # Re-namespace state directory now that we know the agent name
        RELAY_DIR="${BASE_RELAY_DIR}/agents/${agent_name}"
        KEY_FILE="${RELAY_DIR}/workspace-key"
        TOKEN_FILE="${RELAY_DIR}/token"
        STATE_FILE="${RELAY_DIR}/codex-session.json"
        mkdir -p "$RELAY_DIR"
      
        registration=$(register_v1_agents "$base_url" "$workspace_key" "$agent_name")
        if [ -z "$registration" ]; then
          registration=$(register_v1_register "$base_url" "$workspace_key" "$agent_name")
        fi
        [ -z "$registration" ] && exit 0
      
        token=$(json_value "$registration" '.token')
        [ -z "$token" ] && token=$(json_value "$registration" '.data.token')
        registered_name=$(json_value "$registration" '.name')
        [ -z "$registered_name" ] && registered_name=$(json_value "$registration" '.data.name')
        agent_id=$(json_value "$registration" '.id')
        [ -z "$agent_id" ] && agent_id=$(json_value "$registration" '.agent_id')
        [ -z "$agent_id" ] && agent_id=$(json_value "$registration" '.data.id')
        [ -z "$agent_id" ] && agent_id=$(json_value "$registration" '.data.agent_id')
        [ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.workspace_id')
        [ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.workspaceId')
        [ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.data.workspace_id')
        [ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.data.workspaceId')
      
        [ -z "$token" ] && exit 0
        [ -z "$registered_name" ] && registered_name="$agent_name"
        [ -z "$agent_id" ] && agent_id="$registered_name"
        [ -z "$workspace_id" ] && workspace_id="ws_unknown"
      
        persist_state "$base_url" "$workspace_key" "$workspace_id" "$agent_id" "$registered_name" "$token"
      }
      
      main "$@"
      
    • stop-inbox.sh 3.9 KB
      #!/usr/bin/env bash
      
      set -euo pipefail
      
      SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
      PLUGIN_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
      ENV_FILE="${PLUGIN_DIR}/.env"
      BASE_RELAY_DIR="${HOME}/.relay"
      # Per-agent namespacing: use RELAY_AGENT_NAME to avoid concurrent agents
      # overwriting each other's state files
      _agent_ns=$(printf '%s' "${RELAY_AGENT_NAME:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64)
      if [ -n "$_agent_ns" ]; then
        RELAY_DIR="${BASE_RELAY_DIR}/agents/${_agent_ns}"
      else
        RELAY_DIR="${BASE_RELAY_DIR}"
      fi
      TOKEN_FILE="${RELAY_DIR}/token"
      STATE_FILE="${RELAY_DIR}/codex-session.json"
      DEFAULT_BASE_URL="https://cast.agentrelay.com"
      EMPTY_OUTPUT='{}'
      MAX_RENDERED_MESSAGES=20
      
      load_env() {
        if [ -f "$ENV_FILE" ]; then
          set -a
          # shellcheck disable=SC1090
          . "$ENV_FILE"
          set +a
        fi
      }
      
      command_exists() {
        command -v "$1" >/dev/null 2>&1
      }
      
      trim() {
        printf '%s' "${1:-}" | awk '{$1=$1;print}'
      }
      
      normalize_base_url() {
        local value
        value=$(trim "${1:-}")
        if [ -z "$value" ] && [ -f "$STATE_FILE" ] && command_exists jq; then
          value=$(jq -r '.baseUrl // empty' "$STATE_FILE" 2>/dev/null || true)
        fi
        value=$(trim "${value:-$DEFAULT_BASE_URL}")
        value=${value%/}
        printf '%s' "${value:-$DEFAULT_BASE_URL}"
      }
      
      json_number() {
        local payload="$1"
        local query="$2"
        printf '%s' "$payload" | jq -r "$query" 2>/dev/null || printf '0'
      }
      
      read_stop_hook_active() {
        if ! command_exists jq; then
          printf 'false'
          return
        fi
      
        jq -r '.stop_hook_active // false' 2>/dev/null || printf 'false'
      }
      
      read_token() {
        local token
        token=$(trim "${RELAY_TOKEN:-}")
        if [ -n "$token" ]; then
          printf '%s' "$token"
          return
        fi
        if [ -s "$TOKEN_FILE" ]; then
          trim "$(cat "$TOKEN_FILE" 2>/dev/null || true)"
          return
        fi
        printf ''
      }
      
      main() {
        local input stop_hook_active token base_url messages count formatted overflow reason
      
        load_env
      
        if ! command_exists curl || ! command_exists jq; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        input=$(cat)
        stop_hook_active=$(printf '%s' "$input" | read_stop_hook_active)
        if [ "$stop_hook_active" = "true" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        token=$(read_token)
        if [ -z "$token" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        base_url=$(normalize_base_url "${RELAY_BASE_URL:-}")
        messages=$(
          curl -fsS \
            -X POST \
            -H "Authorization: Bearer ${token}" \
            -H 'Content-Type: application/json' \
            -d '{}' \
            "${base_url}/v1/inbox/check" 2>/dev/null || true
        )
      
        if [ -z "$messages" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        count=$(json_number "$messages" '(.messages // []) | length')
        case "$count" in
          ''|*[!0-9]*) count=0 ;;
        esac
      
        if [ "$count" -eq 0 ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        formatted=$(
          printf '%s' "$messages" | jq -r --argjson limit "$MAX_RENDERED_MESSAGES" '
            (.messages // [])
            | .[:$limit]
            | map(
                if ((.channel // "") | length) > 0 then
                  "Relay message from \(.from // "unknown") in #\(.channel)\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
                else
                  "Relay message from \(.from // "unknown")\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
                end
              )
            | join("\n")
          ' 2>/dev/null || true
        )
      
        if [ -z "$formatted" ]; then
          printf '%s\n' "$EMPTY_OUTPUT"
          exit 0
        fi
      
        overflow=""
        if [ "$count" -gt "$MAX_RENDERED_MESSAGES" ]; then
          overflow=$(printf '\n... and %s more unread relay message(s).' "$((count - MAX_RENDERED_MESSAGES))")
        fi
      
        reason=$(printf 'You have %s unread relay message(s). Please read and respond before stopping:\n%s%s' "$count" "$formatted" "$overflow")
        jq -nc --arg reason "$reason" '{decision: "block", reason: $reason}'
      }
      
      main "$@"
      
  • scripts
    • setup.sh 8.8 KB
      #!/usr/bin/env bash
      
      set -euo pipefail
      
      SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
      SKILL_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
      TARGET_ROOT="${1:-$PWD}"
      CODEX_DIR="${TARGET_ROOT}/.codex"
      AGENTS_DIR="${CODEX_DIR}/agents"
      CONFIG_FILE="${CODEX_DIR}/config.toml"
      HOOKS_FILE="${CODEX_DIR}/hooks.json"
      WORKER_SOURCE="${SKILL_DIR}/codex-config/relay-worker.toml"
      WORKER_TARGET="${AGENTS_DIR}/relay-worker.toml"
      
      command_exists() {
        command -v "$1" >/dev/null 2>&1
      }
      
      shell_quote() {
        printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
      }
      
      write_if_changed() {
        local path="$1"
        local tmp="$2"
      
        if [ -f "$path" ] && cmp -s "$path" "$tmp"; then
          rm -f "$tmp"
          return
        fi
      
        mv "$tmp" "$path"
      }
      
      ensure_features_codex_hooks() {
        local file="$1"
        local tmp
        tmp=$(mktemp)
      
        awk '
          BEGIN {
            in_features = 0
            features_seen = 0
            codex_hooks_written = 0
          }
          function write_codex_hooks() {
            if (!codex_hooks_written) {
              print "codex_hooks = true"
              codex_hooks_written = 1
            }
          }
          {
            if ($0 ~ /^[[:space:]]*features[.]codex_hooks[[:space:]]*=/) {
              print "features.codex_hooks = true"
              codex_hooks_written = 1
              next
            }
      
            if ($0 ~ /^\[[^]]+\][[:space:]]*$/) {
              if (in_features) {
                write_codex_hooks()
              }
              if ($0 == "[features]") {
                in_features = 1
                features_seen = 1
              } else {
                in_features = 0
              }
              print
              next
            }
      
            if (in_features && $0 ~ /^[[:space:]]*codex_hooks[[:space:]]*=/) {
              write_codex_hooks()
              next
            }
      
            print
          }
          END {
            if (in_features) {
              write_codex_hooks()
            }
            if (!features_seen && !codex_hooks_written) {
              if (NR > 0) {
                print ""
              }
              print "[features]"
              print "codex_hooks = true"
            }
          }
        ' "$file" > "$tmp"
      
        write_if_changed "$file" "$tmp"
      }
      
      ensure_top_level_approval_policy() {
        local file="$1"
        local tmp
        tmp=$(mktemp)
      
        awk '
          BEGIN {
            found = 0
            first_section = 0
            inserted = 0
          }
          {
            # If we find approval_policy at top level (before any section or as dotted key), mark found
            if (!first_section && $0 ~ /^[[:space:]]*approval_policy[[:space:]]*=/) {
              found = 1
              print
              next
            }
      
            # Detect first section header
            if ($0 ~ /^\[[^]]+\][[:space:]]*$/) {
              if (!first_section && !found && !inserted) {
                # Insert approval_policy before the first section header
                print "approval_policy = \"on-request\""
                print ""
                inserted = 1
              }
              first_section = 1
            }
      
            print
          }
          END {
            # File has no sections at all
            if (!found && !inserted) {
              if (NR > 0) {
                print ""
              }
              print "approval_policy = \"on-request\""
            }
          }
        ' "$file" > "$tmp"
      
        write_if_changed "$file" "$tmp"
      }
      
      ensure_agent_relay_mcp_block() {
        local file="$1"
        local tmp
        tmp=$(mktemp)
      
        awk '
          BEGIN {
            in_block = 0
            block_seen = 0
            dotted_seen = 0
            command_seen = 0
            args_seen = 0
            env_seen = 0
            command_line = "command = \"npx\""
            args_line = "args = [\"-y\", \"agent-relay\", \"mcp\"]"
            env_line = "env = { RELAY_API_KEY = \"\", RELAY_BASE_URL = \"https://cast.agentrelay.com\", RELAY_AGENT_TYPE = \"agent\" }"
          }
          function write_missing_keys() {
            if (!command_seen) {
              print command_line
              command_seen = 1
            }
            if (!args_seen) {
              print args_line
              args_seen = 1
            }
            if (!env_seen) {
              print env_line
              env_seen = 1
            }
          }
          {
            if (!in_block && $0 ~ /^[[:space:]]*mcp_servers[.]agent-relay[.]command[[:space:]]*=/) {
              block_seen = 1
              dotted_seen = 1
              command_seen = 1
              command_line = $0
              sub(/^[[:space:]]*mcp_servers[.]agent-relay[.]command[[:space:]]*=/, "command =", command_line)
              print
              next
            }
            if (!in_block && $0 ~ /^[[:space:]]*mcp_servers[.]agent-relay[.]args[[:space:]]*=/) {
              block_seen = 1
              dotted_seen = 1
              args_seen = 1
              args_line = $0
              sub(/^[[:space:]]*mcp_servers[.]agent-relay[.]args[[:space:]]*=/, "args =", args_line)
              print
              next
            }
            if (!in_block && $0 ~ /^[[:space:]]*mcp_servers[.]agent-relay[.]env[[:space:]]*=/) {
              block_seen = 1
              dotted_seen = 1
              env_seen = 1
              env_line = $0
              sub(/^[[:space:]]*mcp_servers[.]agent-relay[.]env[[:space:]]*=/, "env =", env_line)
              print
              next
            }
      
            if ($0 ~ /^\[[^]]+\][[:space:]]*$/) {
              if (in_block) {
                write_missing_keys()
              }
              if ($0 == "[mcp_servers.agent-relay]") {
                in_block = 1
                block_seen = 1
              } else {
                in_block = 0
              }
              print
              next
            }
      
            if (in_block) {
              if ($0 ~ /^[[:space:]]*command[[:space:]]*=/) {
                command_seen = 1
                print
                next
              }
              if ($0 ~ /^[[:space:]]*args[[:space:]]*=/) {
                args_seen = 1
                print
                next
              }
              if ($0 ~ /^[[:space:]]*env[[:space:]]*=/) {
                env_seen = 1
                print
                next
              }
            }
      
            print
          }
          END {
            if (in_block) {
              write_missing_keys()
            }
            if (!block_seen && !dotted_seen) {
              if (NR > 0) {
                print ""
              }
              print "[mcp_servers.agent-relay]"
              print command_line
              print args_line
              print env_line
            }
          }
        ' "$file" > "$tmp"
      
        write_if_changed "$file" "$tmp"
      }
      
      desired_hooks_json() {
        local session_cmd prompt_cmd stop_cmd
        session_cmd="bash $(shell_quote "${SKILL_DIR}/hooks/session-start.sh")"
        prompt_cmd="bash $(shell_quote "${SKILL_DIR}/hooks/prompt-inbox.sh")"
        stop_cmd="bash $(shell_quote "${SKILL_DIR}/hooks/stop-inbox.sh")"
      
        jq -n \
          --arg session_cmd "$session_cmd" \
          --arg prompt_cmd "$prompt_cmd" \
          --arg stop_cmd "$stop_cmd" \
          '{
            hooks: {
              SessionStart: [
                {
                  hooks: [
                    {
                      type: "command",
                      command: $session_cmd,
                      timeoutSec: 15,
                      statusMessage: "Connecting Relaycast"
                    }
                  ]
                }
              ],
              UserPromptSubmit: [
                {
                  hooks: [
                    {
                      type: "command",
                      command: $prompt_cmd,
                      timeoutSec: 5,
                      statusMessage: "Checking relay inbox"
                    }
                  ]
                }
              ],
              Stop: [
                {
                  hooks: [
                    {
                      type: "command",
                      command: $stop_cmd,
                      timeoutSec: 5,
                      statusMessage: "Verifying relay inbox"
                    }
                  ]
                }
              ]
            }
          }'
      }
      
      merge_hooks_file() {
        local desired_json="$1"
        local tmp
        tmp=$(mktemp)
      
        if [ -f "$HOOKS_FILE" ] && jq empty "$HOOKS_FILE" >/dev/null 2>&1; then
          jq \
            --argjson desired "$desired_json" \
            '
              def remove_owned_groups($event; $status; $script):
                .hooks[$event] = (
                  (.hooks[$event] // [])
                  | map(
                      select(
                        (
                          (.hooks // [])
                          | any(
                              ((.statusMessage // "") == $status) or
                              (((.command // "") | tostring) | contains($script))
                            )
                        ) | not
                      )
                    )
                );
      
              .hooks = (.hooks // {})
              | remove_owned_groups("SessionStart"; "Connecting Relaycast"; "session-start.sh")
              | remove_owned_groups("UserPromptSubmit"; "Checking relay inbox"; "prompt-inbox.sh")
              | remove_owned_groups("Stop"; "Verifying relay inbox"; "stop-inbox.sh")
              | .hooks.SessionStart = ((.hooks.SessionStart // []) + $desired.hooks.SessionStart)
              | .hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) + $desired.hooks.UserPromptSubmit)
              | .hooks.Stop = ((.hooks.Stop // []) + $desired.hooks.Stop)
            ' "$HOOKS_FILE" > "$tmp"
        else
          printf '%s\n' "$desired_json" > "$tmp"
        fi
      
        write_if_changed "$HOOKS_FILE" "$tmp"
      }
      
      install_worker_agent() {
        if [ ! -f "$WORKER_SOURCE" ]; then
          return
        fi
      
        local tmp
        tmp=$(mktemp)
        cp "$WORKER_SOURCE" "$tmp"
        write_if_changed "$WORKER_TARGET" "$tmp"
      }
      
      main() {
        if ! command_exists jq; then
          exit 0
        fi
      
        mkdir -p "$CODEX_DIR" "$AGENTS_DIR"
        touch "$CONFIG_FILE"
      
        chmod +x "${SKILL_DIR}/scripts/setup.sh" "${SKILL_DIR}/hooks/"*.sh 2>/dev/null || true
      
        ensure_features_codex_hooks "$CONFIG_FILE"
        ensure_top_level_approval_policy "$CONFIG_FILE"
        ensure_agent_relay_mcp_block "$CONFIG_FILE"
        merge_hooks_file "$(desired_hooks_json)"
        install_worker_agent
      }
      
      main "$@"
      
  • README.md 5.3 KB
    # Codex Relay Skill
    
    Codex-native multi-agent coordination via Relaycast.
    
    ## What it does
    
    This package gives Codex a reusable relay coordination layer so sub-agents can communicate through Relaycast instead of staying limited to parent-only result collection.
    
    It includes:
    
    - a Codex skill that teaches lead and worker messaging protocol
    - an Agent Relay MCP dependency declaration
    - a template Agent Relay MCP config block for `.codex/config.toml`
    - a `relay-worker` custom agent template for `.codex/agents/`
    
    With these pieces installed, Codex can:
    
    - coordinate teams through direct messages, channels, and threads
    - require ACK/DONE signaling from workers
    - let workers send peer-to-peer updates through Relaycast
    - reuse the same relay workflow across project-scoped and user-scoped setups
    
    ## Installation
    
    ```bash
    mkdir -p .agents/skills
    cp -R plugins/codex-relay-skill .agents/skills/agent-relay
    ```
    
    That's it. Everything else is automatic.
    
    On first use, the skill self-installs by running `scripts/setup.sh`, which:
    
    - adds the Agent Relay MCP server to `.codex/config.toml`
    - enables `features.codex_hooks = true`
    - writes `.codex/hooks.json` with SessionStart, UserPromptSubmit, and Stop hooks
    - installs `.codex/agents/relay-worker.toml`
    
    All of this is idempotent — safe to run multiple times, and it merges with existing config rather than overwriting.
    
    For user-wide availability, install to `$HOME/.agents/skills/agent-relay` instead.
    
    ### Optional: join an existing workspace
    
    Set `RELAY_API_KEY` before launching Codex to join a specific Relaycast workspace:
    
    ```bash
    export RELAY_API_KEY="rk_live_your_key_here"
    ```
    
    If unset, a new workspace is auto-created on the first session.
    
    <details>
    <summary>Manual setup (advanced)</summary>
    
    If you prefer to configure everything manually instead of using the auto-installer:
    
    1. Add to `.codex/config.toml`:
    
    ```toml
    features.codex_hooks = true
    
    [mcp_servers.agent-relay]
    command = "npx"
    args = ["-y", "agent-relay", "mcp"]
    env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://cast.agentrelay.com", RELAY_AGENT_TYPE = "agent" }
    ```
    
    2. Copy hooks config:
    
    ```bash
    cp .agents/skills/agent-relay/hooks/hooks.json .codex/hooks.json
    ```
    
    3. Install the worker agent:
    
    ```bash
    mkdir -p .codex/agents
    cp .agents/skills/agent-relay/codex-config/relay-worker.toml .codex/agents/relay-worker.toml
    ```
    
    </details>
    
    ## Usage
    
    ### Use the skill directly
    
    Invoke the skill explicitly:
    
    ```text
    $agent-relay Coordinate this refactor with two workers and keep all status updates in Relaycast.
    ```
    
    Or describe the task naturally and let Codex match the skill from its description.
    
    ### Spawn relay workers
    
    Once `relay-worker.toml` is installed, delegate bounded tasks to the `relay-worker` custom agent and include:
    
    - the worker relay name
    - the lead relay name
    - the workspace-key source
    - exact task scope
    - completion criteria
    
    Example:
    
    ```text
    Spawn a relay-worker named api-worker.
    Have it check Relaycast, ACK me, update the API route tests only, send STATUS after the first green test run, and send DONE with evidence before exit.
    ```
    
    ### Coordinate a team
    
    Use Relaycast when workers need to message each other directly, not only the lead. Good fits:
    
    - parallel implementation across separate subsystems
    - lead/worker review loops
    - shared channel updates for longer-running tasks
    - cross-terminal or cross-machine collaboration
    
    ## Environment variables
    
    | Variable           | Required | Default                       | Description                                                          |
    | ------------------ | -------- | ----------------------------- | -------------------------------------------------------------------- |
    | `RELAY_API_KEY`    | No       | `""` in the template config   | Relaycast workspace key                                              |
    | `RELAY_BASE_URL`   | No       | `https://cast.agentrelay.com` | Relaycast API base URL                                               |
    | `RELAY_AGENT_TYPE` | No       | `agent`                       | Default Relaycast agent type                                         |
    | `RELAY_AGENT_NAME` | No       | unset                         | Optional stable relay identity when your workflow wants a fixed name |
    
    ## Plugin structure
    
    ```text
    codex-relay-skill/
      SKILL.md                    # Codex skill manifest and workflow instructions
      README.md                   # Installation and usage docs
      agents/
        openai.yaml              # Agent Relay MCP dependency metadata
      codex-config/
        config.toml              # Template MCP server config for .codex/config.toml
        relay-worker.toml        # Template custom worker agent for .codex/agents/
      scripts/
        setup.sh                 # Auto-installer (runs on first skill activation)
      hooks/
        hooks.json               # Hook definitions (SessionStart, Stop, UserPromptSubmit)
        session-start.sh         # Auto-connect and state persistence
        stop-inbox.sh            # Block exit while unread messages exist
        prompt-inbox.sh          # Rate-limited inbox polling and context injection
    ```
    
    Installed layout in a project typically looks like:
    
    ```text
    .agents/skills/agent-relay/   # Skill directory Codex scans
    .codex/config.toml            # Runtime Agent Relay MCP server + features.codex_hooks
    .codex/hooks.json             # Hook wiring (copied from skill)
    .codex/agents/relay-worker.toml
    ```
    
  • SKILL.md 6.8 KB
    ---
    name: agent-relay
    description: Use when you need Codex to coordinate multiple agents through Agent Relay for peer-to-peer messaging, lead/worker handoffs, or shared status tracking across sub-agents and terminals.
    ---
    
    # Agent Relay
    
    Use this skill when Codex needs real-time coordination across multiple agents. It gives Codex a repeatable workflow for:
    
    - connecting to an Agent Relay workspace
    - spawning relay-aware workers
    - sending direct messages, channel updates, and thread replies
    - keeping lead and worker state synchronized through ACK, STATUS, BLOCKED, and DONE signals
    
    Relay fills the peer-to-peer gap in Codex sub-agent workflows. Codex can spawn and collect worker results, but Agent Relay gives those workers a shared message bus so they can talk to the lead and to each other.
    
    ## Auto-setup
    
    On first activation, this skill auto-configures Codex by running `scripts/setup.sh`. This adds the Agent Relay MCP server to `.codex/config.toml`, enables hooks, installs `hooks.json`, and copies the `relay-worker.toml` agent definition. No manual setup is required after installing the skill.
    
    ## Startup protocol
    
    Every relay-connected Codex agent must complete these steps IN ORDER before substantive work:
    
    1. **Set up a workspace.**
       - If `RELAY_WORKSPACE_KEY` is set in the environment, call `set_workspace_key` with that key.
       - If only the legacy `RELAY_API_KEY` alias is set, treat it as the same workspace key.
       - If no key is available, call `create_workspace` to auto-create one. This returns a workspace key — save it for workers.
    
    2. **Register as an agent.** Call `register_agent` with your agent name and `type: "agent"`. Use `RELAY_AGENT_NAME` from the environment if set, otherwise derive a name from the task context (e.g., `lead`, `auth-worker`).
    
    3. **Keep workspace credentials out of output.** Never print the workspace key
       or construct an observer URL from it — it is an administrative credential.
       When the user asks to follow the conversation, run `agent-relay observer`
       and print the URL it returns. That mints a scoped, read-only token
       (`ot_live_...`) that expires in 24 hours and excludes agent DMs, so the link
       is safe to share. Narrow it further with `--channels`, widen it with
       `--include-dms` or `--expires`, and revoke it with
       `agent-relay observer revoke <id>`.
    
    4. **Check the relay inbox.** Call `check_inbox` to see if there are any pending messages or task assignments.
    
    5. **Send an ACK.** If you received a task assignment, send `ACK: <one-sentence understanding>` to your lead via `send_dm`. If the assignment is unclear, send `BLOCKED: <question>` instead of guessing.
    
    6. **When the task is complete**, send `DONE: <summary with evidence>` before stopping.
    
    If workspace creation or registration fails, retry once, then report the failure to the user — do not proceed without a relay connection.
    
    ## Critical rule
    
    **Do not assume the current MCP session already has an active Agent Relay workspace.** Always call `set_workspace_key` or `create_workspace` before registering.
    
    ## Working rules
    
    - Include `as: "<agent-name>"` on relay calls that support explicit attribution.
    - Keep the relay identity stable for the whole task. Do not switch names mid-task.
    - Check the inbox again after meaningful milestones, before long-running work, and before stopping.
    - Prefer direct messages for lead/worker coordination. Use channels only when multiple agents need the same update.
    - Keep status messages short, factual, and scoped to the assigned work.
    - Do not spawn additional relay workers unless the lead explicitly asks for more delegation.
    - If the lead updates the task, follow the newest explicit instruction.
    
    ## Message templates
    
    - `ACK: I understand the assignment and I am starting work on <scope>.`
    - `STATUS: Finished <milestone>; next I am doing <next-step>.`
    - `BLOCKED: I cannot continue because <blocker>.`
    - `DONE: Completed <scope>. Evidence: <files changed, commands run, tests, or decisions>.`
    
    ## Worker patterns
    
    There are two current ways to involve more agents. Use the right one for the
    job.
    
    ### Registered workspace identities
    
    Use `register_agent` for an agent process that is already running and only
    needs a Relay identity. Registration does not start a new model runtime.
    
    **Lead steps:**
    
    1. Ensure workspace exists (`set_workspace_key` or `create_workspace`).
    2. Register the lead (`register_agent`).
    3. Give the other running process the workspace key and tell it to call
       `register_agent` with a stable name.
    4. Send the assignment via `send_dm(to: "worker-name", text: "...")`.
    5. Poll lead inbox for ACK (`check_inbox`).
    
    **Worker steps:**
    
    1. Call `set_workspace_key` with the shared key.
    2. Register with `register_agent`.
    3. Check inbox (`check_inbox`).
    4. Send ACK to lead via `send_dm`.
    5. Perform the assigned scope.
    6. Send DONE to lead via `send_dm`.
    
    ### Relay-spawned workers
    
    Use `add_agent` when the lead should ask Relay to start a provider-backed
    worker. The current tool requires `name`, `cli`, and `task`; optional fields
    include `channel`, `persona`, and `model`.
    
    **Lead steps:**
    
    1. Ensure workspace exists and lead is registered.
    2. Spawn the worker with `add_agent(name: "worker-name", cli: "codex", task: "...")`.
    3. Include `https://agentrelay.com/skill`, the lead name, exact scope, and
       completion criteria in the task prompt.
    4. Poll lead inbox for ACK (`check_inbox`).
    5. Release the worker with `remove_agent` after the work is accepted.
    
    **Worker steps:**
    
    1. Follow the `using-agent-relay` role from `https://agentrelay.com/skill`.
    2. Check inbox, send ACK, do the assigned work, and send DONE.
    
    ### Codex sub-agents
    
    If your Codex surface has a sub-agent spawn capability, use the bundled
    `relay-worker` agent definition for code-heavy work that needs a separate Codex
    runtime with file access and tools. Include the workspace key, relay name, lead
    name, exact scope, and completion criteria in the sub-agent prompt. If that
    spawn capability is not available, use `add_agent` instead.
    
    ## Worker ACK fallback
    
    If a worker does not ACK within 30 seconds:
    
    1. Check whether the worker appears in `list_agents`.
    2. If this is a running process, have it call `register_agent`.
    3. If this should be a spawned worker, call `add_agent` with `name`, `cli`, and `task`.
    4. Send (or re-send) the assignment via `send_dm`.
    5. Poll the lead inbox again for ACK.
    6. If still no ACK after a second attempt, report the exact failed step to the user.
    
    ## Handoff template
    
    ```text
    Worker: api-worker
    Type: relay-spawned worker (use add_agent with name, cli, and task)
    Lead: lead
    Scope: check the Agent Relay inbox and confirm connectivity
    Protocol:
      1. Check inbox
      2. DM lead with ACK
      3. Perform scope
      4. DM lead with DONE
    ```
    
    For code-heavy tasks, change the type line to:
    
    ```text
    Type: Codex sub-agent (use relay-worker if your Codex surface provides sub-agent spawning)
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related