gh:#1924 PR3 — Porting Grok to the HarnessAdapter Protocol

The Broader Goal at the End of the Previous PR

PR1/PR2 of the gh:#1924 strangler-pattern migration had already landed the shared DispatchRequest + HarnessAdapter Protocol + registry + dispatch_pipeline.py, with LegacyAdapter as the fallback for every harness, and CodexAdapter ported and verified against real dispatch traffic as the pilot. The goalpost at that point was: repeat the same verify-before-next-port discipline for each remaining harness, one PR at a time, so a quirk in one harness never again requires touching the shared dispatch_agent() conditionals that used to hold all four harnesses' branching logic at once.

Strategic Shifts in This PR

None. This PR is PR3/5 of the plan exactly as scoped: port Grok next, in isolation, leaving Agy, Claude, and local untouched behind LegacyAdapter.

What This PR Shipped

GrokAdapter implements the HarnessAdapter protocol for Grok, covering the four pieces of harness-specific behavior this session had previously hit as bespoke carve-outs scattered through dispatch_agent():

  • Permission translation — the plan-specified mapping from a generic permission grant to Grok's own CLI flags.
  • Command building — build_cmd() assembling the Grok invocation from the translated permissions plus the harness's dispatch flags.
  • Failure classification / output parsing — explicitly covering the three failure modes this session had already hit live against Grok: 402 billing exhaustion, session expiry, and the sandbox's silent bash-execution denial (which reports exit 0 with no real side effect — exactly the "job status alone is not ground truth" pattern this project has learned to distrust).
  • Model resolution — Grok's model-list lookup, moved out of the shared conditional.

Five Task 10 regression tests exercise permissions, 402 billing exhaustion, session expiry, and sandbox bash denial directly against the new adapter. The registry change is a single line: only grok gets rebound to GrokAdapter; agy, claude, local, and codex stay exactly as PR2 left them.

What Was Achieved Toward the Long-Arc Goal

Two harnesses (Codex, Grok) are now off LegacyAdapter and onto the typed protocol, with their quirks encoded as adapter methods instead of if harness == "grok" branches living next to three other harnesses' branches. The strangler pattern is holding exactly as designed: each port is independently testable and independently verifiable against the exact failure modes that harness has actually produced in production dispatch traffic, not hypothetical ones.

The New Goalpost

Port Agy next (PR4), then Claude/local (PR5), then delete LegacyAdapter once nothing depends on it. Agy's known failure mode — credit exhaustion returning a 429 — is the next adapter's classify_failure() to get right.

Refs #1924.