# Grok engine wrapper + preset — design

audience: AI coding agents first

## Problem

Harness dispatches engines only through WRAPPER-CONTRACT scripts + adapter catalog + presets.
Grok is named as a future engine in `WRAPPER-CONTRACT.md` and the foundation design ("grok later —
additive, one wrapper") but has no wrapper, no adapter row, no preset. Orchestrator cannot seat Grok.

## Decisions already locked (user)

| Decision | Value |
|----------|--------|
| Scope | Wrapper **and** preset (not adapter-only) |
| Script name | `wrappers/grok.sh` |
| Coder model | `grok-composer-2.5-fast` on every coder tier |
| Reviewer / fixer model | `grok-4.5` |
| Auth | Probe then fail-closed; optional `GROK.env` for `XAI_API_KEY` only if present — do NOT require env file when browser login works |

## Approaches

### Approach 1: Headless `grok -p` (recommended)

One wrapper clones `wrappers/ca.sh` / `wrappers/codex.sh` shape. Dispatch:

```
grok -p <prompt> -m <model> --cwd <workspace> --output-format streaming-json --always-approve
  [--resume <sessionId>]
```
# `--yolo` is documented alias of `--always-approve`; wrapper MUST pass `--always-approve` (flag present in `grok --help`).

Bounded with `timeout -k 5 <secs>`, stdin `</dev/null`. Status JSON on stdout. Continuity via
`--resume` / surface `session_id` from stream `end.sessionId` or final json.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Matches existing headless contract engines; documented exit codes + stream events |
| Long-term | Same adapter/preset path as codex/cursor; zero orchestrator change |
| Scalability | One process per task attempt — same cost model as ca.sh |
| Performance | streaming-json keeps long runs mineable; timeout bound prevents hang |
| Reversibility | two-way door — delete wrapper + adapter + preset |
| Infra cost | Uses existing grok install + auth; no new service |

**Weakness:** streaming-json event surface is thinner than cursor stream-json (tool calls may not appear as first-class events) — UI may show coarser transcripts until Grok enriches the stream.

### Approach 2: ACP `grok agent stdio` client

Wrapper speaks JSON-RPC ACP, maps tool_call updates into harness events.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Richer tool visibility; more moving parts (JSON-RPC client, permission protocol) |
| Long-term | Second protocol family inside wrappers/ — harder to keep contract-simple |
| Reversibility | two-way door but larger blast radius |

**Weakness:** overbuild for WRAPPER-CONTRACT (dispatch + rc). Harness does not need IDE protocol.

### Approach 3: Final-json only (`--output-format json`)

Same as Approach 1 but non-streaming final object.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Simpler parse; lost mid-run visibility / cost mining on long tasks |
| Performance | Worse for hung detection and live UI |

**Weakness:** reintroduces "black box until exit" class other wrappers already left.

**Recommended: Approach 1** — same pattern as shipped wrappers, documented headless path, minimal surface, full contract coverage. Approach 2 deferred until stream lacks tool events and a real consumer needs them.

## Design

### Components

1. **`wrappers/grok.sh`** — contract wrapper (dispatch + inspection)
2. **`presets/adapters.json`** — adapter `id: "grok"`
3. **`presets/grok.json`** — preset seats
4. **`spec/events.schema.json`** — dialect enum adds `"grok"` (REQUIRED by `_validate.mjs` cross-check)
5. **`lib/normalize-events.js`** — dialect branch for Grok streaming-json
6. **Tests** — `wrappers/test/grok-session.sh` (mock binary); normalize fixture + tests
7. **Optional** `GROK.env` template search (like CODEX.env) — source if present; never required

### 1. Wrapper seam — `wrappers/grok.sh`

**Invocation (fixed flags + additive):**

```
wrappers/grok.sh --workspace <dir> --trust <prompt> --task-slug <slug> --model <id> [--timeout <secs>] [--resume <sessionId>]
wrappers/grok.sh --health
wrappers/grok.sh --list-models
```

**MUST behavior** (mirror contract; pin to ca.sh/codex.sh patterns):

1. Parse flags fail-closed: missing/invalid → exit `2` + `{"ok":false,"detail":...}` on stderr.
2. `--model` REQUIRED on dispatch — no silent default.
3. Binary: `$_GROK_ENGINE_BIN` override else `grok` on PATH. Missing binary → exit `3`.
4. Optional env load (non-fatal if absent): candidates `_GROK_ENV`, `$REPO_ROOT/GROK.env`,
   `$HOME/.claude/workflows/lib/GROK.env`. Source failure of an **existing** file → exit `3`.
5. **Preflight probe (fail-closed):** run `"$GROK_BIN" version` (or `--version` if that is the
   working form on installed binary). Nonzero / empty → exit `3` with detail. Do NOT dispatch.
6. **`--health`:** probe version; print JSON `{"ok":true,"version":"..."}` (or provider payload);
   exit 0/3/2 per contract. No workspace/trust required.
7. **`--list-models`:** run `grok models` (proxy stdout). Exit 0/3. No invent second schema.
8. Log dir same discipline as ca.sh/codex (`~/Projects/mega-plan-harness/tmp/logs` with TMPDIR fallback).
9. Log header FIRST lines: TOOL + `%q` CMD + workspace/timeout/log path from ONE built args array.
10. Dispatch args array (single source for log + run):

```
GROK_ARGS=(
  timeout -k 5 "$TIMEOUT"
  "$GROK_BIN"
  -p "$PROMPT"
  -m "$MODEL"
  --cwd "$WORKSPACE"
  --output-format streaming-json
  --always-approve
)
# if --resume set: GROK_ARGS+=(--resume "$RESUME_ID")
```

Default `TIMEOUT=360`. stdin `</dev/null`. cd workspace before run.

11. Tee engine stdout/stderr through log + raw file; when `HARNESS_TRANSCRIPT_PATH` set, tee there too
    (WRAPPER-CONTRACT-AMENDMENTS W7).
12. Exit map:

| engine rc / signal | contract |
|--------------------|----------|
| 0 | 0 |
| 124 / 137 | 124 |
| 2 (wrapper usage only) | 2 — do not map engine 2 unless clearly usage |
| raw matches rate-limit / 429 / usage limit | 75 + error event `errorKind:"quota"` + optional `resume_at` |
| else nonzero (incl. auth/network) | 3 |

13. On every exit path (success and failure): emit final `usage` event. Grok stream currently has no
    token fields → `{"kind":"usage","usage":{"unknown":true,"model":"<model>"}}`.
14. On success stdout status line (and log):

```json
{"ok":true,"detail":"grok completed","session_id":"<id-or-empty>"}
```

`session_id` from last streaming event with `sessionId` (`type:end`) or empty string if absent.
On failure: `{"ok":false,"detail":"...","session_id":"..."}` with `ok` agreeing exit code.

15. Continuity: accept `--resume <id>` only (ca.sh style). Forward on run line. NEVER invent gate/retry policy.

**NEVER inside wrapper:** gates, review, fallback to other engines, background daemon, interactive TUI.

### 2. Adapter catalog

Add to `presets/adapters.json`:

```json
{
  "id": "grok",
  "wrapper": "wrappers/grok.sh",
  "description": "Grok Build headless wrapper (grok -p).",
  "listModels": true,
  "metered": false,
  "models": ["grok-4.5", "grok-composer-2.5-fast"]
}
```

`metered: false` REQUIRED while usage is unknown-only. Preflight WARNs unattended if metered false
(existing engine behavior — do not invent new policy).

### 3. Preset `presets/grok.json`

```json
{
  "name": "grok",
  "version": "preset/v1",
  "description": "Grok-only execution: composer-fast for coder, grok-4.5 for review/fix.",
  "seats": {
    "coder": {
      "low":    { "wrapper": "wrappers/grok.sh", "model": "grok-composer-2.5-fast", "timeout": 1200 },
      "medium": { "wrapper": "wrappers/grok.sh", "model": "grok-composer-2.5-fast", "timeout": 1200 },
      "high":   { "wrapper": "wrappers/grok.sh", "model": "grok-composer-2.5-fast", "timeout": 1200 }
    },
    "reviewer": {
      "wrapper": "wrappers/grok.sh",
      "model": "grok-4.5",
      "timeout": 1200
    },
    "fixer": {
      "wrapper": "wrappers/grok.sh",
      "model": "grok-4.5",
      "timeout": 1200
    }
  }
}
```

Tiered coder MUST list all three tiers (`low`/`medium`/`high`) — `_validate.mjs` rejects partial tier maps.
Models MUST be subset of adapter models list.

Do NOT wire grok into `anthropic-less` or other presets in this change.

### 4. Dialect + normalize

- `spec/events.schema.json` `dialect.enum` MUST include `"grok"`.
- `lib/normalize-events.js`:

```
normalizeGrok(parsed, event):
  switch parsed.type:
    "text"     → kind message, subtype text, text=parsed.data
    "thought"  → kind message, subtype thought, text=parsed.data
    "end"      → kind status, subtype end (carry stopReason if present)
    "error"    → kind error, error={code: grok_error, message: parsed.message || ...}
    other      → status/unknown (preserve raw)
```

Wire `case 'grok': return normalizeGrok(...)` in `normalizeParsed`.

Add fixture `lib/test/fixtures/grok.jsonl` with representative lines; extend normalize tests.

### 5. Tests

**`wrappers/test/grok-session.sh`** (model after `wrappers/test/codex-session.sh` / `ca-session.sh`):

| Case | Expect |
|------|--------|
| missing --workspace / --trust / --task-slug / --model | exit 2 |
| mock binary missing / version fails | exit 3 |
| --health with mock version ok | exit 0 + ok JSON |
| --list-models | proxies mock models output |
| happy dispatch | builds args with `-p`, `-m`, `--cwd`, `streaming-json`, `--always-approve`; exit 0; status ok |
| --resume forwarded | args contain `--resume <id>` |
| timeout mock 124 | contract 124 |
| rate-limit text in raw | contract 75 + usage event |
| HARNESS_TRANSCRIPT_PATH set | transcript file non-empty after run |
| usage event always emitted | success and failure paths |

Use `_GROK_ENGINE_BIN` pointing at fixture mock, not real grok.

**Normalize tests:** grok fixture lines normalize without `unknown_dialect`; text/error paths assert kinds.

**Presets:** `node presets/_validate.mjs` green with new adapter + preset.

### 6. Data flow

```
resolve-seat(preset=grok, seat, tier)
  → binding { wrapper: wrappers/grok.sh, model, timeout }
  → grok.sh --workspace WT --trust PROMPT --task-slug SLUG --model MODEL --timeout T
  → preflight version → timeout-bounded grok -p ... streaming-json --yolo
  → log/raw/transcript tee
  → exit code + status JSON + usage event
  → runner journals attempt; gates unchanged
```

### 7. Out of scope

- Changing anthropic-less / risk ladder to include grok
- ACP stdio / WebSocket agent mode
- Real usage metering (until Grok stream exposes tokens)
- Interactive TUI dispatch
- Worktree flags (`--worktree`) — harness owns worktrees

## Architecture Decisions

| Decision | Reason |
|----------|--------|
| Headless `-p` over ACP | WRAPPER-CONTRACT is dispatch+rc; ACP is IDE protocol (Approach 2 rejected) |
| `streaming-json` over final `json` | Live log + sessionId on `end`; matches cost-mining log pattern |
| `--always-approve` always on dispatch | Unattended harness; no human at prompt (`--yolo` alias OK for humans, not for wrapper) |
| `metered: false` + usage.unknown | Stream has no token fields today; honest catalog |
| Adapter id = dialect = `grok` | `_validate.mjs` requires adapter.id ∈ dialect enum |
| Script name `grok.sh` | User lock; full-name style like codex.sh |
| Optional GROK.env only | Browser login already valid; env for CI/key path |
| Coder all tiers same model | Only two stable models; tiers differ by seat (coder vs review), not effort ladder |
| Collapse normalize into same module | Second adapter path not needed; deletion test: separate package would scatter one switch case |

## Acceptance

1. `wrappers/grok.sh` executable; contract flags work under mock tests.
2. `presets/adapters.json` lists grok; `presets/grok.json` validates.
3. `node presets/_validate.mjs` passes.
4. `wrappers/test/grok-session.sh` passes.
5. Normalize tests cover dialect `grok`.
6. Manual smoke (optional, not gate):  
   `wrappers/grok.sh --health` and one short `--workspace` dispatch with real grok when authenticated.
