---
name: gmd:set-cross-model
description: Set or change the cross-model routing matrix for this project (persists to .planning/cross-model.json with strict schema validation + capability-probe at edit time)
allowed-tools:
  - Read
  - Write
  - Bash
  - AskUserQuestion
---

<objective>
Set or change the cross-model routing matrix for the current project and
persist it to `.planning/cross-model.json` under the project root. The matrix
has 2 top-level keys (`executor` + `verifier`), each holding a qualified
`vendor:model-id` string (e.g. `"codex:gpt-5.5"`), the bare `"claude"`
L6-default sentinel, or an array of qualified values (MATRIX-03 additive list
form per L8).

This is the human-invocable entry point for (a) selecting a foreign model
for verifier-side review (Phase 54 ships verifier substitution; v1.5.7 MVP
fallback per ROADMAP), (b) selecting a foreign model for executor-side work
(Phase 55-56), and (c) the gate-flip flow once a vendor CLI is installed
(`/gmd:set-cross-model` does not flip gates — see snapshot semantics below).

Persists at project scope only — never writes to
`~/.claude/get-magic-done/config.json`. Reuses the v1.3.4
`/gmd:set-vault-root` precedent (interactive prompts + validation + atomic
write + project-scope persistence).
</objective>

<flag_mode>
Usage:
  /gmd:set-cross-model                                                          → interactive (default)
  /gmd:set-cross-model --executor codex:gpt-5.5 --verifier codex:gpt-5.5        → non-interactive (CI/scripted)
  /gmd:set-cross-model --verifier gemini:gemini-3-pro                           → non-interactive verifier-only (executor unchanged)
  /gmd:set-cross-model --executor codex:gpt-5.5 --force                         → non-interactive + skip capability-probe warning

Contract:
  - When BOTH --executor AND --verifier absent → interactive prompts (default; matches /gmd:set-vault-root v1.3.4 UX).
  - When EITHER --executor OR --verifier present → non-interactive write-then-validate path.
  - Capability-probe (via adapter.capabilities) STILL fires in non-interactive mode; unknown variant warns to stderr; --force suppresses the warning and proceeds.
  - Schema validation (via matrix.parseVendorModel) fires in BOTH modes; malformed values reject hard with example of correct form, NEVER silent-accept.

Decision: critic refinement 3 (decision_id 21d61cf8-9f1b-4d7e-bea4-cb9f3e2c8c11).
Non-interactive surface ~30 LOC; prevents future re-litigation when scripted
set-cross-model is needed in CI. Refinement carried forward from Phase 54
54-CONTEXT.md "Setter command style" decisions block.
</flag_mode>

<process>
Follow these steps when the user invokes `/gmd:set-cross-model`. The
substrate code lives in `bin/lib/cross-model/matrix.js` (schema validator),
`bin/lib/cross-model/adapter.js` (capability probe), and
`bin/lib/planning-config.js` (atomic write pattern); this command is a
thin orchestration layer over those Phase 53 + v1.3.4 shipped primitives.

### Step 1 — Parse argv (mode detection; refinement 3)

Scan the invocation arguments for the three documented flags. Both `=`-form
(`--executor=codex:gpt-5.5`) and space-form (`--executor codex:gpt-5.5`)
MUST be accepted (conventional Unix CLI UX). If `--executor` or `--verifier`
is present with no value (e.g. the next argv slot is another `--flag` or
missing entirely), REJECT with an actionable error citing the qualified
form (e.g. `e.g. codex:gpt-5.5`).

```
exec_flag    = parsed value of --executor (or undefined if absent)
verifier_flag = parsed value of --verifier (or undefined if absent)
force        = boolean true if --force present, else false
```

Mode decision: when BOTH `exec_flag` AND `verifier_flag` are absent, `mode
= 'interactive'`. When EITHER is present, `mode = 'non-interactive'`. Mode
governs Step 3 only; Steps 4-7 (validate, probe, write, emit) fire
identically in both modes.

### Step 2 — Read existing matrix

Load the on-disk matrix via the shipped substrate. Null result (file
missing) means start with an empty `{}` object — that is the L6 all-Claude
default per `matrix.js` line 21 contract.

```js
const matrix = require('bin/lib/cross-model/matrix.js');
const current = matrix.loadMatrix(repoRoot) || {};
```

Preserve any keys NOT being edited this invocation (e.g. when only
`--verifier` is supplied in non-interactive mode, do not overwrite the
existing `executor` value). The matrix is also user-editable by hand; this
setter MUST behave as a merge over the existing on-disk state, not a
clobber.

### Step 3a — Interactive mode

When `mode === 'interactive'`, prompt the user in CONTEXT.md decisions
order: executor first, then verifier. Pre-fill each prompt with the
existing value from `current[role]` when present (free-text editable).
Accept the following input shapes:

- Qualified form: `codex:gpt-5.5`, `gemini:gemini-3-pro`, `claude:opus-4.5`
- Bare `claude` (the L6 default sentinel; matrix.parseVendorModel accepts
  it as `{vendor: 'claude', model_id: null}`)
- Array form (MATRIX-03 additive list): user types a JSON array literal,
  e.g. `["codex:gpt-5.5","gemini:gemini-3-pro"]`. Parse the literal; on
  parse failure, re-prompt.

Use the `AskUserQuestion` tool for each prompt. On parse or validation
failure (delegated to Step 4), re-prompt the SAME role until the user
provides a valid value OR aborts.

### Step 3b — Non-interactive mode

When `mode === 'non-interactive'`, populate `next.executor` from
`exec_flag` (or `current.executor` if absent) and `next.verifier` from
`verifier_flag` (or `current.verifier` if absent). DO NOT prompt — every
value is sourced from argv or carried over from the existing on-disk
matrix.

### Step 4 — Validate via shipped parseVendorModel (SETTER-02)

For each role's value, validate via the Phase 53 shipped schema validator.
This setter MUST NOT inline a parallel regex/parser — `parseVendorModel`
is the single source of truth for the `vendor:model-id` schema, shared
with adapter.js and the matrix loader. If this command's setter inlined a
parallel validator and the matrix loader's regex drifted, round-tripping
would break silently.

```js
const matrix = require('bin/lib/cross-model/matrix.js');

function validateRoleValue(role, value) {
  if (Array.isArray(value)) {
    // MATRIX-03 additive list form.
    if (value.length === 0) {
      throw new Error('Invalid matrix value at ' + role + ': empty array.');
    }
    for (let i = 0; i < value.length; i++) {
      matrix.parseVendorModel(value[i], role + '[' + i + ']');
    }
    return;
  }
  // Single-string form (or bare 'claude' L6 sentinel).
  matrix.parseVendorModel(value, role);
}
```

On `parseVendorModel` throw: the error message from the validator already
includes an `example of correct form` (e.g. `codex:gpt-5.5`) per matrix.js
lines 70-71 + 83. In interactive mode, re-prompt the user (Step 3a) with
the error verbatim. In non-interactive mode, print the error and exit 1.
NEVER silent-accept invalid values. NEVER silently substitute a default
for a malformed input — the user wrote `--executor codex` and they need
to see `Expected qualified vendor:model-id form (e.g. "codex:gpt-5.5")`.

### Step 5 — Capability-probe via shipped adapter.capabilities (SETTER-03)

For each role's resolved vendor (`codex` or `gemini`; bare `claude` does
not require a probe — it's always available natively), invoke the shipped
capability probe. DO NOT inline a parallel probe (which would violate
probe-discipline; the Phase 53 ALLOWLIST covers `adapter.js` spawn sites
only, and the 5-min capability cache lives there).

```js
const adapter = require('bin/lib/cross-model/adapter.js');
const phase_id = 'set-cross-model:' + Date.now();
const caps = await adapter.capabilities({ phase_id: phase_id });
// caps = { codex: bool, gemini: bool, _phase_id, _stub }
```

For each requested vendor that is NOT `claude`:

- `caps[vendor] === true` → proceed silently.
- `caps[vendor] === false` AND `--force` supplied → proceed; suppress
  warning. (Future-model preregistration path per Pitfall 9.)
- `caps[vendor] === false` AND NO `--force` → emit loud stderr warning:
  ```
  [cross-model] WARN: vendor "<v>" not on PATH; matrix entry "<value>" preregistered but unrunnable until install (unknown variant — install <v> CLI or re-run with --force to suppress this warning).
  ```
  In interactive mode, prompt the user to confirm proceed. In
  non-interactive mode without `--force`, exit 1 (CI gets a hard fail
  rather than a silent preregistration; --force is the explicit opt-in
  for capability-probe failed scenarios).

### Step 6 — Atomic write via planning-config.js pattern

Persist the new matrix object to `.planning/cross-model.json`. Reuse the
v1.3.4 atomic-write semantics: `mkdir -p .planning/` then `fs.writeFileSync`
with 2-space indent + trailing newline. This is the same shape as
`writePlanningConfig` in `bin/lib/planning-config.js` lines 48-54, applied
to a different target file. The setter MUST NOT use shell redirects (e.g.
`cat`-redirect or `echo`-redirect targeting the matrix file) — those are
non-atomic and lack the write-then-rename guarantees of `fs.writeFileSync`.
Audit Section [8] guards atomic-write reuse with a negative regression
guard against such redirect patterns.

```js
const fs = require('fs');
const path = require('path');
const planningDir = path.join(repoRoot, '.planning');
fs.mkdirSync(planningDir, { recursive: true });
const out = path.join(planningDir, 'cross-model.json');
fs.writeFileSync(out, JSON.stringify(next, null, 2) + '\n', 'utf8');
```

Note path discipline: `.planning/cross-model.json` (the matrix) is a
SEPARATE file from `.planning/config.json` (the vault config). They are
distinct concerns and live side-by-side. This setter MUST NOT merge the
two — `writePlanningConfig` from planning-config.js targets the vault
config; we mirror its pattern shape but write a different file.

### Step 7 — Emit status line

Write a single status line to stdout summarizing the change:

```
[cross-model] matrix updated: executor=<value> verifier=<value> (snapshot taken at <ISO-timestamp>)
```

The "snapshot taken at" phrasing seeds the SETTER-04 help-text language
(see `<help_text>` below). When a role was unchanged (e.g. only
`--verifier` was specified in non-interactive mode), still emit both
keys with their resolved values so the user can confirm the merged state.
</process>

<help_text>
Snapshot semantics:
  Gate flips take effect at next phase entry; current phase uses snapshot taken at <timestamp>.

  The cross-model substrate snapshots gate state at phase entry via the
  `cross_model_gates_at_phase_start` field in `gmd-tools.cjs init execute-phase` output
  (Phase 53 GATE-03). Per-task lookups during a phase read this snapshot, NOT live config.

  Practical consequence: running `/gmd:set-cross-model` mid-phase WILL update
  .planning/cross-model.json on disk, but the CURRENT phase will continue to use
  the snapshot taken at its entry. Your changes take effect at the next
  /gmd:execute-phase or /gmd:verify-phase invocation.

  Why this design (Pitfall 11 UX): per-task live-reads of cross-model.json mid-phase
  would create non-deterministic phase behavior (some tasks routed to old config,
  others to new); the snapshot at phase entry guarantees a phase executes against
  a consistent matrix. The `cross_model_gates_at_phase_start` field in the init JSON
  is the contract — the matrix is read once at phase entry, cached for the duration.

Schema:
  Top-level object with `executor` + `verifier` keys. Leaf values are qualified
  `vendor:model-id` strings (e.g. `"codex:gpt-5.5"`, `"gemini:gemini-3-pro"`,
  `"claude:opus-4.5"`), the bare `"claude"` L6-default sentinel, or arrays of
  qualified values (MATRIX-03 additive list form per L8).

  Example .planning/cross-model.json:
    {
      "executor": "claude",
      "verifier": "codex:gpt-5.5"
    }

  Or additive list form (Phase 57 CRITIC-X consumes disagreement across this list):
    {
      "executor": "claude",
      "verifier": ["codex:gpt-5.5", "gemini:gemini-3-pro"]
    }

  Validation: the shipped schema validator (`matrix.parseVendorModel` in
  `bin/lib/cross-model/matrix.js`) rejects malformed values with an actionable
  error citing the qualified form. Setter never silent-accepts invalid input;
  the user always sees `Expected qualified vendor:model-id form (e.g. "codex:gpt-5.5")`.

Gates:
  This command does NOT flip the cross-model gates (`components.cross_model.executor.enabled`
  / `components.cross_model.verifier.enabled` in .planning/config.json). Setting the
  matrix preregisters a routing; turning on the gate enables it. Use `/gmd:external-agents`
  to flip gates. Snapshot semantics (above) apply to gate flips identically — the
  current phase uses the snapshot taken at its entry.
</help_text>

<set_vault_root_precedent>
This command's UX shape follows the v1.3.4 `/gmd:set-vault-root` precedent
(see `commands/gmd/set-vault-root.md`):

- Interactive prompts with defaults pre-filled (free-text editable).
- Strict validation BEFORE write; never silent-accept invalid input.
- Atomic write via `fs.writeFileSync` after `mkdir -p .planning/`
  (the `writePlanningConfig` shape).
- Persists at project scope only (`.planning/`), never to
  `~/.claude/get-magic-done/`.
- Status line emitted on stdout after successful write so the user sees
  what was persisted.

Differences from set-vault-root:
- Two prompts (executor + verifier) instead of one (vault root).
- Capability-probe step (Step 5) — set-vault-root has no equivalent because
  filesystem path validity is checked by writeFileSync itself; cross-model
  needs an explicit probe because `vendor:model-id` validity has two layers
  (schema form + CLI installed-on-PATH).
- Optional non-interactive flag-mode (refinement 3) for CI/scripted use
  (decision_id 21d61cf8-9f1b-4d7e-bea4-cb9f3e2c8c11); set-vault-root is
  interactive-only per v1.3.4.
</set_vault_root_precedent>
