fix: exec gstack-brain-sync through bash on Windows

The #1731 fix gated the brain-sync spawns behind
`shell: NEEDS_SHELL_ON_WINDOWS`, but that routes through cmd.exe,
which cannot execute an extensionless bash shebang script. Every
/sync-gbrain run on Windows errored with "'gstack-brain-sync' is not
recognized as an internal or external command" and the stage reported
"brain-sync exited 1".

Invoke the script through bash explicitly on Windows (Git Bash ships
with git, a hard gstack dependency); POSIX keeps the direct shebang
exec. Also surface spawn errors (e.g. bash missing from PATH) in the
stage summary instead of a bare exit code, and update the #1731
tripwire test to pin the new invariant: brain-sync spawns must exec
bash, not rely on shell:true.

Observed on Windows 11, bun 1.3.14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
vaston-viji 2026-08-04 15:02:27 +10:00
parent a3259400a3
commit e525385298
2 changed files with 31 additions and 19 deletions

View File

@ -1145,25 +1145,35 @@ function runBrainSyncPush(args: CliArgs): StageResult {
return { name: "brain-sync", ran: false, ok: true, duration_ms: 0, summary: "skipped (gstack-brain-sync not installed)" };
}
// #1731: gstack-brain-sync is a bash shebang script; Windows can't spawn it
// without a shell, which surfaced as "brain-sync exited undefined".
spawnSync(brainSyncPath, ["--discover-new"], {
stdio: args.quiet ? ["ignore", "ignore", "ignore"] : ["ignore", "inherit", "inherit"],
timeout: 60 * 1000,
shell: NEEDS_SHELL_ON_WINDOWS,
});
const result = spawnSync(brainSyncPath, ["--once"], {
stdio: args.quiet ? ["ignore", "ignore", "ignore"] : ["ignore", "inherit", "inherit"],
timeout: 60 * 1000,
shell: NEEDS_SHELL_ON_WINDOWS,
});
// #1731 gated these spawns behind `shell: NEEDS_SHELL_ON_WINDOWS`, but
// cmd.exe cannot execute an extensionless bash shebang script either — the
// stage errored "'gstack-brain-sync' is not recognized as an internal or
// external command". Invoke the script through bash explicitly on Windows
// (Git Bash ships with git, a hard gstack dependency); POSIX keeps the
// direct shebang exec.
const runBrainSync = (flag: string) =>
spawnSync(
NEEDS_SHELL_ON_WINDOWS ? "bash" : brainSyncPath,
NEEDS_SHELL_ON_WINDOWS ? [brainSyncPath, flag] : [flag],
{
stdio: args.quiet ? ["ignore", "ignore", "ignore"] : ["ignore", "inherit", "inherit"],
timeout: 60 * 1000,
},
);
runBrainSync("--discover-new");
const result = runBrainSync("--once");
return {
name: "brain-sync",
ran: true,
ok: result.status === 0,
duration_ms: Date.now() - t0,
summary: result.status === 0 ? "curated artifacts pushed" : `gstack-brain-sync exited ${result.status}`,
summary:
result.status === 0
? "curated artifacts pushed"
: result.error
? `gstack-brain-sync spawn failed (${result.error.message})`
: `gstack-brain-sync exited ${result.status}`,
};
}

View File

@ -34,12 +34,14 @@ describe("#1731 gbrain spawns carry the Windows shell flag", () => {
});
}
test("orchestrator brain-sync spawns carry the Windows shell flag", () => {
test("orchestrator brain-sync spawns route through bash on Windows", () => {
const src = read("bin/gstack-gbrain-sync.ts");
const brainSyncSpawns = src.match(/spawnSync\(brainSyncPath,/g)?.length ?? 0;
expect(brainSyncSpawns).toBe(2);
// Both spawnSync(brainSyncPath, ...) blocks must include the shell flag.
const withShell = src.match(/spawnSync\(brainSyncPath,[\s\S]*?shell:\s*NEEDS_SHELL_ON_WINDOWS/g)?.length ?? 0;
expect(withShell).toBe(2);
// cmd.exe (shell: true) cannot execute an extensionless bash shebang
// script, so the brain-sync invocations must exec bash explicitly on
// Windows instead of relying on the shell flag.
expect(src).toMatch(/NEEDS_SHELL_ON_WINDOWS \? "bash" : brainSyncPath/);
expect(src).toMatch(/NEEDS_SHELL_ON_WINDOWS \? \[brainSyncPath, flag\] : \[flag\]/);
// And no brain-sync spawn may still route through cmd via shell:true.
expect(src).not.toMatch(/spawnSync\(brainSyncPath,[\s\S]{0,300}?shell:\s*NEEDS_SHELL_ON_WINDOWS/);
});
});