Skip to content

On Windows, decision bins silently file everything under slug unknown (resolveSlug spawn fails) #2396

Closed

Description

@wd-crm-bot

Environment

  • Windows 10 (10.0.19045), Git for Windows bash on PATH
  • bun 1.3.14
  • gstack HEAD a325940 (team-mode install)

Bug

lib/bin-context.ts resolveSlug() spawns the gstack-slug helper directly:

const r = spawnSync(slugBinPath, { encoding: "utf-8" });

bin/gstack-slug is a #!/usr/bin/env bash script. Windows cannot exec a shebang script, so the spawn fails and resolveSlug returns its "unknown" fallback:

$ bun -e 'const {spawnSync}=require("child_process"); const r=spawnSync("C:/Users/<user>/.claude/skills/gstack/bin/gstack-slug",{encoding:"utf-8"}); console.log(String(r.error), r.stdout)'
Error: ENOENT: no such file or directory, uv_spawn 'C:/Users/<user>/.claude/skills/gstack/bin/gstack-slug' null

Because only the bun-based bins use resolveSlug (gstack-decision-search, gstack-decision-log), the effect on Windows is:

  • every decision, from every repo on the machine, is written to and read from ~/.gstack/projects/unknown/ — no error, no warning;
  • repo-scoped decisions bleed across unrelated projects (they all share the unknown store, and filterByScope passes scope: "repo" rows unconditionally);
  • the bash-based bins (gstack-learnings-search etc.), which eval "$(gstack-slug)" in bash, resolve the slug correctly — so on Windows the learnings store and the decision store for the same repo live under different slugs, and the decision store is invisible to anything that looks under the real slug.

Observed in the wild before diagnosing: a project whose learnings landed under its real slug while its one logged decision sat in projects/unknown/.

Fix (tested on Windows + does not change POSIX behavior)

Fall back to running the helper through bash when the direct spawn fails. Two Windows-specific gotchas, both measured:

  1. bun's import.meta.dir returns backslash paths, and bash consumes backslashes in arguments — spawnSync("bash", ["C:\\...\\bin/gstack-slug"]) launches bash fine but exits 127. The path must be handed over with forward slashes.
  2. With forward slashes it works: exit 0, SLUG=<owner-repo> parsed as on POSIX.
/** Resolve the project slug via the `gstack-slug` helper (parses `SLUG=...`). */
export function resolveSlug(slugBinPath: string): string {
  let r = spawnSync(slugBinPath, { encoding: "utf-8" });
  if (r.error || !r.stdout) {
    // Windows cannot exec a shebang script directly (uv_spawn ENOENT); re-run it
    // through bash (Git for Windows ships one). Bash eats backslashes in its
    // arguments, so the path must cross over with forward slashes.
    r = spawnSync("bash", [slugBinPath.replace(/\\/g, "/")], { encoding: "utf-8" });
  }
  const m = (r.stdout || "").match(/^SLUG=(.+)$/m);
  return m ? m[1].trim() : "unknown";
}

Verified after patching: gstack-decision-search resolves the real slug and reads ~/.gstack/projects/<owner-repo>/ (proved by moving the unknown/ store aside and observing the decision still returned from the correct dir).

Migration note for affected users

Existing Windows users will have decisions stranded in ~/.gstack/projects/unknown/. After the fix, those files need a one-time move/merge into the real slug dir (append-only JSONL; decisions.active.json can be rebuilt from the log). It may be worth a line in the changelog or a check in gstack-upgrade.

Activity

  1. dmfrede commented on Aug 4, 2026

    @dmfrede
    Contributor

    Corroborating on Windows 11 / git-bash / Node v24.14.0, gstack 1.60.1.0: resolveSlug is unchanged and 216 decisions have accumulated under projects/unknown/ here.

    One addition that isn't in the report, and worth ruling out explicitly because it's the natural first reach: shell: true looks like the smaller fix and is not one.

    spawnSync(path)                error=ENOENT  status=null  stdout=undefined
    spawnSync(path,{shell:true})   error=none    status=1     stdout=""
                                   stderr="'C:/.../gstack-slug' is not recognized as an
                                           internal or external command,"
    spawnSync('bash',[path])       error=none    status=0     stdout="SLUG=<owner>-<repo>\n..."
    

    On Windows shell: true routes through cmd.exe, which can't exec a shebang either — so it fixes nothing, and it's actively worse for diagnosis: it converts the hard ENOENT into no r.error at all, status 1, empty stdout. A guard written as if (r.error) would read that as success.

    The if (r.error || !r.stdout) condition in the proposed fix is already correct here, but only because it also checks stdout — worth keeping that second clause if anyone simplifies it later.

    Small note on the reproduction: on Node 24 the failing call gives stdout === undefined rather than "", so !r.stdout is the right test and r.stdout === "" would miss it.

    The cross-store split in point 3 is also visible on disk as duplicate slug directories — ~/.gstack/projects/ here contains both <owner>-<repo> and bare <repo> for five different repos, one pair per repo, depending on which resolver wrote first.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions