---
mode: subagent
---

<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2025-2026 Marcus Quinn -->
# t1665.1: Create runtime-registry.sh — Central Runtime Data Source

## Origin

- **Created:** 2026-03-26
- **Session:** claude-code:interactive
- **Created by:** marcusquinn (human + ai-interactive)
- **Parent task:** t1665
- **Conversation context:** Audit found 45+ files with hardcoded runtime references. Need a single source of truth for runtime properties.

## What

A new shell library `.agents/scripts/runtime-registry.sh` that defines all supported AI CLI runtimes and provides lookup functions. Every script that currently hardcodes `command -v opencode` or `~/.config/opencode/opencode.json` will instead source this file and call registry functions.

## Why

- 6+ scripts contain duplicate `if command -v opencode ... elif command -v claude ...` detection patterns
- 5+ scripts hardcode process detection patterns (`opencode|opencode-ai`, `claude|claude-ai`)
- 30+ files hardcode config paths (`~/.config/opencode/opencode.json`, `~/.claude/settings.json`)
- Adding a new runtime means finding and updating all these locations — error-prone and unscalable
- The registry centralizes this so adding a runtime is one data entry, not 45 file edits

## How (Approach)

### File: `.agents/scripts/runtime-registry.sh`

**Data model** — Bash 3.2 compatible using parallel indexed arrays (no associative arrays):

```bash
# Runtime identifiers (used as keys throughout the framework)
RUNTIME_IDS=("opencode" "claude" "codex" "cursor" "droid" "gemini" "windsurf" "continue" "kilo" "kiro" "aider")

# CLI binary names (for `command -v` detection)
RUNTIME_BINARIES=("opencode" "claude" "codex" "cursor" "droid" "gemini" "windsurf" "continue" "kilo" "kiro" "aider")

# Primary config file paths (empty string = no file-based config)
RUNTIME_CONFIG_PATHS=(
  "$HOME/.config/opencode/opencode.json"
  ""  # Claude uses CLI, not file editing
  "$HOME/.codex/config.toml"
  "$HOME/.cursor/mcp.json"
  ""  # Droid uses CLI
  "$HOME/.gemini/settings.json"
  "$HOME/.codeium/windsurf/mcp_config.json"
  "$HOME/.continue/config.json"
  "$HOME/.kilo/mcp.json"
  "$HOME/.kiro/mcp.json"
  ""  # Aider uses YAML/CLI
)

# Config format identifiers (consumed by mcp-config-adapter.sh)
RUNTIME_CONFIG_FORMATS=(
  "json-opencode"       # JSON with .mcp key, command as array, env as "environment"
  "cli-claude"          # claude mcp add-json CLI
  "toml-codex"          # TOML [mcp_servers.NAME] sections
  "json-mcpServers"     # JSON with .mcpServers key (shared by Cursor/Windsurf/Gemini/Kilo/Kiro)
  "cli-droid"           # droid mcp add CLI
  "json-mcpServers"
  "json-mcpServers"
  "json-array-continue" # JSON with mcpServers as array, transport sub-object
  "json-mcpServers"
  "json-mcpServers"
  "yaml-aider"          # YAML mcp-servers list
)

# MCP root key in config file (empty = N/A for CLI-based runtimes)
RUNTIME_MCP_ROOT_KEYS=("mcp" "" "mcp_servers" "mcpServers" "" "mcpServers" "mcpServers" "mcpServers" "mcpServers" "mcpServers" "")

# Slash command / skill directory paths
RUNTIME_COMMAND_DIRS=(
  "$HOME/.config/opencode/command"
  "$HOME/.claude/commands"
  "$HOME/.codex/skills"
  ""  # Cursor has no slash command dir
  "$HOME/.factory/skills"
  ""
  ""
  ""
  ""
  ""
  ""
)

# System prompt injection mechanism identifier (consumed by prompt-injection-adapter.sh)
RUNTIME_PROMPT_MECHANISMS=(
  "json-instructions"          # opencode.json "instructions" field (array of file paths)
  "agents-md-autodiscovery"    # ~/.config/Claude/AGENTS.md + ~/.claude/AGENTS.md auto-loaded
  "codex-instructions-md"      # ~/.codex/instructions.md (global) + per-project
  "cursorrules-plus-agents"    # .cursorrules (project) + ~/.cursor/rules/ (global) + AGENTS.md
  "factory-skills"             # ~/.factory/skills/ symlink
  "gemini-agents-md"           # ~/.gemini/AGENTS.md
  "windsurfrules"              # .windsurfrules (project)
  "continue-rules"             # .continuerules (project) + config.json systemMessage
  "kilo-rules"                 # AGENTS.md
  "kiro-rules"                 # AGENTS.md
  "aider-read"                 # .aider.conf.yml read: field
)

# Session database paths (for conversational memory lookup — empty = not applicable)
RUNTIME_SESSION_DBS=(
  "$HOME/.local/share/opencode/opencode.db"
  ""  # Claude Code uses ~/.claude/projects/ JSONL, not a DB
  ""
  ""
  ""
  ""
  ""
  ""
  ""
  ""
  ""
)

# Process name patterns for ps/pgrep detection (pipe-separated regex)
RUNTIME_PROCESS_PATTERNS=(
  "opencode|opencode-ai"
  "claude|claude-ai"
  "codex"
  "cursor"
  "droid"
  "gemini"
  "windsurf"
  ""
  ""
  ""
  "aider"
)

# Human-readable display names
RUNTIME_DISPLAY_NAMES=("OpenCode" "Claude Code" "Codex" "Cursor" "Droid" "Gemini CLI" "Windsurf" "Continue.dev" "Kilo Code" "Kiro" "Aider")
```

**Lookup functions:**

```bash
# Get index for a runtime ID (returns 1 if not found)
runtime_index() { local id="$1"; ... }

# Get a property by runtime ID
get_runtime_binary() { local id="$1"; ... }
get_runtime_config_path() { local id="$1"; ... }
get_runtime_config_format() { local id="$1"; ... }
get_runtime_mcp_root_key() { local id="$1"; ... }
get_runtime_command_dir() { local id="$1"; ... }
get_runtime_prompt_mechanism() { local id="$1"; ... }
get_runtime_session_db() { local id="$1"; ... }
get_runtime_process_pattern() { local id="$1"; ... }
get_runtime_display_name() { local id="$1"; ... }

# Detection functions
detect_installed_runtimes()    # Returns newline-separated list of installed runtime IDs
is_runtime_installed() { local id="$1"; command -v "$(get_runtime_binary "$id")" &>/dev/null; }
detect_runtime_config_exists() { local id="$1"; local p; p=$(get_runtime_config_path "$id"); [[ -n "$p" && -f "$p" ]]; }

# Iteration helpers
for_each_installed_runtime() { local callback="$1"; shift; ... }  # Calls callback(runtime_id) for each installed
for_each_runtime_with_config() { local callback="$1"; shift; ... }  # Only runtimes with config files
```

**Integration with shared-constants.sh:**

Source `runtime-registry.sh` from `shared-constants.sh` so all scripts that already source shared-constants get the registry automatically. Replace the existing `detect_available_backends()` function (lines 1382-1392) with a wrapper around `detect_installed_runtimes()`.

**Feature flag integration:**

The existing `manage_opencode_config` / `manage_claude_config` feature flags become `manage_runtime_config_<id>` or a single `managed_runtimes` list in `aidevops.defaults.jsonc`. The registry respects these — `for_each_installed_runtime` skips runtimes where management is disabled.

### Key files to reference

- `.agents/scripts/shared-constants.sh:1382-1392` — existing `detect_available_backends()`
- `setup.sh:387-395` — existing `find_opencode_config()`
- `.agents/scripts/runner-helper.sh:133-169` — existing `_detect_backend()`
- `.agents/scripts/session-count-helper.sh:116-338` — existing per-runtime session counting
- `.agents/scripts/verify-mirrors.sh:24-32` — existing `EXPECTED_SYMLINKS` array (similar pattern)

## Acceptance Criteria

- [ ] `runtime-registry.sh` exists at `.agents/scripts/runtime-registry.sh`
  ```yaml
  verify:
    method: bash
    run: "test -f ~/.aidevops/agents/scripts/runtime-registry.sh"
  ```
- [ ] Defines 11+ runtimes (opencode, claude, codex, cursor, droid, gemini, windsurf, continue, kilo, kiro, aider)
  ```yaml
  verify:
    method: bash
    run: "source ~/.aidevops/agents/scripts/runtime-registry.sh && [[ ${#RUNTIME_IDS[@]} -ge 11 ]]"
  ```
- [ ] All lookup functions work: `get_runtime_binary opencode` returns `opencode`
  ```yaml
  verify:
    method: bash
    run: "source ~/.aidevops/agents/scripts/runtime-registry.sh && [[ $(get_runtime_binary opencode) == 'opencode' ]]"
  ```
- [ ] `detect_installed_runtimes` returns only runtimes whose binary is in PATH
- [ ] `is_runtime_installed` returns correct exit codes
- [ ] Sourced from `shared-constants.sh` so all existing scripts inherit it
- [ ] Bash 3.2 compatible — no associative arrays, no mapfile, no `${var,,}`
  ```yaml
  verify:
    method: bash
    run: "/bin/bash -c 'source ~/.aidevops/agents/scripts/runtime-registry.sh && detect_installed_runtimes'"
  ```
- [ ] `shellcheck` passes with zero violations
  ```yaml
  verify:
    method: bash
    run: "shellcheck ~/.aidevops/agents/scripts/runtime-registry.sh"
  ```
- [ ] Existing `detect_available_backends()` in shared-constants.sh delegates to registry
