Skip to content

Commit 5e72dc8

Browse files
committed
fix(exec): keep framework verb out of backtick-wrapped failure message (#97319)
1 parent 830467b commit 5e72dc8

4 files changed

Lines changed: 90 additions & 3 deletions

File tree

src/agents/embedded-agent-runner/run/payloads.errors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ describe("buildEmbeddedRunPayloads", () => {
781781
});
782782

783783
expectSinglePayloadSummary(payloads, {
784-
text: "⚠️ 🛠️ `show matrix-progress-@room-@alice:matrix-qa.test-!room:matrix-qa.test.txt (workspace)` failed",
784+
text: "⚠️ 🛠️ `matrix-progress-@room-@alice:matrix-qa.test-!room:matrix-qa.test.txt (workspace)` failed",
785785
isError: true,
786786
});
787787
});

src/agents/embedded-agent-runner/run/payloads.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
extractAssistantThinking,
4242
extractAssistantVisibleText,
4343
} from "../../embedded-agent-utils.js";
44+
import { stripLeadingExecDisplayVerb } from "../../tool-display-exec.js";
4445
import { isExecLikeToolName, type ToolErrorSummary } from "../../tool-error-summary.js";
4546
import { isLikelyMutatingToolName } from "../../tool-mutation.js";
4647

@@ -550,9 +551,18 @@ export function buildEmbeddedRunPayloads(params: {
550551
// Surface mutating failures unless the assistant explicitly acknowledged the failed action.
551552
// Otherwise, keep the previous behavior and only surface non-recoverable failures when no reply exists.
552553
if (warningPolicy.showWarning) {
554+
// For exec/bash failures, strip the leading framework verb from the meta so the
555+
// backtick-wrapped text is exactly what was run, not `run <command>`. Otherwise the
556+
// verb adjacent to the command in the rendered warning is indistinguishable from an
557+
// agent-typed `run <command>` invocation (issue #97319).
558+
const meta = params.lastToolError.meta;
559+
const displayMeta =
560+
isExecLikeToolName(params.lastToolError.toolName) && meta
561+
? stripLeadingExecDisplayVerb(meta)
562+
: meta;
553563
const toolSummary = formatToolAggregate(
554564
params.lastToolError.toolName,
555-
params.lastToolError.meta ? [params.lastToolError.meta] : undefined,
565+
displayMeta ? [displayMeta] : undefined,
556566
{ markdown: useMarkdown },
557567
);
558568
const errorSuffix =

src/agents/tool-display-exec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,55 @@ function summarizeKnownExec(words: string[]): string {
287287
return /^[A-Za-z0-9._/-]+$/.test(arg) ? `run ${bin} ${arg}` : `run ${bin}`;
288288
}
289289

290+
// Known display verbs that summarizeKnownExec emits as the first token of an exec label.
291+
// Used to split the framework action from the literal command subject so progress and
292+
// failure surfaces do not backtick-wrap the verb together with the command (issue #97319).
293+
const EXEC_DISPLAY_VERBS: ReadonlySet<string> = new Set([
294+
"run",
295+
"check",
296+
"view",
297+
"show",
298+
"list",
299+
"switch",
300+
"create",
301+
"pull",
302+
"push",
303+
"fetch",
304+
"merge",
305+
"rebase",
306+
"stage",
307+
"restore",
308+
"reset",
309+
"stash",
310+
"find",
311+
"search",
312+
"print",
313+
"copy",
314+
"move",
315+
"remove",
316+
"install",
317+
"start",
318+
]);
319+
320+
/**
321+
* Strips a leading exec display verb (e.g. "run" in "run python3 /path") so the
322+
* framework action label is not backtick-wrapped together with the literal command.
323+
*
324+
* Returns the original meta when no known exec display verb prefix is present, so
325+
* raw shell command text passes through unchanged.
326+
*/
327+
export function stripLeadingExecDisplayVerb(meta: string): string {
328+
const match = meta.match(/^(\S+)\s+(\S.*)$/);
329+
if (!match) {
330+
return meta;
331+
}
332+
const verb = match[1];
333+
if (!EXEC_DISPLAY_VERBS.has(verb)) {
334+
return meta;
335+
}
336+
return match[2];
337+
}
338+
290339
function summarizePipeline(stage: string): string {
291340
const pipeline = splitTopLevelPipes(stage);
292341
if (pipeline.length > 1) {

src/agents/tool-display.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
import { describe, expect, it } from "vitest";
66
import { resolveToolSearchCodeDisplayTarget } from "./tool-display-common.js";
7-
import { resolveExecDetail } from "./tool-display-exec.js";
7+
import { resolveExecDetail, stripLeadingExecDisplayVerb } from "./tool-display-exec.js";
88
import { formatToolDetail, formatToolSummary, resolveToolDisplay } from "./tool-display.js";
99

1010
describe("tool display details", () => {
@@ -586,6 +586,34 @@ describe("compactRawCommand middle truncation", () => {
586586
});
587587
});
588588

589+
describe("stripLeadingExecDisplayVerb", () => {
590+
it("strips a known exec display verb so it stays outside backtick-wrapped command text", () => {
591+
expect(stripLeadingExecDisplayVerb("run python3 /path/to/script.py")).toBe(
592+
"python3 /path/to/script.py",
593+
);
594+
expect(stripLeadingExecDisplayVerb("check git status")).toBe("git status");
595+
expect(stripLeadingExecDisplayVerb("show some-file.txt (workspace)")).toBe(
596+
"some-file.txt (workspace)",
597+
);
598+
expect(stripLeadingExecDisplayVerb('find files named "x" in /path')).toBe(
599+
'files named "x" in /path',
600+
);
601+
});
602+
603+
it("returns the meta unchanged when the first token is not a known exec display verb", () => {
604+
// `cd` is a real shell builtin, not a summary verb, so a raw `cd ~/dir && ls` meta survives.
605+
expect(stripLeadingExecDisplayVerb("cd ~/dir && ls")).toBe("cd ~/dir && ls");
606+
expect(stripLeadingExecDisplayVerb("git status")).toBe("git status");
607+
expect(stripLeadingExecDisplayVerb("npm install")).toBe("npm install");
608+
});
609+
610+
it("returns the meta unchanged for single-token or empty input", () => {
611+
expect(stripLeadingExecDisplayVerb("run")).toBe("run");
612+
expect(stripLeadingExecDisplayVerb("")).toBe("");
613+
expect(stripLeadingExecDisplayVerb(" ")).toBe(" ");
614+
});
615+
});
616+
589617
describe("coerceDisplayValue middle truncation", () => {
590618
it("preserves start and end of long string values", () => {
591619
const longPath =

0 commit comments

Comments
 (0)