Skip to content

Commit eb21471

Browse files
cteclaude
andauthored
feat(cli): include exitCode in command tool_result events (#11820)
Propagate the command exit code through the JSON event emitter so CLI consumers can distinguish between successful and failed command executions without parsing output text. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7118a14 commit eb21471

4 files changed

Lines changed: 28 additions & 7 deletions

File tree

apps/cli/src/agent/__tests__/json-event-emitter-streaming.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ describe("JsonEventEmitter streaming deltas", () => {
302302

303303
emitter.emitCommandOutputChunk("line1\n")
304304
emitter.emitCommandOutputChunk("line1\nline2\n")
305-
emitter.emitCommandOutputDone()
305+
emitter.emitCommandOutputDone(17)
306306

307307
// This completion say is expected from the extension, but should be suppressed
308308
// because we already streamed and completed via commandExecutionStatus.
@@ -339,7 +339,7 @@ describe("JsonEventEmitter streaming deltas", () => {
339339
type: "tool_result",
340340
id: commandId,
341341
subtype: "command",
342-
tool_result: { name: "execute_command" },
342+
tool_result: { name: "execute_command", exitCode: 17 },
343343
done: true,
344344
})
345345
})

apps/cli/src/agent/json-event-emitter.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,12 @@ export class JsonEventEmitter {
310310
return normalized.startsWith(previous) ? normalized.slice(previous.length) : normalized
311311
}
312312

313-
private emitCommandOutputEvent(commandId: number, fullOutput: string | undefined, isDone: boolean): void {
313+
private emitCommandOutputEvent(
314+
commandId: number,
315+
fullOutput: string | undefined,
316+
isDone: boolean,
317+
exitCode?: number,
318+
): void {
314319
if (this.mode === "stream-json") {
315320
const outputDelta = this.computeCommandOutputDelta(commandId, fullOutput)
316321
const event: JsonEvent = {
@@ -324,6 +329,13 @@ export class JsonEventEmitter {
324329
event.tool_result = { name: "execute_command", output: outputDelta }
325330
}
326331

332+
if (isDone && exitCode !== undefined) {
333+
event.tool_result = {
334+
...(event.tool_result ?? { name: "execute_command" }),
335+
exitCode,
336+
}
337+
}
338+
327339
if (isDone) {
328340
event.done = true
329341
this.previousCommandOutputByToolUseId.delete(commandId)
@@ -347,7 +359,11 @@ export class JsonEventEmitter {
347359
type: "tool_result",
348360
id: commandId,
349361
subtype: "command",
350-
tool_result: { name: "execute_command", output: fullOutput },
362+
tool_result: {
363+
name: "execute_command",
364+
output: fullOutput,
365+
...(isDone && exitCode !== undefined ? { exitCode } : {}),
366+
},
351367
...(isDone ? { done: true } : {}),
352368
})
353369

@@ -371,15 +387,15 @@ export class JsonEventEmitter {
371387
this.emitCommandOutputEvent(commandId, outputSnapshot, false)
372388
}
373389

374-
public emitCommandOutputDone(): void {
390+
public emitCommandOutputDone(exitCode?: number): void {
375391
const commandId = this.activeCommandToolUseId
376392
if (commandId === undefined) {
377393
return
378394
}
379395

380396
this.statusDrivenCommandOutputIds.add(commandId)
381397
this.suppressNextCommandOutputSay = true
382-
this.emitCommandOutputEvent(commandId, undefined, true)
398+
this.emitCommandOutputEvent(commandId, undefined, true, exitCode)
383399
}
384400

385401
/**

apps/cli/src/commands/cli/stdin-stream.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,11 @@ export async function runStdinStreamMode({ host, jsonEmitter, setStreamRequestId
419419
parsedStatus.status === "timeout" ||
420420
parsedStatus.status === "fallback"
421421
) {
422-
jsonEmitter.emitCommandOutputDone()
422+
const exitCode =
423+
parsedStatus.status === "exited" && typeof parsedStatus.exitCode === "number"
424+
? parsedStatus.exitCode
425+
: undefined
426+
jsonEmitter.emitCommandOutputDone(exitCode)
423427
return
424428
}
425429

packages/types/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export const rooCliToolResultSchema = z.object({
115115
name: z.string(),
116116
output: z.string().optional(),
117117
error: z.string().optional(),
118+
exitCode: z.number().optional(),
118119
})
119120

120121
export type RooCliToolResult = z.infer<typeof rooCliToolResultSchema>

0 commit comments

Comments
 (0)