From 24d6398bea25ef78ce13fb8fa9a4d915e20d9f26 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Tue, 20 Oct 2026 22:17:16 +0200 Subject: [PATCH] feat(prompt): state the command syntax for cmd.exe and PowerShell The system prompt only named the shell path, so rules and habits written for bash silently ran with cmd semantics on Windows. Add a Shell Syntax line for cmd.exe and PowerShell (when execute_command is available) covering command chaining, variables and exit codes. Refs #2083 Co-Authored-By: Claude Sonnet 5.5 --- .../sections/__tests__/system-info.spec.ts | 24 +++++++++++++++ src/core/prompts/sections/system-info.ts | 10 ++++++- src/utils/__tests__/shellDialect.spec.ts | 23 ++++++++++++++ src/utils/shellDialect.ts | 30 +++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/utils/__tests__/shellDialect.spec.ts create mode 100644 src/utils/shellDialect.ts diff --git a/src/core/prompts/sections/__tests__/system-info.spec.ts b/src/core/prompts/sections/__tests__/system-info.spec.ts index 7c3b53c426..421c3a23b6 100644 --- a/src/core/prompts/sections/__tests__/system-info.spec.ts +++ b/src/core/prompts/sections/__tests__/system-info.spec.ts @@ -10,6 +10,7 @@ vi.mock("../../../../utils/shell", () => ({ })) import { getSystemInfoSection } from "../system-info" +import { getShell } from "../../../../utils/shell" import osName from "os-name" const mockOsName = osName as unknown as ReturnType @@ -101,4 +102,27 @@ describe("getSystemInfoSection", () => { // sentinel string) breaks this exact join. expect(result).toContain("default directory for all tool operations. When the user initially gives you a task") }) + + describe("shell syntax", () => { + const cmdPath = "C:\\Windows\\System32\\cmd.exe" + + it("states the dialect for cmd.exe when execute_command is available", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + const result = getSystemInfoSection(mockCwd, policyFor()) + + expect(result).toContain(`Default Shell: ${cmdPath} +Shell Syntax: Commands run through Windows cmd.exe`) + }) + + it("omits the dialect without execute_command", () => { + vi.mocked(getShell).mockReturnValueOnce(cmdPath) + + expect(getSystemInfoSection(mockCwd, policyFor(false))).not.toContain("Shell Syntax") + }) + + it("omits the dialect for POSIX shells", () => { + expect(getSystemInfoSection(mockCwd, policyFor())).not.toContain("Shell Syntax") + }) + }) }) diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 003c555d75..6e7067bf7c 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -2,6 +2,7 @@ import os from "os" import osName from "os-name" import { getShell } from "../../../utils/shell" +import { describeShellDialect } from "../../../utils/shellDialect" import type { EffectiveToolPolicy } from "../tools/effective-tool-policy" @@ -33,12 +34,19 @@ export function getSystemInfoSection(cwd: string, policy: EffectiveToolPolicy): ? " New terminals will be created in the current workspace directory, however if you change directories in a terminal it will then have a different working directory; changing directories in a terminal does not modify the workspace directory, because you do not have access to change the workspace directory." : "" + const shell = getShell() + const shellDialect = executeCommandAvailable ? describeShellDialect(shell) : undefined + const shellDialectLine = shellDialect + ? ` +Shell Syntax: ${shellDialect}` + : "" + const details = `==== SYSTEM INFORMATION Operating System: ${osInfo} -Default Shell: ${getShell()} +Default Shell: ${shell}${shellDialectLine} Home Directory: ${os.homedir().toPosix()} Current Workspace Directory: ${cwd.toPosix()} diff --git a/src/utils/__tests__/shellDialect.spec.ts b/src/utils/__tests__/shellDialect.spec.ts new file mode 100644 index 0000000000..c5db9ee722 --- /dev/null +++ b/src/utils/__tests__/shellDialect.spec.ts @@ -0,0 +1,23 @@ +import { describeShellDialect } from "../shellDialect" + +describe("describeShellDialect", () => { + it.each([ + { shell: "C:\\Windows\\System32\\cmd.exe", expected: "cmd.exe" }, + { shell: "C:\\Windows\\System32\\CMD.EXE", expected: "cmd.exe" }, + { shell: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", expected: "PowerShell" }, + { shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", expected: "PowerShell" }, + ])("names the dialect for $shell", ({ shell, expected }) => { + expect(describeShellDialect(shell)).toContain(expected) + }) + + it("warns that ';' does not chain commands in cmd.exe", () => { + expect(describeShellDialect("C:\\Windows\\System32\\cmd.exe")).toContain("`;` is not a separator") + }) + + it.each(["/bin/bash", "/bin/zsh", "C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Windows\\System32\\wsl.exe"])( + "adds nothing for POSIX shell %s", + (shell) => { + expect(describeShellDialect(shell)).toBeUndefined() + }, + ) +}) diff --git a/src/utils/shellDialect.ts b/src/utils/shellDialect.ts new file mode 100644 index 0000000000..048f62cd35 --- /dev/null +++ b/src/utils/shellDialect.ts @@ -0,0 +1,30 @@ +/** + * Describes the command syntax of Windows-native shells for the system prompt. + * + * Rules and habits written for bash silently do the wrong thing under these shells + * (e.g. `;` is not a command separator in cmd.exe), so the model is told which + * dialect it is writing for. Returns undefined for POSIX shells, where the shell + * path already names the dialect. + */ +export function describeShellDialect(shellPath: string): string | undefined { + // path.basename follows the host platform; split on both separators so Windows paths work anywhere. + const name = (shellPath.split(/[\\/]/).pop() ?? "").toLowerCase() + + if (name === "cmd.exe" || name === "cmd") { + return ( + "Commands run through Windows cmd.exe, not bash. Use cmd syntax: `&` or `&&` to chain commands " + + "(`;` is not a separator), `%VAR%` for variables, `set X=Y`, `%ERRORLEVEL%` for exit codes, " + + "and `dir`, `type`, `findstr`. POSIX tools may be missing or shadowed by other binaries on PATH." + ) + } + + if (name === "powershell.exe" || name === "pwsh.exe" || name === "powershell" || name === "pwsh") { + return ( + "Commands run through PowerShell, not bash. Use PowerShell syntax: `;` to chain commands, " + + "`$env:VAR` for environment variables, `$LASTEXITCODE` for exit codes, and cmdlets such as " + + "`Get-ChildItem` and `Select-String`." + ) + } + + return undefined +}