Skip to content

Commit 6c87988

Browse files
committed
refactor(agents): improve command argument handling and detection
1 parent 63f4c7a commit 6c87988

9 files changed

Lines changed: 69 additions & 40 deletions

File tree

src/supervisor/agents/claude/index.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,35 @@ export function createClaudeAdapter(): AgentAdapter {
108108
defaultOneShotModel: "haiku",
109109
buildOneShotCommand(model, effort, prompt) {
110110
if (!prompt) return undefined;
111-
const args = ["-p", prompt, "--model", model];
111+
// --no-session-persistence keeps title/commit/PR-summary calls out of
112+
// the `/resume` picker. --fallback-model auto-degrades to Haiku if the
113+
// primary is overloaded so async title generation does not silently
114+
// fail when the API throttles.
115+
const args = [
116+
"-p",
117+
prompt,
118+
"--model",
119+
model,
120+
"--fallback-model",
121+
"haiku",
122+
"--no-session-persistence",
123+
];
112124
if (effort) {
113125
args.push("--effort", effort);
114126
}
115127
return { command: "claude", args, stdin: "" };
116128
},
117129
buildContextExtractionCommand(sessionRef, _location, model) {
118-
const args = ["-p", "--resume", sessionRef.providerSessionId, "--model", model ?? "haiku"];
130+
// The resumed session is read-only here; --no-session-persistence
131+
// prevents the extraction turn from being written back to disk.
132+
const args = [
133+
"-p",
134+
"--resume",
135+
sessionRef.providerSessionId,
136+
"--model",
137+
model ?? "haiku",
138+
"--no-session-persistence",
139+
];
119140
return { command: "claude", args };
120141
},
121142
};

src/supervisor/agents/commandBuilders.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,17 @@ describe("agent command builders", () => {
273273
createClaudeAdapter().buildOneShotCommand?.("haiku", "low", "Summarize this diff"),
274274
).toEqual({
275275
command: "claude",
276-
args: ["-p", "Summarize this diff", "--model", "haiku", "--effort", "low"],
276+
args: [
277+
"-p",
278+
"Summarize this diff",
279+
"--model",
280+
"haiku",
281+
"--fallback-model",
282+
"haiku",
283+
"--no-session-persistence",
284+
"--effort",
285+
"low",
286+
],
277287
stdin: "",
278288
});
279289

@@ -362,7 +372,7 @@ describe("agent command builders", () => {
362372
);
363373

364374
it.skipIf(process.platform !== "win32")(
365-
"prefixes the initial Copilot interactive prompt with /plan in plan mode",
375+
"passes --plan and an unmodified initial prompt when launching in plan mode",
366376
() => {
367377
const spec = launch(
368378
createCopilotAdapter(),
@@ -373,9 +383,10 @@ describe("agent command builders", () => {
373383
const { cmd, cmdArgs } = parseWindowsSpec(spec);
374384

375385
expect(cmd).toBe("copilot");
386+
expect(cmdArgs).toContain("--plan");
376387
expect(cmdArgs).toContain("-i");
377-
expect(cmdArgs).toContain("/plan hi");
378-
expect(cmdArgs).not.toContain("hi");
388+
expect(cmdArgs).toContain("hi");
389+
expect(cmdArgs).not.toContain("/plan hi");
379390
},
380391
);
381392

src/supervisor/agents/copilot/argv.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,13 @@
11
import type { ThreadConfig } from "@/shared/contracts";
22
import { sanitizeModelName } from "./terminal";
33

4-
export function formatCopilotInteractivePrompt(prompt: string, config?: ThreadConfig): string {
5-
if (config?.mode !== "plan") {
6-
return prompt;
7-
}
8-
9-
const trimmed = prompt.trimStart();
10-
if (trimmed.startsWith("/")) {
11-
return prompt;
12-
}
13-
14-
return `/plan ${prompt}`;
15-
}
16-
174
export function buildCopilotArgs(
185
config: ThreadConfig,
196
prompt: string,
207
sessionId: string,
218
_launchOptions?: { suppressResumeConfigOverrides?: boolean },
229
): string[] {
2310
const args = [`--resume=${sessionId}`, "--allow-all-paths"];
24-
const formattedPrompt = formatCopilotInteractivePrompt(prompt, config);
2511

2612
// Copilot's TUI only reflects the selected model/effort when the resume
2713
// command also carries those flags, even if ACP already applied them.
@@ -35,11 +21,14 @@ export function buildCopilotArgs(
3521
if (config.effort) {
3622
args.push("--effort", config.effort);
3723
}
24+
if (config.mode === "plan") {
25+
args.push("--plan");
26+
}
3827
if (config.approvalPolicy === "never") {
3928
args.push("--yolo");
4029
}
41-
if (formattedPrompt.trim().length > 0) {
42-
args.push("-i", formattedPrompt);
30+
if (prompt.trim().length > 0) {
31+
args.push("-i", prompt);
4332
}
4433

4534
return args;

src/supervisor/agents/copilot/copilot.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,18 +398,18 @@ describe("createCopilotAdapter", () => {
398398
},
399399
);
400400

401-
it("prefixes plan-mode interactive prompts with /plan", () => {
401+
it("passes plan-mode prompts through unchanged (mode is set via --plan at launch)", () => {
402402
const adapter = createCopilotAdapter();
403403

404404
expect(
405405
adapter.buildDirectInput?.("fix bug", undefined, {
406406
model: "gpt-5.4",
407407
mode: "plan",
408408
}),
409-
).toEqual(["/plan fix bug", "@wait:40", "\r"]);
409+
).toEqual(["fix bug", "@wait:40", "\r"]);
410410
});
411411

412-
it("does not double-prefix existing slash commands in plan mode", () => {
412+
it("preserves user-typed slash commands verbatim", () => {
413413
const adapter = createCopilotAdapter();
414414

415415
expect(

src/supervisor/agents/copilot/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from "../base";
1414
import { resolveAgentBinaryPath } from "../binaryResolver";
1515
import { resolveInstallNodePath, warnIfPluginManifestMissing } from "../plugin/installerBase";
16-
import { buildCopilotArgs, formatCopilotInteractivePrompt } from "./argv";
16+
import { buildCopilotArgs } from "./argv";
1717
import { buildCopilotCommand, copilotDefaultCapabilities, copilotDetectionSpec } from "./detection";
1818
import {
1919
installCopilotPlugin,
@@ -118,8 +118,10 @@ export function createCopilotAdapter(): AgentAdapter {
118118
createInitialSessionRef() {
119119
return undefined;
120120
},
121-
buildDirectInput(prompt, _segments, config) {
122-
return [formatCopilotInteractivePrompt(prompt, config), "@wait:40", "\r"];
121+
buildDirectInput(prompt) {
122+
// Mode is set at launch via --plan / --mode. The TUI persists the
123+
// selection across turns, so subsequent prompts pass through as-is.
124+
return [prompt, "@wait:40", "\r"];
123125
},
124126
formatPromptSegments(segments: PromptSegment[]) {
125127
const attachments = segments.filter((segment) => segment.kind === "attachment");

src/supervisor/agents/gemini/argv.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export function buildGeminiArgs(
88
): string[] {
99
const args: string[] = [];
1010

11+
// Gemini emits "Skipping project agents due to untrusted folder..." on
12+
// stdout when the workspace is not pre-trusted. In --acp mode that string
13+
// collides with JSON-RPC frames and breaks the stream parser. --skip-trust
14+
// suppresses the prompt for this session (replaces the older
15+
// GEMINI_CLI_TRUST_WORKSPACE=true env var, which only worked through env
16+
// inheritance and was easy to lose across spawn wrappers).
17+
args.push("--skip-trust");
18+
1119
if (resumeSessionId) {
1220
args.push("--resume", resumeSessionId);
1321
} else if (assignedSessionId) {

src/supervisor/agents/gemini/detection.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe("geminiDetectionSpec", () => {
5151

5252
expect(buildAgentCommandMock).toHaveBeenCalledWith(location, "/Users/demo/.local/bin/gemini", [
5353
"--acp",
54+
"--skip-trust",
5455
]);
5556
expect(probeAcpCapabilitiesMock).toHaveBeenCalledWith(
5657
"/bin/zsh",

src/supervisor/agents/gemini/detection.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,18 @@ export const geminiDetectionSpec: DetectionSpec = {
136136
// Bypass Gemini's folder-trust check during the probe so the AgentRegistry
137137
// doesn't emit "Skipping project agents..." onto stdout, which can collide
138138
// with JSON-RPC frames and break the ACP parser.
139-
const trustEnv = { GEMINI_CLI_TRUST_WORKSPACE: "true" };
139+
const probeArgs = ["--acp", "--skip-trust"];
140140
const probeCmd =
141141
ctx.location.kind === "wsl"
142-
? buildAgentCommand(ctx.location, "gemini", ["--acp"], ctx.executablePath, trustEnv)
143-
: buildAgentCommand(ctx.location, ctx.executablePath, ["--acp"]);
142+
? buildAgentCommand(ctx.location, "gemini", probeArgs, ctx.executablePath)
143+
: buildAgentCommand(ctx.location, ctx.executablePath, probeArgs);
144144
const probeCwd = ctx.location.kind === "wsl" ? "/tmp" : homedir();
145145
const probeResult = await probeAcpCapabilities(probeCmd.command, probeCmd.args, probeCwd, {
146146
timeoutMs: 15_000,
147147
label:
148148
ctx.location.kind === "wsl"
149149
? `gemini:wsl:${ctx.location.distro}`
150150
: `gemini:${ctx.location.kind}`,
151-
...(ctx.location.kind === "wsl" ? {} : { env: trustEnv }),
152151
});
153152
if (!probeResult) return undefined;
154153
const modelTokens = new Map<string, number>();

src/supervisor/agents/gemini/index.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@ export function createGeminiAdapter(): AgentAdapter {
4444
get capabilities() {
4545
return capabilities;
4646
},
47-
// GEMINI_CLI_TRUST_WORKSPACE=true bypasses Gemini's folder-trust check.
48-
// Without it, the AgentRegistry emits "Skipping project agents due to
49-
// untrusted folder..." onto stdout, which can collide with JSON-RPC
50-
// frames in --acp mode and break the ACP stream parser.
47+
// Workspace trust is now suppressed via --skip-trust on every gemini
48+
// invocation (see buildGeminiArgs and the --acp launch below). WSL still
49+
// needs BROWSER=/bin/true so the OAuth flow does not try to xdg-open a
50+
// browser inside the distro and hang the PTY.
5151
spawnEnv: {
52-
native: { GEMINI_CLI_TRUST_WORKSPACE: "true" },
53-
wsl: { BROWSER: "/bin/true", GEMINI_CLI_TRUST_WORKSPACE: "true" },
52+
wsl: { BROWSER: "/bin/true" },
5453
},
5554
pluginId: "lightcode-status@gemini",
5655
pluginVersion: GEMINI_PLUGIN_VERSION,
@@ -108,9 +107,8 @@ export function createGeminiAdapter(): AgentAdapter {
108107
const command = buildAgentCommand(
109108
input.projectLocation,
110109
"gemini",
111-
["--acp"],
110+
["--acp", "--skip-trust"],
112111
resolveAgentBinaryPath(input.projectLocation, "gemini"),
113-
{ GEMINI_CLI_TRUST_WORKSPACE: "true" },
114112
);
115113
return createAcpStructuredSession(command, input);
116114
},

0 commit comments

Comments
 (0)