Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,44 @@ priority: P2
status: open
title: "Grok peer-call failure — cursor-agent exit 1 during multi-agent review"
created: 2026-05-11
last_updated: 2026-05-11
last_updated: 2026-05-13
depends_on: []
composes_with: []
type: friction-reducer
---

## Progress 2026-05-13

- **Acceptance criterion 3** (surface cursor-agent errors more
visibly) ADDRESSED: `tools/peer-call/grok.ts` now captures
cursor-agent's stderr (was previously inherited; streaming-only
visibility) and, on the empty-stdout + failure case, writes a
**self-documenting failure marker** to the output file containing
exit code (or signal name / spawn-error placeholder when
applicable) + model + prompt size + spawn-error message +
captured stderr. Format matches `parsed.outputFormat` (Markdown
for text, JSON for json, NDJSON for stream-json) so consumers
don't break on mixed formats. The output file is no longer
silently empty on cursor-agent failure. Trade-off: stderr is now
delivered post-exit (mirrored to caller stderr after spawnSync
returns), not in real-time — long-running hangs lose live stderr
streaming. Acceptable for the observability gain on the typical
exit-1 failure mode (which delivers stderr quickly).
- **Acceptance criteria 1 + 2** (reproduce + identify root cause):
still open. Aaron noted 2026-05-13 that the Grok website-text-mode
git connector is the working orientation path until B-0421 fully
resolves (see PR #2945 and the peer-call-infrastructure rule
update on PR #2946). When the failure recurs, the captured
stderr in the new failure marker should expose the root cause.
- **Acceptance criterion 4** (4-wrapper smoke test, generalized to
8 wrappers): ADDRESSED via `tools/peer-call/smoke.test.ts`
(PR #2950). 27 tests / 51 expect() calls / 613ms / all pass.

Status remains `open` (per backlog frontmatter schema enum: open /
closed / superseded-by-B-NNNN / deferred — there is no
`in-progress` value). Acceptance criteria 1 + 2 still pending root-
cause identification when the failure recurs.

# B-0421 — Grok peer-call failure investigation

## What
Expand Down
119 changes: 108 additions & 11 deletions tools/peer-call/grok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,11 @@
const outputFile = parsed.outputFile.length > 0 ? parsed.outputFile : autogenOutputPath("grok");
ensureParentDir(outputFile);

// cursor-agent invocation: capture stdout so we can tee to file +
// emit OUTPUT-FILE marker. stderr passes through inherit. The
// user's prompt is one fixed argument after `--`; cursor-agent
// cursor-agent invocation: capture stdout + stderr so we can tee
// stdout to file, emit OUTPUT-FILE marker, AND write a self-
// documenting failure marker (including stderr) to the output
// file when cursor-agent exits non-zero with empty stdout (B-0421).
// The user's prompt is one fixed argument after `--`; cursor-agent
// does its own argument parsing.
const result = spawnSync(
// eslint-disable-next-line sonarjs/no-os-command-from-path
Expand All @@ -367,31 +369,126 @@
fullPromptResult.value,
],
{
stdio: ["inherit", "pipe", "inherit"],
stdio: ["inherit", "pipe", "pipe"],
maxBuffer: SPAWN_MAX_BUFFER,
Comment thread
AceHack marked this conversation as resolved.
encoding: "buffer",
},
);

const stdoutBuf: Buffer = (result.stdout as Buffer | null) ?? Buffer.alloc(0);
// Tee: write full reply to file AND mirror to our stdout.
const stderrBuf: Buffer = (result.stderr as Buffer | null) ?? Buffer.alloc(0);
// Distinguish spawn-success vs spawn-failure (per Copilot #2949
// round-1): spawnSync returns status: null on ENOENT / signal /
// maxBuffer-exceeded etc. and sets result.error / result.signal.
// Reporting exitCode=1 in those cases loses real diagnostic info.
const spawnError: Error | undefined = result.error;
const spawnSignal: NodeJS.Signals | null = result.signal ?? null;
const rawStatus: number | null = result.status ?? null;
const exitCode = rawStatus ?? 1;
// exitCodeDisplay carries the most-informative value for the
// failure marker: signal name if killed by signal, "null (spawn
// error)" if spawn failed, numeric otherwise.
const exitCodeDisplay =
spawnSignal !== null
? `null (terminated by signal ${spawnSignal})`
: rawStatus === null
? `null (spawn error — see error field)`
: String(rawStatus);

// Mirror captured stderr to our stderr (post-exit; live streaming
// was lost when stderr changed from "inherit" to "pipe" per
// Copilot #2949 round-1; this is a trade-off for capturing stderr
// into the failure marker for output-file-only consumers).
if (stderrBuf.length > 0) {
process.stderr.write(stderrBuf);
}
Comment thread
AceHack marked this conversation as resolved.

// Determine failure case for self-documenting marker:
// - Empty stdout AND (non-zero exit OR spawn error OR signal)
// → write self-documenting failure marker
// - Otherwise → write stdout buffer (success path; preserves
// existing JSON / stream-json contracts)
const isFailureCase =
stdoutBuf.length === 0 &&
(exitCode !== 0 || spawnError !== undefined || spawnSignal !== null);
let fileContent: Buffer;
if (isFailureCase) {
const stderrText =
stderrBuf.length > 0
? stderrBuf.toString("utf8")
: "(empty — cursor-agent produced no stderr)";
const errorMessage = spawnError !== undefined ? spawnError.message : "";
const promptBytes = Buffer.byteLength(fullPromptResult.value, "utf8");
// Emit the marker in a format that matches parsed.outputFormat
// so JSON/stream consumers don't break on Markdown input
// (per Copilot #2949 round-1).
if (parsed.outputFormat === "json") {
const obj = {
error: "cursor-agent failure (B-0421 self-documenting marker)",
exitCode: exitCodeDisplay,
model,
promptBytes,
signal: spawnSignal,
spawnError: errorMessage,
stderr: stderrText,
};
fileContent = Buffer.from(`${JSON.stringify(obj, null, 2)}\n`, "utf8");
} else if (parsed.outputFormat === "stream-json") {
const obj = {
type: "error",
message: "cursor-agent failure (B-0421 self-documenting marker)",
exitCode: exitCodeDisplay,
model,
promptBytes,
signal: spawnSignal,
spawnError: errorMessage,
stderr: stderrText,
};
fileContent = Buffer.from(`${JSON.stringify(obj)}\n`, "utf8");
} else {
// text → Markdown marker for human consumption
const failureMarker =
`# cursor-agent failure (B-0421 self-documenting marker)\n` +
`\n` +
`Exit code: ${exitCodeDisplay}\n` +
`Model: ${model}\n` +
`Prompt size (bytes): ${String(promptBytes)}\n` +
(errorMessage.length > 0 ? `Spawn error: ${errorMessage}\n` : "") +
`\n` +
`## Captured stderr\n` +
`\n` +
`\`\`\`\n${stderrText}${stderrText.endsWith("\n") ? "" : "\n"}\`\`\`\n`;
fileContent = Buffer.from(failureMarker, "utf8");
}
} else {
fileContent = stdoutBuf;
}

try {
writeFileSync(outputFile, stdoutBuf);
writeFileSync(outputFile, fileContent);

Check failure

Code scanning / CodeQL

Insecure temporary file High

Insecure creation of file in
the os temp dir
.
Comment thread
AceHack marked this conversation as resolved.
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`error: failed to write output-file ${outputFile}: ${msg}\n`);
}
process.stdout.write(stdoutBuf);

// Mirror file content to our stdout so shell pipelines see what
// we wrote (preserves prior tee behavior; failure marker is
// visible to callers reading stdout).
process.stdout.write(fileContent);
// Final marker on its own line for `tail -1` recovery.
if (stdoutBuf.length > 0 && !stdoutBuf.subarray(-1).equals(Buffer.from("\n"))) {
if (fileContent.length > 0 && !fileContent.subarray(-1).equals(Buffer.from("\n"))) {
process.stdout.write("\n");
}
process.stdout.write(`OUTPUT-FILE: ${outputFile}\n`);

const exitCode = result.status ?? 1;
if (exitCode !== 0) {
if (exitCode !== 0 || spawnError !== undefined || spawnSignal !== null) {
process.stderr.write("\n");
process.stderr.write(`cursor-agent exited with code ${String(exitCode)}\n`);
process.stderr.write(`cursor-agent exited with code ${exitCodeDisplay}\n`);
if (isFailureCase) {
process.stderr.write(
`cursor-agent produced empty stdout; B-0421 failure marker written to ${outputFile}\n`,
);
}
return 2;
}
return 0;
Expand Down
Loading