Files
fusion/plugins/fusion-plugin-grok-runtime/src/cli-stream.ts
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

49 lines
2.0 KiB
TypeScript

import { spawn, type ChildProcessByStdio } from "node:child_process";
import type { Readable } from "node:stream";
/*
FNXC:GrokCli 2026-07-10-12:50:
FN-7796: the operator-installed binary is xAI's Grok Build TUI (`grok 0.2.93`). Its `--output-format streaming-json` mode intermittently ends `stopReason:"Cancelled"` with zero `text` events, so Fusion's headless prompt path uses the reliable single-object contract, `grok -p <prompt> --output-format json [-m <model>] [--cwd <dir>]`. Keep foreground piped stdio and Windows shell handling so the adapter can buffer stdout, parse the object on close, and surface close/stderr diagnostics without raw detached processes.
*/
export type GrokStreamProcess = ChildProcessByStdio<null, Readable, Readable>;
export interface SpawnGrokStreamOptions {
cwd?: string;
model?: string;
signal?: AbortSignal;
}
/**
* Spawn `grok -p <prompt> --output-format json [-m <model>] [--cwd <cwd>]`
* with piped stdio. The adapter buffers stdout and parses the complete
* single-object response on subprocess close.
*/
export function spawnGrokStream(binary: string, prompt: string, options?: SpawnGrokStreamOptions): GrokStreamProcess {
const args: string[] = ["-p", prompt, "--output-format", "json"];
const model = options?.model?.trim();
if (model) {
// FNXC:GrokCliRouting 2026-07-10-10:49: FN-7790 keeps FN-7753's concrete `grok-cli/*` model preservation but uses xAI Grok Build TUI's accepted short flag, `-m <model>`, with the provider prefix stripped by runtime-adapter.ts.
args.push("-m", model);
}
if (options?.cwd) {
args.push("--cwd", options.cwd);
}
return spawn(binary, args, {
cwd: options?.cwd,
stdio: ["ignore", "pipe", "pipe"],
shell: process.platform === "win32",
signal: options?.signal,
}) as GrokStreamProcess;
}
/** Force-kill a Grok CLI streaming subprocess. Best-effort; never throws. */
export function forceKillGrokStream(proc: GrokStreamProcess): void {
try {
proc.kill("SIGKILL");
} catch {
// best effort
}
}