{
  "$comment": [
    "Machine-readable harness capability registry. Consumed by mux (lead@mux-v2) to decide",
    "whether a seat can hold a Patchcord listener and whether two seats may share a host.",
    "",
    "TWO SCOPE FIELDS ON PURPOSE. `installer_scope` is where OUR installer writes.",
    "`harness_scope` is what the HARNESS itself supports. They are not the same question and",
    "conflating them is what this file exists to prevent: in one thread I answered three",
    "harness-capability questions with installer behaviour and was wrong all three times.",
    "hermes is the worked example — our installer writes one global file, and hermes supports",
    "many isolated profiles. 'Our installer writes X' never implies 'the harness only does X'.",
    "",
    "EVIDENCE IS EARNED, NOT TYPED. This file may only ever say \"declared\" or \"absent\".",
    "\"tested\" is not a legal value here — it lives in tests/test-harness-registry.mjs, in a",
    "list that names what was observed and by whom. A writer can type a confident guess into",
    "a data file; a writer cannot type an observation into a test they did not run. If you",
    "believe a row deserves \"tested\", add it there with the observation, not here.",
    "",
    "Any row whose listener.wake is not \"none\" MUST have a real subscribe path on disk, or",
    "carry blocked_reason saying why not. hermes is why: it HAS a subscribe skill directory",
    "and, until 0.6.41, could not actually be armed. Directory presence is necessary, never",
    "sufficient."
  ],
  "version": 1,
  "harnesses": [
    {
      "id": "claude_code",
      "aliases": [
        "claude"
      ],
      "cli": "claude",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".mcp.json",
      "harness_scope": "project",
      "listener": {
        "wake": "realtime",
        "mechanism": "subscribe-skill-under-monitor",
        "self_arm": true,
        "survives_wake": true,
        "evidence": "declared"
      }
    },
    {
      "id": "codex",
      "aliases": [],
      "cli": "codex",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".codex/config.toml",
      "harness_scope": "project",
      "listener": {
        "wake": "none",
        "mechanism": "stop-hook-nudge-only",
        "self_arm": false,
        "survives_wake": null,
        "evidence": "declared",
        "note": "A stop hook fires at the END of a turn. An idle codex seat hears nothing. No subscribe skill exists for codex in this package."
      }
    },
    {
      "id": "cursor",
      "aliases": [],
      "cli": "cursor-agent",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".cursor/mcp.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "realtime",
        "mechanism": "subscribe-skill-background-shell",
        "self_arm": true,
        "survives_wake": null,
        "evidence": "declared",
        "note": "The skill claims it wakes the agent. Nobody in this repo has watched it happen."
      }
    },
    {
      "id": "grok",
      "aliases": [
        "grok_cli",
        "grok_build"
      ],
      "cli": "grok",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".grok/config.toml",
      "harness_scope": "project",
      "listener": {
        "wake": "realtime",
        "mechanism": "subscribe-skill",
        "self_arm": true,
        "survives_wake": null,
        "evidence": "declared",
        "note": "Grok resolves configs from cwd up to the git root, so per-seat identity works. Wake is claimed by the skill, not observed."
      }
    },
    {
      "id": "opencode",
      "aliases": [],
      "cli": "opencode",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": "opencode.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "realtime",
        "mechanism": "installed-plugin",
        "self_arm": true,
        "survives_wake": true,
        "evidence": "declared",
        "note": "The installer writes .opencode/plugins/patchcord.js, which spawns `patchcord subscribe` and calls client.session.prompt() on each message. It prompts on the realtime event rather than session.idle deliberately: session.idle re-prompting has a teardown race in headless `opencode run`."
      }
    },
    {
      "id": "kimi",
      "aliases": [],
      "cli": "kimi",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".kimi-code/mcp.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "poll",
        "mechanism": "poll-script",
        "self_arm": true,
        "survives_wake": false,
        "evidence": "declared",
        "note": "NOT realtime. patchcord-subscribe.sh polls /api/inbox and EXITS 0 on a pending count; Kimi starts a turn when a background task reaches a terminal state. So the listener dies on every delivery and the agent must re-arm it. Re-arm is the failure point: an agent that skips it is silently deaf from then on."
      }
    },
    {
      "id": "hermes",
      "aliases": [],
      "cli": "hermes",
      "kind": "terminal",
      "installer_scope": "env-directed",
      "installer_config": "$HERMES_HOME/config.yaml if set, else the output of `hermes config path` if the binary resolves (which reflects the ambient profile), else ~/.hermes/config.yaml",
      "harness_scope": "per-profile-or-per-home",
      "installer_defect": "WAS: the path was hardcoded to ~/.hermes/config.yaml. The installer never called `hermes config path` and did not know profiles exist, so installing while a non-default profile was active wrote the DEFAULT profile's config — the wrong file, silently. FIXED (scripts/lib/hermes-home.mjs, resolveHermesHome/resolveHermesConfigPath/resolveHermesSkillsDest, all three deriving from the same resolved home): HERMES_HOME from the environment, else a shelled `hermes config path` (which itself honours HERMES_HOME — the child inherits the environment), else the unchanged ~/.hermes fallback. Applies to config.yaml AND to where Hermes skills install (~/.hermes/skills/integrations was hardcoded in three more places, unaware of HERMES_HOME — a seat isolated by home for identity but not for skills is not isolated: skills are a guard, not documentation). mux measured two independent isolation mechanisms this way — HERMES_HOME and -p/--profile, both real, both giving a separate mcp_servers AND a separate .env — which is why harness_scope reads per-profile-or-per-home rather than per-profile alone. installer_scope changed from 'global' to 'env-directed' for the same reason: our installer's write target is no longer a fixed machine-wide path, it is wherever HERMES_HOME points when mux sets it, falling back to global only when nothing directs it. PROFILE AXIS: COVERED, AND MEASURED RATHER THAN CLAIMED. This entry previously said profile support was deliberately not built, and that a user on a non-default -p/--profile without a matching HERMES_HOME would still get the silent-wrong-file failure. That was WRONG and lead@mux-v2 disproved it on a real Hermes install (boca, 2026-08-20): `hermes profile use sticky1` then `hermes config path` answers $HERMES_HOME/profiles/sticky1/config.yaml. The shelled resolver therefore honours the ambient profile by construction, because Hermes resolves it, not us. The same measurement confirmed the output is a FILE path, so dirname() in resolveHermesHome is correct. Resolution order in full: HERMES_HOME set (a mux seat) uses it directly and the profile is correctly ignored; HERMES_HOME unset with a profile active gets the profile's config; no hermes binary falls back to ~/.hermes, and a host with no hermes has no hermes seat. Under a sticky profile skills land at .../profiles/<name>/skills/integrations - profile-local skills beside profile-local config, which is the intent. Shipped in 0.6.44.",
      "listener": {
        "wake": "realtime",
        "mechanism": "webhook-bridge",
        "self_arm": false,
        "survives_wake": true,
        "evidence": "declared",
        "note": "`patchcord subscribe --hermes` POSTs to a Hermes gateway webhook per message; the gateway injects a prompt. No re-arm needed. self_arm is false: a human creates the route and starts the bridge once. A deploy that waits N seconds for a SEAT-started listener will fail hermes forever while hermes is fully push-capable.",
        "blocked_until": "0.6.41",
        "blocked_reason": "Before 0.6.41 the `patchcord subscribe` wrapper spawned subscribe.mjs with no arguments, so --hermes never reached the child that gates on it. The env var arrived, the flag did not, the flag is the gate. The bridge was unreachable by its own documented command and failed silently. Fixed; not yet observed end to end."
      }
    },
    {
      "id": "antigravity",
      "aliases": [
        "agy"
      ],
      "cli": "antigravity",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".agents/mcp_config.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "none",
        "mechanism": null,
        "self_arm": false,
        "survives_wake": null,
        "evidence": "declared",
        "note": "No subscribe skill of any kind in this package."
      }
    },
    {
      "id": "openclaw",
      "aliases": [],
      "cli": "openclaw",
      "kind": "terminal",
      "installer_scope": "unknown",
      "installer_config": "~/.openclaw/openclaw.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "none",
        "mechanism": null,
        "self_arm": false,
        "survives_wake": null,
        "evidence": "absent",
        "note": "installer_scope is unknown, not global. We PREFER `openclaw mcp set patchcord`, so openclaw decides where the entry lands; the ~/.openclaw path is only our fallback when that CLI is missing. Nobody here has openclaw installed. Do not refuse a second openclaw seat on the strength of this row — refuse on a test or not at all."
      }
    },
    {
      "id": "windsurf",
      "aliases": [],
      "cli": null,
      "kind": "editor",
      "installer_scope": "global",
      "installer_config": "~/.codeium/windsurf/mcp_config.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "none",
        "mechanism": null,
        "self_arm": false,
        "survives_wake": null,
        "evidence": "declared"
      }
    },
    {
      "id": "vscode",
      "aliases": [],
      "cli": null,
      "kind": "editor",
      "installer_scope": "project",
      "installer_config": ".vscode/mcp.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "none",
        "mechanism": null,
        "self_arm": false,
        "survives_wake": null,
        "evidence": "declared"
      }
    },
    {
      "id": "zed",
      "aliases": [],
      "cli": null,
      "kind": "editor",
      "installer_scope": "global",
      "installer_config": "~/.config/zed/settings.json",
      "harness_scope": "unknown",
      "listener": {
        "wake": "none",
        "mechanism": null,
        "self_arm": false,
        "survives_wake": null,
        "evidence": "declared"
      }
    },
    {
      "id": "cline",
      "aliases": [],
      "cli": null,
      "kind": "editor",
      "installer_scope": "global",
      "installer_config": "cline_mcp_settings.json (VS Code globalStorage)",
      "harness_scope": "unknown",
      "listener": {
        "wake": "none",
        "mechanism": null,
        "self_arm": false,
        "survives_wake": null,
        "evidence": "declared"
      }
    },
    {
      "id": "jcode",
      "aliases": [],
      "cli": "jcode",
      "kind": "terminal",
      "installer_scope": "project",
      "installer_config": ".jcode/mcp.json",
      "harness_scope": "project",
      "listener": {
        "wake": "stall",
        "mechanism": "stdio-stall-signal, with api-socket soft_interrupt when the bridge runs",
        "self_arm": true,
        "survives_wake": null,
        "evidence": "declared",
        "note": "`patchcord subscribe --jcode --stall-signal`, stdio only - jcode cannot speak HTTP MCP, so the listener is a background bash task reading stdout. Skills are GLOBAL (~/.jcode/skills/), not per-project; jcode also loads ~/.agents/skills and the Claude plugins root, which is why patchcord slash commands can appear twice under two naming conventions. TWO WAKE PATHS, and wake stays 'stall' because that is the floor: REAL PUSH via jcode's versioned harness API socket (jcode-api.sock, started by the opt-in `jcode api-bridge`) carrying soft_interrupt - inject at the next safe point without cancelling - lands within a second; the FLOOR is --stall-signal going quiet on purpose until the stall watchdog fires (MIN_STALL_WAKE_SECONDS=30, STALL_POLL_INTERVAL=5s), so 30-40s. The bridge is usually absent and subscribe reports which path armed. Read from jcode's source 2026-08-20: crates/jcode-harness-api/src/requests.rs, crates/jcode-harness-api-server/src/lib.rs, crates/jcode-base/src/background.rs."
      }
    }
  ],
  "retired": [
    {
      "id": "gemini",
      "reason": "Retired as an install target; Antigravity covers Google's agent. CLIENT_TYPE_MAP slot \"5\" is left UNASSIGNED rather than reused, so a stale connect page sending client_type=gemini falls through to no choice instead of silently configuring a different harness and writing someone's token to the wrong file. Do not mirror the gap as a bug and do not fill it."
    }
  ]
}
