Skip to content

fix(windows): resolveSlug silently returns "unknown", sending all decision data to projects/unknown - #2380

Open
fongwc wants to merge 1 commit into
garrytan:mainfrom
fongwc:fix/windows-spawn-bash-scripts
Open

fix(windows): resolveSlug silently returns "unknown", sending all decision data to projects/unknown#2380
fongwc wants to merge 1 commit into
garrytan:mainfrom
fongwc:fix/windows-spawn-bash-scripts

Conversation

@fongwc

@fongwc fongwc commented Jul 29, 2026

Copy link
Copy Markdown

Summary

On Windows, resolveSlug() in lib/bin-context.ts always returns "unknown", so every bun-based bin that resolves a project slug reads and writes ~/.gstack/projects/unknown/ instead of the real project.

The visible effect is that /decision data is invisible: gstack-decision-log writes into a shared unknown bucket, gstack-decision-search returns [] for every project, and the Context Recovery block in every skill preamble never surfaces a decision, because it looks for decisions.active.json under the correct slug.

The failure is silent. Exit code 0, empty result, nothing in any log. I only found it after noticing a project's decisions had been accumulating in ~/.gstack/projects/unknown/ for weeks.

Root cause

bin/gstack-slug is a #!/usr/bin/env bash script with no file extension. Windows has no shebang support, so spawning it directly fails and r.stdout is null, which falls through to the "unknown" default:

export function resolveSlug(slugBinPath: string): string {
  const r = spawnSync(slugBinPath, { encoding: "utf-8" });   // ENOENT on Windows
  const m = (r.stdout || "").match(/^SLUG=(.+)$/m);          // null -> no match
  return m ? m[1].trim() : "unknown";                        // always "unknown"
}

Measured on the real path:

spawnSync(bin)                  -> status undefined, error ENOENT
                                   uv_spawn '.../bin/gstack-slug'
spawnSync(bin, {shell: true})   -> status 1
                                   '.../gstack-slug' is not recognized as an
                                   internal or external command
spawnSync("bash", [bin])        -> status 0, "SLUG=my-project\nBRANCH=master"

Relationship to #1731 and #2356

This is the same spawn bug class as the second half of #2356, at a call site that issue does not list.

It is also the same class #1731 already fixed for gbrain, via NEEDS_SHELL_ON_WINDOWS (shell: true). Worth being explicit about why that remedy is not reused here: shell: true is correct for gbrain because gbrain ships a .cmd shim on Windows, which cmd.exe can run. gstack's own bins have no shim, so shell: true routes to cmd.exe and fails, as measured above. Only spawning bash works.

The fix

Adds spawnBashBin() and routes resolveSlug through it. The direct spawn is attempted first, so POSIX behaviour is byte-for-byte unchanged; the bash fallback only runs on the path that is currently broken.

Verification

Windows 11 (10.0.26200), bun 1.3.13, Git Bash, global git install at ~/.claude/skills/gstack, upstream a3259400.

before: gstack-decision-search --json  ->  []      (2 decisions present on disk)
after:  gstack-decision-search --json  ->  2 decisions

Correct from both the repo root and a subdirectory with a different git remote, i.e. the slug now tracks the real project rather than collapsing to unknown.

Existing tests, run before and after the change on Windows:

test/gbrain-spawn-windows-shell.test.ts
test/gstack-decision.test.ts
test/gstack-decision-bins.test.ts
test/bin-windows-bun-import-paths.test.ts
test/gstack-slug-sanitize.test.ts

  baseline (clean a3259400):  39 pass, 18 fail
  with this change:           39 pass, 18 fail

Identical, so no regressions. The 18 pre-existing failures look like separate Windows path bugs inside the tests themselves (for example gstack-slug-sanitize builds a cache key containing C:\... and then tries to open it as a nested path). Not touched here.

Deliberately not included

bin/gstack-gbrain-sync.ts:1150,1155 spawns gstack-brain-sync, which is also an extensionless bash script, using shell: NEEDS_SHELL_ON_WINDOWS. By the measurement above that spawn cannot be working on Windows either, despite the #1731 comment above it.

I left it alone because test/gbrain-spawn-windows-shell.test.ts explicitly pins that call site to exactly two spawnSync(brainSyncPath, ...) occurrences carrying shell: NEEDS_SHELL_ON_WINDOWS. Changing it means overturning an invariant a maintainer wrote deliberately, and I have no gbrain install here to verify the end-to-end path. Happy to follow up in a separate PR, or file it as an issue, whichever you prefer.

…rojects/unknown

lib/bin-context.ts spawns the `gstack-slug` bash script directly. Windows has
no shebang support, so spawnSync fails with ENOENT, stdout is null, and the
slug silently falls back to "unknown".

Every bun-based bin that resolves a slug this way then reads and writes
~/.gstack/projects/unknown/ instead of the real project. In practice that means
gstack-decision-log writes decisions somewhere gstack-decision-search on another
project can also see, and the Context Recovery block in every skill preamble
never surfaces them, because it checks for decisions.active.json under the
correct slug. The failure is silent: exit 0, empty results, nothing in any log.

Same root cause as the spawn half of garrytan#2356, and the same class the garrytan#1731 fix
addressed for gbrain. That fix used `shell: true`, which is right for gbrain
because gbrain ships a .cmd shim on Windows. gstack's own bins are extensionless
bash scripts, so cmd.exe answers "is not recognized as an internal or external
command" and only `spawnSync("bash", [script])` starts them:

  spawnSync(bin)                  -> ENOENT
  spawnSync(bin, {shell: true})   -> not recognized as an internal or external command
  spawnSync("bash", [bin])        -> status 0

Adds spawnBashBin() and routes resolveSlug through it. The direct spawn is still
attempted first, so POSIX behaviour is byte-for-byte unchanged and the bash
fallback only runs on the path that is currently broken.

Verified on Windows 11, bun 1.3.13, Git Bash, global git install at
~/.claude/skills/gstack:

  before: gstack-decision-search --json -> []   (2 decisions on disk)
  after:  gstack-decision-search --json -> 2 decisions, from both the repo root
          and a subdirectory with a different git remote

Test suite unchanged: test/gbrain-spawn-windows-shell.test.ts,
gstack-decision.test.ts, gstack-decision-bins.test.ts,
bin-windows-bun-import-paths.test.ts and gstack-slug-sanitize.test.ts report
39 pass / 18 fail both before and after this commit on Windows at a325940,
so this introduces no regressions. Those 18 pre-existing failures look like
separate Windows path bugs in the tests themselves and are not touched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant