Skip to content

squad loop preflight fails on Windows: execFile('copilot') can't resolve copilot.ps1/.cmd (missing shell flag) #1372

Closed

Description

@duongau

squad loop preflight fails on Windows — execFile('copilot', ['--version']) can't resolve copilot.ps1/.cmd (missing shell flag)

Summary

On Windows, squad loop aborts with "Copilot CLI required" even when the Copilot CLI is installed and working. The preflight check uses execFile('copilot', ['--version']) without shell: true, so it can't resolve the copilot.ps1 / copilot.cmd shim that the CLI ships as on Windows, and false-negatives.

This is an inconsistency regression: the same check was fixed in sibling call sites (doctor, monitor-email, monitor-teams, agent-spawn) but loop was missed.

Where

packages/squad-cli/src/cli/commands/loop.ts:

// ~line 250
async function checkCopilotCli(): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    execFile('copilot', ['--version'], (err) => {   // ← no `shell` option
      if (err) reject(err);
      else resolve();
    });
  });
}

On rejection the run aborts (~line 322):

fatal('Copilot CLI required. Install from https://cli.github.com/ and run `gh extension install github/gh-copilot`');

Root cause

On Windows the Copilot CLI is not a copilot.exe — it's installed as a copilot.ps1 / copilot.cmd shim. Node's child_process.execFile/spawn does not consult PATHEXT unless shell: true, so a bare execFile('copilot', ...) looks only for an exact-named executable, fails with ENOENT, and the preflight concludes the CLI is missing. The loop then exits in ~0.5s having done no work.

This is already fixed elsewhere — loop was just missed

Call site Windows-safe?
commands/loop.ts checkCopilotCli ❌ execFile('copilot', ['--version'], cb) — no shell
commands/doctor.ts ✅ { shell: true, timeout: 5000 }
commands/watch/capabilities/monitor-email.ts ✅ { shell: true, timeout: 5000 }
commands/watch/capabilities/monitor-teams.ts ✅ { shell: true, timeout: 5000 }
commands/watch/agent-spawn.ts resolveCopilotCmd ✅ { shell: IS_WINDOWS } + falls back to gh copilot

The CHANGELOG even records the fix wave: "Fix Windows spawn EINVAL in watch capabilities by adding shell: true to execFile() calls in monitor-teams, monitor-email, retro, and decision-hygiene… (closes #920, #880, #1107)" — but loop (and anything routing through its preflight) wasn't included.

Repro

On Windows, with the standalone copilot CLI installed as copilot.ps1 on PATH (verify: Get-Command copilot shows an ExternalScript/Application .ps1/.cmd, not a .exe):

cd <a squad-initialized repo>
squad loop --file loop.md

Expected: the loop runs the configured agent.
Actual: Copilot CLI required. Install from https://cli.github.com/ … and immediate exit — even though copilot --version works in the same shell.

Suggested fix

Mirror the sibling sites — add the shell flag (platform-scoped) to checkCopilotCli:

execFile('copilot', ['--version'], { shell: process.platform === 'win32', timeout: 5000 }, (err) => {
  if (err) reject(err);
  else resolve();
});

Even better: reuse resolveCopilotCmd() from watch/agent-spawn.ts as the single source of truth — it already handles shell: IS_WINDOWS and falls back to gh copilot, so loop would also work for users who only have the gh extension rather than the standalone CLI. Worth auditing triage / any other command that preflights copilot the same way.

Environment

  • OS: Windows
  • Copilot CLI: standalone, installed as copilot.ps1 on PATH
  • Symptom reproduced and root-caused while building a Windows integration on top of squad loop; worked around downstream by spawning copilot directly via pwsh -NoProfile -File copilot.ps1.

Activity

  1. duongau commented on Jun 23, 2026

    @duongau
    ContributorAuthor

    Independent corroboration from microsoft/conductor

    Flagging that Microsoft's Conductor project hit this exact Windows launcher bug. Their README's Windows/Development section documents it verbatim:

    "Windows subprocess cannot resolve .bat/.ps1 wrappers by name alone. If you see [WinError 2] The system cannot find the file specified when running workflows, set the full path to the Copilot CLI: $env:COPILOT_CLI_PATH = "...\copilot.cmd"."

    Same root cause as the squad loop preflight failure here: a no-shell execFile/subprocess call to bare copilot can't resolve the Windows .ps1/.cmd shim via PATHEXT. Conductor's mitigation is a COPILOT_CLI_PATH env-var override honored by the Copilot SDK.

    Two suggestions for a fix:

    1. Resolve the actual launcher file on PATH (copilot.ps1 -> pwsh -NoProfile -File, copilot.cmd/.bat -> cmd /c) and invoke it directly with shell: false, rather than relying on PATHEXT. Note: keep shell: false so a large multi-line -p prompt argument isn't mangled by the shell.
    2. Honor a COPILOT_CLI_PATH override as an escape hatch for nonstandard installs (matching the Copilot SDK / Conductor convention).

    Given two separate Microsoft projects independently hitting this on Windows, it'd be great to prioritize. Happy to send a PR if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triagedIssue has been triagedwave:1-nextWave 1 — immediate next sprint

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions