Files
fusion/docs/grok-cli-contract.md
gsxdsm c258fc1590 FN-7796: switch Grok CLI headless prompts to the reliable single-object JSON output
Narrative: the streaming-json headless contract intermittently emitted only thought events then stopReason:Cancelled with zero text, leaving Chat replies silently empty; the adapter now spawns grok with --output-format json, buffers stdout, and parses the single JSON response on process close, with streaming-json parsing kept only as a diagnostic fallback.

- Change grok CLI invocation from --output-format streaming-json to --output-format json (cli-stream.ts)
- Add GrokCliJsonResponse type ({text, stopReason, sessionId, requestId, thought}) and parseJsonOutput() to stream-parser.ts, keeping legacy NDJSON line parsing for fallback/diagnostics
- Rework runtime-adapter.ts to buffer full stdout, parse it via parsePromptOutput (JSON object first, NDJSON fallback), and surface a formatTerminalNoTextDiagnostic when a non-EndTurn stopReason yields no assistant text
- Rename first-line/inactivity timeout bookkeeping from line-based to output/chunk-based (FIRST_OUTPUT_TIMEOUT_MS, firstOutputReceived, firstStdoutChunk) since stdout is no longer consumed via readline
- Update cli-stream/runtime-adapter/stream-parser tests to cover the JSON response path and the Cancelled/no-text diagnostic
- Update docs/grok-cli-contract.md and plugin README to document the json output-format contract and diagnostics
- Add changeset fn-7796-grok-cli-reliable-headless.md (patch, fix)

Files changed:
 .changeset/fn-7796-grok-cli-reliable-headless.md   |   7 +
 docs/grok-cli-contract.md                          | 108 ++++++++-----
 plugins/fusion-plugin-grok-runtime/README.md       |  16 +-
 .../src/__tests__/cli-stream.test.ts               |   4 +-
 .../src/__tests__/runtime-adapter.test.ts          |  73 ++++++++-
 .../src/__tests__/stream-parser.test.ts            |  80 +++++----
 .../fusion-plugin-grok-runtime/src/cli-stream.ts   |  14 +-
 .../src/runtime-adapter.ts                         | 180 ++++++++++++---------
 .../src/stream-parser.ts                           |  65 ++++++--
 plugins/fusion-plugin-grok-runtime/src/types.ts    |  13 +-
 10 files changed, 373 insertions(+), 187 deletions(-)

Fusion-Task-Id: FN-7796

Fusion-Task-Lineage: c920fcf0-98f8-42ec-867a-7f76c0aca1b7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-10 12:57:41 -07:00

8.4 KiB

Grok CLI Contract (FN-7790, updated by FN-7796)

Date: 2026-07-10

Ground truth

Fusion shells out to an operator-installed grok binary. The binary is not downloaded or bundled by Fusion, so the authoritative contract is the installed xAI CLI's own help/version output plus live execution on an authenticated machine.

External integration evidence:

  • Canonical upstream: xAI official Grok CLI / Grok Build TUI, surfaced by the installed binary as grok 0.2.93 (f00f96316d4b).
  • Docs/homepage: https://grok.com/, https://docs.x.ai/, and grok --help / grok agent --help for exact flags.
  • Release/download: operator-installed; Fusion resolves grok from PATH or grokCliBinaryPath and does not bundle a release artifact.
  • Binary name: grok.
  • Checksum: upstream-pending-verification because Fusion does not pin or download the operator's binary.

The previously documented https://github.com/superagent-ai/grok-cli contract is a different product that happens to use the same binary name. Its grok --prompt <text> --format json invocation is not accepted by xAI's CLI.

Failures that shaped the contract

Wrong-product flags (FN-7790)

The old adapter invocation fails against the real xAI binary:

grok --prompt "say hello" --format json

Observed result:

exit 2
stdout: <empty>
stderr:
error: unexpected argument '--prompt' found

  tip: a similar argument exists: '--prompt-file'

Usage: grok --prompt-file <PATH> [PROMPT]

Because no renderable assistant text is produced, Fusion surfaced a blank/no-message assistant response.

Streaming JSON cancellation with zero text (FN-7796)

FN-7790 correctly switched to xAI's real flags and streaming event union, but live triage found --output-format streaming-json is intermittently unreliable. The same authenticated grok 0.2.93 binary sometimes emits only reasoning events, then ends with stopReason:"Cancelled" and no text event while still exiting 0 with empty stderr.

Live-captured shape:

{"type":"thought","data":"..."}
{"type":"thought","data":"..."}
{"type":"end","stopReason":"Cancelled","sessionId":"...","requestId":"..."}

The adapter previously saw parsed events and a successful close, accumulated empty assistant text, set no error, and produced a silent no-message bubble. The reliable replacement is the single-object JSON contract below.

Confirmed non-interactive invocation used by Fusion

Use xAI Grok Build TUI's single-turn prompt mode with single-object JSON:

grok -p "<text>" --output-format json
# equivalent long prompt flag:
grok --single "<text>" --output-format json

Supported companion flags used by Fusion:

  • -p, --single <PROMPT> — run a single prompt, print the response, and exit. This does not require interactive stdin.
  • --output-format <plain|json|streaming-json> — Fusion uses json for reliable headless prompts.
  • -m, --model <MODEL> — optional concrete model id. Fusion omits this for the model-less grok/default Runtime-mode path.
  • --cwd <CWD> — optional working directory. This replaces the wrong-product --directory flag.

Other observed flags include --prompt-file <PATH>, --prompt-json <JSON>, -s/--session-id <UUID>, --sandbox <PROFILE>, --system-prompt-override <PROMPT>, and --max-turns <N>, but Fusion's adapter does not currently use them.

Reliable JSON response schema

--output-format json emits one final JSON object rather than an NDJSON stream. Observed shape:

interface GrokJsonResponse {
  text?: string;
  stopReason?: string;
  sessionId?: string;
  requestId?: string;
  thought?: string;
}

Example:

{
  "text": "Hello",
  "stopReason": "EndTurn",
  "sessionId": "019f4d81-8fb1-7f11-98ca-5ae00654b518",
  "requestId": "bb7952e2-f1bc-4574-b409-5cc568817fe5",
  "thought": "The user wants me to say hello in one word..."
}

Mapping in Fusion:

  • thought → onThinking(thought) when non-empty.
  • text → onText(text) and accumulated assistant content when non-empty.
  • sessionId → session.sessionId when present.
  • subprocess close remains the authoritative promise resolution point because it carries exit status/stderr diagnostics.

Live reliability evidence from FN-7796: grok -p "say hello in one word" --output-format json returned real text with stopReason:"EndTurn" on 4/4 direct runs, and the built GrokRuntimeAdapter carried real text through onText/persisted assistant content on 3/3 end-to-end runs against the real binary.

Streaming JSON event schema (not the primary prompt path)

--output-format streaming-json emits one JSON object per line:

type GrokStreamingJsonEvent =
  | { type: "thought"; data: string }
  | { type: "text"; data: string }
  | { type: "end"; stopReason?: string; sessionId?: string; requestId?: string };

Successful captured tail:

{"type":"thought","data":" one"}
{"type":"thought","data":"-"}
{"type":"thought","data":"word"}
{"type":"thought","data":" greeting"}
{"type":"thought","data":"."}
{"type":"text","data":"Hello"}
{"type":"text","data":"!"}
{"type":"end","stopReason":"EndTurn","sessionId":"019f4d1e-2582-70e0-a174-c8774782ab01","requestId":"2233f1dc-e9ad-4ae4-8221-caa6afade07f"}

Fusion does not use streaming-json as the primary headless prompt path because it intermittently produces the cancelled/no-text shape documented above. Parser support remains only to keep diagnostics and regression tests concrete if captured streaming output appears in buffered stdout.

Other output formats

--output-format plain prints renderable response text, but does not expose sessionId, requestId, stopReason, or thought.

Model discovery

grok models is plain text, not JSON. Observed shape:

You are logged in with grok.com.

Default model: grok-4.5

Available models:
  * grok-4.5 (default)
  - grok-composer-2.5-fast

Fusion parses the bullet list conservatively and exposes ids under provider grok-cli when the useGrokCli toggle is enabled.

Auth and readiness

The CLI owns authentication for CLI-routed execution. Fusion's readiness probe uses grok --version; a passing probe proves only that a compatible-looking binary exists, not that the prompt path is authenticated or serviceable. The prompt path is proven by a real grok -p ... --output-format json run.

Fusion-visible GROK_API_KEY remains relevant for the direct xAI OpenAI-compatible endpoint. For CLI-routed sessions, Fusion does not need to see a key as long as the operator-installed CLI is authenticated by its own supported mechanism.

Runtime routing

The Grok runtime adapter is reached when:

  1. an agent explicitly sets runtimeConfig.runtimeHint === "grok"; or
  2. the FN-7753/FN-7758 no-visible-key fallback derives the same runtime hint for a grok-cli/* default/fallback provider selection and the bundled Grok Runtime plugin is registered.

The selected grok-cli/<id> or grok/<id> model is normalized to <id> and passed to the CLI as -m <id>. The explicit no-model Runtime-mode path keeps grok/default and omits -m.

Diagnostics and empty-output invariant

The adapter preserves the resolve-never-reject runtime contract while surfacing concrete diagnostics:

  • spawn failure → session.state.errorMessage and diagnostic onText.
  • non-zero subprocess close with no text → stderr/exit diagnostic.
  • code-0 close with no parseable JSON response → wrong-binary/interactive-EOF diagnostic.
  • parseable response with no text and stopReason !== "EndTurn" → stop-reason diagnostic, e.g. Grok CLI ended with stopReason Cancelled and produced no assistant text.
  • parseable EndTurn response with no assistant text → legitimate silent response, not a diagnostic.
  • text emitted before a noisy/non-zero close → keep the assistant text and avoid replacing it with an error.

This invariant prevents the original blank/no-message symptom while still allowing genuinely empty model turns.