---
type: reference
subject: cross-model-routing-matrix
audience: developers
last_updated: 2026-05-22
milestone: v1.5.7
---

# Cross-Model Routing Matrix

The cross-model routing matrix is the static decision surface that controls which
backend model executes or reviews each task type when v1.5.7's cross-model
substitution gates are enabled. It lives at `.planning/cross-model.json`
(project-local; per-plan frontmatter overrides are explicitly NOT supported per
v1.5.7 locked decision L3 — matrix-only this milestone).

The matrix exists because the v1.5.7 cross-model executor + verifier substrate
is opt-in and explicitly user-controlled: per-component gates default OFF
(L4 + GATE-02 manual-flip-only critic-gate pattern), and the matrix is the
single SoT for task-type → model routing. Combined with the gates, the matrix
gives lead-machine users explicit control over (a) **independence** (additive
list semantics route Claude + Codex/Gemini side-by-side per task type for
defense-in-depth on Stage-2 verification), (b) **substitution** (single
qualified model value reroutes the task entirely — cost / throughput / model
strength alignment per L8), and (c) **fallback** (advisory failure mode per L6
— matrix-missing or unparseable falls back to all-Claude default with gap
report aggregated at phase end).

The closure-substrate audit (`tests/audit-cross-model-matrix.sh` Phase 53;
MATRIX-01..04) enforces strict schema validation, qualified-model-value
parsing, additive list semantics, and the per-session 5-min in-process cache.
The setter command (`/gmd:set-cross-model`; Phase 54 SETTER-01..04) is the
interactive edit surface that capability-probes every requested
`vendor:model-id` at edit time and warns on unknown variants.

## Matrix Schema

The matrix is a JSON file at the canonical path
`.planning/cross-model.json` (project-relative; gitignored by convention but
checked-in optional per project policy). The schema is:

```json
{
  "version": "1.0",
  "routing": {
    "<task_type>": "claude" | "<vendor>:<model-id>" | ["<entry>", "<entry>", ...]
  },
  "escalation_timeout_seconds": 300,
  "commit_timeout_seconds": 60
}
```

**Top-level keys:**

| Key | Type | Required | Default | Semantics |
|-----|------|----------|---------|-----------|
| `version` | string (semver) | yes | — | Schema version. v1.5.7 ships `"1.0"`. Major bump on breaking schema change. |
| `routing` | object | yes | `{}` | Task-type → model mapping. Empty object equivalent to all-Claude default. |
| `escalation_timeout_seconds` | number | no | `300` | `--auto`-mode adjudication timeout per CRITIC-X-05. Interactive mode preserves indefinite wait. |
| `commit_timeout_seconds` | number | no | `60` | Wait-for-commit barrier timeout per CRITIC-X-02 (poll interval 100ms). |

**`routing.<task_type>` value semantics (3 shapes):**

1. **`"claude"` sentinel** — explicit Claude-default route. Adapter returns
   `{use_native: true}` so the caller invokes native `Task()` directly (no
   adapter overhead, no subprocess spawn).
2. **Single qualified model string** `"<vendor>:<model-id>"` — substitution.
   Adapter runs the task end-to-end against that model. Reject malformed:
   bare `"codex"` / `"gemini"` (no model-id) → `matrix.js` parse failure →
   fall back to all-Claude per L6.
3. **Additive list** `["<entry>", "<entry>", ...]` — additive review path per
   L8 + CRITIC-X-02. Lead (typically `"claude"`) executes and commits;
   adapter dispatches non-Claude reviewer(s) read-only against `git diff
   PRE_SHA..HEAD`. Disagreement → `dispatchCritic` adjudication per L7
   (CRITIC-X-04). Empty list rejected as malformed.

**Worked example showing all 3 shapes:**

```json
{
  "version": "1.0",
  "routing": {
    "ui_implementation": "claude:sonnet-4.5",
    "data_transformation": "gemini:gemini-3-pro",
    "code_review": ["claude", "codex:gpt-5.5"],
    "documentation": "claude"
  },
  "escalation_timeout_seconds": 300,
  "commit_timeout_seconds": 60
}
```

In this example: `ui_implementation` substitutes to Claude Sonnet 4.5 (variant-
qualified Claude — adapter explicitly invokes that variant rather than the
default native `Task()`); `data_transformation` substitutes to Gemini;
`code_review` runs additively (Claude leads + Codex reviews); `documentation`
takes the sentinel Claude-default route.

## Qualified Model Values

All non-sentinel routing values MUST be `vendor:model-id` qualified per L5. Bare
`vendor` strings (e.g., `"codex"` without a model-id) are rejected by
`matrix.js::parseRouting` with an error that includes an example of the
correct form. The supported `vendor` keys and current model-id allowlist:

| Vendor | Model-id allowlist | Substrate | Notes |
|--------|---------------------|-----------|-------|
| `claude` | `sonnet-4.5`, `opus-4.7`, `haiku-4` | Native `Task()` via `{use_native: true}` sentinel | Variant-qualified Claude bypasses the default profile-resolved variant — useful when a specific Claude tier is preferred per task type. |
| `codex` | `gpt-5.5`, `gpt-5-turbo` | Codex CLI ≥ 0.20.0 | Detected at install via `bin/lib/probe-cli.js::existsOnDisk` + `launchProbe` (ADAPT-02; ≤5s probe envelope). |
| `gemini` | `gemini-3-pro`, `gemini-3-flash` | Gemini CLI ≥ 0.42.0 | Binary name configurable via `components.cross_model.shared.gemini_binary` for Antigravity migration readiness (2026-06-18; ADAPT-03). |

**Antigravity migration note:** the Gemini CLI binary may rename to
`antigravity` post-2026-06-18. The matrix is unchanged — only the
`shared.gemini_binary` config override needs updating. Adapter consults the
override via the existing `bin/lib/config.js` per GATE-01.

**Unknown variants** (e.g., `codex:gpt-6` before it ships) are warned-not-
rejected by the setter at edit time. The setter allows override with explicit
`--force` for future-model preregistration (SETTER-03 capability-probe
mitigation per Pitfall 9). At adapter dispatch time, unknown variants surface
as `fallback_reason: "capability_probe_failed"` and the task falls back to
Claude per L6.

## Additive List Semantics

Additive paths execute via the L8 + CRITIC-X-02 sequential-dispatch protocol —
NEVER parallel dispatch (Pitfall 7; honoring v1.5.1 Phase 41 race-lesson lock):

1. **Lead executes and commits.** First entry in the list (typically
   `"claude"`) runs `adapter.runExecutorTask` end-to-end. Lead is responsible
   for the atomic per-task commit.
2. **Adapter captures `PRE_SHA = git rev-parse HEAD`** before lead dispatch.
3. **Wait-for-commit barrier.** Adapter polls `git rev-parse HEAD` at 100ms
   intervals until SHA changes (lead's commit landed) OR
   `commit_timeout_seconds` elapses. On timeout: `fallback_reason:
   "commit_barrier_timeout"` recorded, lead's verdict treated as sole verdict.
4. **Non-Claude reviewer dispatches read-only.** Subsequent list entries run
   with `--sandbox read-only` (Codex) or equivalent (Gemini) per CRITIC-X-03
   — additive ⇒ review-only invariant enforced in adapter code, NOT delegated
   to foreign model config. Reviewer reads `git diff PRE_SHA..HEAD` to produce
   its verdict.
5. **Disagreement → `dispatchCritic` adjudication per L7.** Normalized verdict
   envelopes compared; mismatch routes to `bin/lib/cross-model/adjudicate.js`
   which wraps `bin/lib/critic-dispatch.js::dispatchCritic` (CRITIC-X-01 —
   D2 harness-neutral dispatcher reused unchanged from v1.5.5).
6. **Critic gate disabled + disagreement → advisory-gap behavior** per
   CRITIC-X-06. `adjudicate.js` detects the dispatcher's noop envelope
   (`{verdict: 'noop', _disabled: true}`) and routes to the
   `fallback_reason: "critic_gate_disabled"` path: both verdicts recorded
   verbatim in `VERIFICATION.md` "Cross-Model Coverage" section, surfaced in
   phase-end gap report, no automatic adjudication. Closes the canonical
   adoption-path gap per `[[feedback_critic_gate_manual_toggle]]` (critic gate
   at false is the default posture; forget-to-flip case is canonical not
   edge).
7. **`--auto` escalation timeout.** Default 300s
   (`escalation_timeout_seconds`); on timeout: Claude verdict wins on tie
   (conservative regression baseline per v1.5.5 P1); records
   `fallback_reason: "escalation_timeout_auto_mode"` (CRITIC-X-05).
   Interactive (non-auto) mode preserves indefinite wait.

## Capability-Probe Readback

The Phase 53 spike (research outcome at `.planning/research/SUMMARY.md`)
established the actual model-variant detection pattern per ADAPT-02 + ADAPT-03
+ SETTER-03:

- **Codex readback.** If `codex exec` supports `--output-actual-model` (or
  equivalent) at probe time, parse that field from the response. If absent,
  parse the model name from a trivial test-prompt response metadata. Result
  surfaces as the `model_actual` field in the 3-tuple audit's `executed_by`
  value (e.g. requested `codex:gpt-5.5` may resolve to actual
  `codex:gpt-5.5-2026-04`; the resolved variant is what gets recorded).
- **Gemini readback.** Equivalent probe via `--output-format json` model
  field per ADAPT-03. Antigravity migration: the JSON schema is binary-
  agnostic; only the invocation binary changes (per
  `shared.gemini_binary` override).
- **Setter capability-probe.** `/gmd:set-cross-model` (Phase 54 SETTER-03)
  capability-probes the requested `vendor:model-id` at edit time using the
  same `bin/lib/probe-cli.js::existsOnDisk` + `launchProbe` envelope (≤5s).
  Unknown variant → warn + ask user to confirm; `--force` flag bypasses
  the warn for future-model preregistration.

## Cache Semantics

Matrix lookups are cached per-session (5-min in-process cache, mirrors v1.3
RETR-08 vault-orient pattern) per MATRIX-04. The cache is invalidated at
**phase boundary** (when `gmd-tools.cjs init execute-phase` runs); per-task
lookups within a phase read from the cache, not from disk. This prevents the
"matrix changed mid-phase" failure mode (Pitfall 11) by ensuring the matrix
state is consistent across every task in a phase.

**Gate state is similarly snapshotted at phase entry** per GATE-03: the
`init execute-phase` init JSON is extended with
`cross_model_gates_at_phase_start: {executor: bool, verifier: bool}`, and
per-task lookups read that snapshot — NOT live config. This is the same
Pitfall 11 mitigation as the matrix cache, applied to the gate state.

To force a matrix reload mid-phase (e.g., after editing the matrix via
`/gmd:set-cross-model` while a phase is paused at a checkpoint), advance the
phase boundary by re-invoking `/gmd:execute-phase` (which re-runs
`init execute-phase` and refreshes both the matrix cache and the gate
snapshot).

## See Also

- [`README.md#cross-model-routing`](../README.md#cross-model-routing) — user-
  facing overview (DOCS-02): per-component gates, matrix syntax,
  `/gmd:set-cross-model` walkthrough, `routed_to ≠ executed_by` audit
  troubleshooting.
- [`commands/gmd/set-cross-model.md`](../commands/gmd/set-cross-model.md) —
  interactive setter command (Phase 54 SETTER-01).
- [`references/cross-model-invariants.md`](./cross-model-invariants.md) —
  EXEC-INVAR-03 invariant scaffolding contract for prompt-injected GMD-
  invariant preamble (Phase 55).
- [`references/cross-model-dispatch.md`](./cross-model-dispatch.md) — adapter
  dispatch primitives (Phase 53 ADAPT-01).
- [`references/probe-discipline.md`](./probe-discipline.md) — CLI capability-
  probe allowlist convention (ADAPT-04 + ADAPT-05).
- [`CHANGELOG.md`](../CHANGELOG.md) — `[1.5.7]` entry summarizing the full
  v1.5.7 ship.
