From de5998fd491f4526717e40768314f7d20d6ea0f7 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sun, 25 Oct 2026 23:47:58 +0000 Subject: [PATCH] fix(windows): detect Git Bash/MSYS2/Cygwin from OSTYPE, as gstack-doctor does, so uname stubs in setup fixtures stay untouched Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- AGENTS.md | 2 +- CHANGELOG.md | 4 +- autoplan/bin/phase-publication-hook | 2 +- bin/gstack-brain-sync | 2 +- bin/gstack-browser-ensure | 2 +- bin/gstack-codex-probe | 2 +- bin/gstack-codex-session-import | 2 +- bin/gstack-developer-profile | 2 +- bin/gstack-distill-apply | 2 +- bin/gstack-distill-free-text | 2 +- bin/gstack-doctor | 2 +- bin/gstack-doctor-check.sh | 2 +- bin/gstack-gbrain-install | 2 +- bin/gstack-gbrain-source-wireup | 2 +- bin/gstack-hook-check | 2 +- bin/gstack-install-verdict.sh | 2 +- bin/gstack-ios-qa-daemon | 2 +- bin/gstack-ios-qa-mint | 2 +- bin/gstack-ios-qa-regen | 2 +- bin/gstack-learnings-log | 2 +- bin/gstack-learnings-search | 2 +- bin/gstack-memorable | 2 +- bin/gstack-question-log | 2 +- bin/gstack-question-preference | 2 +- bin/gstack-relink | 2 +- bin/gstack-review-log | 2 +- bin/gstack-review-read | 2 +- bin/gstack-settings-hook | 2 +- bin/gstack-specialist-stats | 2 +- bin/gstack-team-init | 2 +- bin/gstack-telemetry-log | 2 +- bin/gstack-timeline-log | 2 +- bin/gstack-timeline-read | 2 +- gstack-upgrade/migrations/v1.86.0.0.sh | 2 +- hosts/claude/hooks/auq-error-fallback-hook | 2 +- hosts/claude/hooks/memorable-user-prompt-hook | 2 +- hosts/claude/hooks/question-log-hook | 2 +- hosts/claude/hooks/question-preference-hook | 2 +- hosts/claude/hooks/timeline-stop-hook | 2 +- lib/bun-isolation.ts | 4 +- plan-ceo-review/bin/mode-handoff-hook | 2 +- scripts/build-cso.sh | 2 +- setup | 2 +- test/bun-config-shell-bins.test.ts | 63 +++++++++---------- test/bun-entry-isolation.test.ts | 2 +- test/bun-isolation-launch.test.ts | 2 +- test/plan-mode-preamble-render.test.ts | 2 +- 47 files changed, 77 insertions(+), 82 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a9747bc36..0c248edda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -303,4 +303,4 @@ Agents poll `eval:bg:*` logs for the `### gstack-detach EXIT= ###` sentine - Every Codex run's verdict goes through `lib/outside-review-result.ts`; its reason codes live in `lib/gate-outcomes.ts`, each with a `docs/troubleshooting.md` anchor. - `test/module-size-ratchet.test.ts` keeps refactored owner modules at or under 800 lines (150 per function) and residual files from growing. - The `claude` CLI binary resolves via `lib/claude-bin.ts` (re-exported from `browse/src/claude-bin.ts` for browse internals; `Bun.which()` + `GSTACK_CLAUDE_BIN` override). Set `GSTACK_CLAUDE_BIN=wsl` plus `GSTACK_CLAUDE_BIN_ARGS='["claude"]'` to run Claude through WSL on Windows. -- Every Bun gstack starts ignores the working directory's `.env` files and `bunfig.toml`: bun shebangs are `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`, skill prose passes `bun --no-env-file --config=/dev/null`, shipped shell files define `GSTACK_BUN_CONFIG` with the line in `test/bun-entry-isolation.test.ts` (`NUL` under MINGW, MSYS and Cygwin, where native Bun cannot open `/dev/null`, #3415) and pass `bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}"`, TypeScript spawns use `bunIsolationArgs()` from `lib/bun-isolation.ts`, and `bun build --compile` adds `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`. `test/bun-entry-isolation.test.ts` enforces all four. +- Every Bun gstack starts ignores the working directory's `.env` files and `bunfig.toml`: bun shebangs are `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`, skill prose passes `bun --no-env-file --config=/dev/null`, shipped shell files define `GSTACK_BUN_CONFIG` with the line in `test/bun-entry-isolation.test.ts` (`NUL` when `OSTYPE` is `msys` or `cygwin`, where native Bun cannot open `/dev/null`, #3415) and pass `bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}"`, TypeScript spawns use `bunIsolationArgs()` from `lib/bun-isolation.ts`, and `bun build --compile` adds `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`. `test/bun-entry-isolation.test.ts` enforces all four. diff --git a/CHANGELOG.md b/CHANGELOG.md index 256d0b9f3..f9dccbd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,12 @@ **Windows: gstack's shell bins stop losing review, question, learning and timeline records when Git Bash path conversion is off (#3415).** Since GSTA-56 every shell bin started Bun with `--config=/dev/null`. Native `bun.exe` cannot open `/dev/null`: it exits 1 with `EINVAL` on Bun 1.3.10, 1.4.2 and 1.4.4 (measured on windows-latest). Git Bash hides this by rewriting the argument to `nul`, but only while MSYS path conversion is on. With it off (`MSYS_NO_PATHCONV` set to any value, even empty; `MSYS2_ARG_CONV_EXCL=*`; or Cygwin), `gstack-review-log`, `gstack-question-log` and `gstack-learnings-log` exited 1 with nothing written, and `gstack-timeline-log` exited 0 with nothing written. ### Fixed -- **40 shipped shell files** (the bash bins, the Claude hook wrappers, `setup`, `scripts/build-cso.sh` and one migration) define `GSTACK_BUN_CONFIG` once (`NUL` under MINGW, MSYS and Cygwin, `/dev/null` elsewhere) and start Bun with `--config="${GSTACK_BUN_CONFIG:-/dev/null}"`. Bun 1.4.2 and later, gstack's minimum, read `NUL` as an empty config whether or not conversion is on. The `:-/dev/null` default fails closed: a call that loses its definition still passes `/dev/null`, so POSIX is unchanged and Windows fails loudly as before, and never reads the repository's `bunfig.toml`. `bin/gstack-codex-probe` is sourced into the caller's shell, so it defines the variable inside the one function that starts Bun and still runs nothing at source time. +- **40 shipped shell files** (the bash bins, the Claude hook wrappers, `setup`, `scripts/build-cso.sh` and one migration) define `GSTACK_BUN_CONFIG` once (`NUL` when `OSTYPE` is `msys` or `cygwin`, as in Git Bash, MSYS2 and Cygwin, the check `gstack-doctor` already uses; `/dev/null` elsewhere) and start Bun with `--config="${GSTACK_BUN_CONFIG:-/dev/null}"`. Bun 1.4.2 and later, gstack's minimum, read `NUL` as an empty config whether or not conversion is on. The `:-/dev/null` default fails closed: a call that loses its definition still passes `/dev/null`, so POSIX is unchanged and Windows fails loudly as before, and never reads the repository's `bunfig.toml`. `bin/gstack-codex-probe` is sourced into the caller's shell, so it defines the variable inside the one function that starts Bun and still runs nothing at source time. - **`bin/gstack-timeline-log`**: invalid input still exits 0 with nothing logged (the hook-path contract). Its validator now exits 3 for invalid input, and any other non-zero exit, which is Bun itself failing, is reported on stderr and returned instead of being skipped as invalid. - Unchanged: bun shebangs and skill prose keep `--config=/dev/null`. A shebang cannot name a per-platform path, and with conversion off their POSIX script paths (`/c/Users/...`) fail in `bun.exe` anyway, so they work wherever they worked before. TypeScript spawns keep `bunIsolationArgs()`, and `scripts/ubicloud/` keeps `/dev/null` because it runs on Linux. ### Tests -- `test/bun-config-shell-bins.test.ts` (new; force-included in the Windows lane): on every platform, the four record bins run through `lib/script-invocation.ts` with conversion off in a repository whose `bunfig.toml` preload and `.env` are hostile. Each writes exactly one record, and neither the preload nor the `.env` state root takes effect. Invalid timeline input still exits 0 silently. On POSIX hosts a simulated Windows bash (uname reports MINGW64, and a `bun` stand-in rejects `/dev/null` with EINVAL as `bun.exe` does) reproduces the bug: on main the four bins write nothing and the timeline control exits 0 (5 failures); with the fix they pass. The controls, with uname reporting Linux, show each bin exits non-zero with nothing written. +- `test/bun-config-shell-bins.test.ts` (new; force-included in the Windows lane): on every platform, the four record bins run through `lib/script-invocation.ts` with conversion off in a repository whose `bunfig.toml` preload and `.env` are hostile. Each writes exactly one record, and neither the preload nor the `.env` state root takes effect. Invalid timeline input still exits 0 silently. On POSIX hosts a simulated Windows bash (`OSTYPE=msys`, and a `bun` stand-in rejects `/dev/null` with EINVAL as `bun.exe` does) reproduces the bug: on main the four bins write nothing and the timeline control exits 0 (5 failures); with the fix they pass. The controls, without `OSTYPE=msys`, show each bin exits non-zero with nothing written. - `test/bun-isolation-launch.test.ts`: the shell-file spelling stays isolated with conversion off, and a win32-only control shows that the literal `--config=/dev/null` fails there with EINVAL. - `test/bun-entry-isolation.test.ts`: shipped shell files must use the fail-closed spelling and define `GSTACK_BUN_CONFIG` with the canonical line before first use. Skill sources keep the literal spelling. - `test/learnings-injection.test.ts`, `browse/test/learnings-injection.test.ts` and `test/setup-hook-canonical-paths.test.ts` now anchor on the new spelling. Their assertions are unchanged. diff --git a/autoplan/bin/phase-publication-hook b/autoplan/bin/phase-publication-hook index f7633d29f..0bde64eb7 100755 --- a/autoplan/bin/phase-publication-hook +++ b/autoplan/bin/phase-publication-hook @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Native Read barrier. A broken install must return deny JSON, never silence. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac _AUTOPLAN_DECIDED='' _autoplan_backstop() { if [ -z "$_AUTOPLAN_DECIDED" ]; then diff --git a/bin/gstack-brain-sync b/bin/gstack-brain-sync index 8886a84bc..de78eed05 100755 --- a/bin/gstack-brain-sync +++ b/bin/gstack-brain-sync @@ -77,7 +77,7 @@ BASH_COMPAT=50 set -uo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select diff --git a/bin/gstack-browser-ensure b/bin/gstack-browser-ensure index f64765dfc..f50d04533 100755 --- a/bin/gstack-browser-ensure +++ b/bin/gstack-browser-ensure @@ -24,7 +24,7 @@ # GSTACK_CHROMIUM_NO_SANDBOX=1 for the probe. Bash 3.2 plus POSIX tools. BASH_COMPAT=50 set -u -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" || { echo "gstack-browser-ensure: cannot resolve the install root" >&2; exit 1; } DOCS="https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#browser-ensure-failed" diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe index becf3653c..11f65f58b 100755 --- a/bin/gstack-codex-probe +++ b/bin/gstack-codex-probe @@ -157,7 +157,7 @@ _gstack_codex_select_model() { # working-directory context forwarded to the shared role resolver. # The TOML is read by the TypeScript resolver; bash never parses it. local _kind="${1:-exec}" _explicit="${2:-}" _role="${3:-}" _cwd="${4:-}" _line _rc _rest - local GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac + local GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac case "$_kind" in exec|review) ;; *) echo "ERROR: _gstack_codex_select_model takes exec or review" >&2; return 2 ;; esac case "$_role" in ''|plan-review) ;; *) echo "ERROR: _gstack_codex_select_model role must be plan-review" >&2; return 2 ;; esac if ! command -v bun >/dev/null 2>&1; then diff --git a/bin/gstack-codex-session-import b/bin/gstack-codex-session-import index 9138be09c..88443d389 100755 --- a/bin/gstack-codex-session-import +++ b/bin/gstack-codex-session-import @@ -20,7 +20,7 @@ # Writes via bin/gstack-question-log so source tagging, dedup, and async # derive all apply uniformly. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select diff --git a/bin/gstack-developer-profile b/bin/gstack-developer-profile index 03a466c44..de10bb92e 100755 --- a/bin/gstack-developer-profile +++ b/bin/gstack-developer-profile @@ -28,7 +28,7 @@ # docs/designs/PLAN_TUNING_V0.md). Event file: ~/.gstack/projects/{SLUG}/ # question-events.jsonl. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" diff --git a/bin/gstack-distill-apply b/bin/gstack-distill-apply index a45c86bbe..013998183 100755 --- a/bin/gstack-distill-apply +++ b/bin/gstack-distill-apply @@ -23,7 +23,7 @@ # gstack-distill-apply --proposal --gbrain-published true # gstack-distill-apply --list # show pending proposals set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select diff --git a/bin/gstack-distill-free-text b/bin/gstack-distill-free-text index d3e7cb569..85b87b743 100755 --- a/bin/gstack-distill-free-text +++ b/bin/gstack-distill-free-text @@ -20,7 +20,7 @@ # auditability via --status when you want it. # Per D6: Anthropic SDK direct call, fail-loud on missing ANTHROPIC_API_KEY. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac # Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a # pipe in the forked child before exec, with no reader on the other end. On diff --git a/bin/gstack-doctor b/bin/gstack-doctor index 002eebf8d..e6ebdba85 100755 --- a/bin/gstack-doctor +++ b/bin/gstack-doctor @@ -40,7 +40,7 @@ BASH_COMPAT=50 set -u -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac usage() { sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//' diff --git a/bin/gstack-doctor-check.sh b/bin/gstack-doctor-check.sh index 1fc94ee59..870607490 100644 --- a/bin/gstack-doctor-check.sh +++ b/bin/gstack-doctor-check.sh @@ -19,7 +19,7 @@ # remote's capy/support.env, `unknown` when its fetch failed, or ""), LIVE. # GSTACK_CAPY_WRAPPER_PATH (optional) names the wrapper a below-minimum fix runs. -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac . "$BIN/gstack-doctor-components.sh" || { echo "gstack-doctor: $BIN/gstack-doctor-components.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; } . "$BIN/gstack-install-registry.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-install-registry.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; } . "$BIN/gstack-bun-version.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-bun-version.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; } diff --git a/bin/gstack-gbrain-install b/bin/gstack-gbrain-install index f311a68d6..c4b6fb269 100755 --- a/bin/gstack-gbrain-install +++ b/bin/gstack-gbrain-install @@ -36,7 +36,7 @@ # 2 — prerequisite missing or invalid argument # 3 — post-install validation failed (PATH shadow, broken binary, etc.) set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac # --- defaults --- # No version pin by default — install the latest default-branch HEAD (#1744). diff --git a/bin/gstack-gbrain-source-wireup b/bin/gstack-gbrain-source-wireup index cb20bc3e4..ab216c58d 100755 --- a/bin/gstack-gbrain-source-wireup +++ b/bin/gstack-gbrain-source-wireup @@ -39,7 +39,7 @@ # Depends on: bun (already a gstack dependency), gbrain, git. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CONFIG_BIN="$SCRIPT_DIR/gstack-config" diff --git a/bin/gstack-hook-check b/bin/gstack-hook-check index c0b2d1b5f..bffd2448b 100755 --- a/bin/gstack-hook-check +++ b/bin/gstack-hook-check @@ -37,7 +37,7 @@ # (needed when a hook runs TypeScript) is not found. Set GSTACK_HOOK_CHECK_BUN to # the bun to run (setup passes its own). set -u -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac LIST_ONLY=0 SETUP_FILE="" while [ $# -gt 1 ]; do diff --git a/bin/gstack-install-verdict.sh b/bin/gstack-install-verdict.sh index d8cd779c5..4fff5c06c 100644 --- a/bin/gstack-install-verdict.sh +++ b/bin/gstack-install-verdict.sh @@ -31,7 +31,7 @@ # spawned sessions (ambient OpenClaw) only warn. GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 # admits anyway and says so. -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac # gstack_source_root BIN_DIR — the source checkout behind an install: an # installed runtime root's recorded .source-path (setup _link_runtime_dists), diff --git a/bin/gstack-ios-qa-daemon b/bin/gstack-ios-qa-daemon index 1912a58b3..366f2c6b7 100755 --- a/bin/gstack-ios-qa-daemon +++ b/bin/gstack-ios-qa-daemon @@ -21,7 +21,7 @@ # session tokens are outstanding. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac # The --help heredoc is over 512 bytes; keep it off a pipe (test/heredoc-pipe-deadlock.test.ts). BASH_COMPAT=50 diff --git a/bin/gstack-ios-qa-mint b/bin/gstack-ios-qa-mint index 0e9610307..8192c88a6 100755 --- a/bin/gstack-ios-qa-mint +++ b/bin/gstack-ios-qa-mint @@ -10,7 +10,7 @@ # Allowlist file: ~/.gstack/ios-qa-allowlist.json (mode 0600). set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" GSTACK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" diff --git a/bin/gstack-ios-qa-regen b/bin/gstack-ios-qa-regen index 917e23190..ea4451cc5 100755 --- a/bin/gstack-ios-qa-regen +++ b/bin/gstack-ios-qa-regen @@ -3,7 +3,7 @@ # package and the app-owned typed state accessors. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac usage() { cat <<'EOF' diff --git a/bin/gstack-learnings-log b/bin/gstack-learnings-log index d8a03b5a3..117e5b68b 100755 --- a/bin/gstack-learnings-log +++ b/bin/gstack-learnings-log @@ -6,7 +6,7 @@ # Append-only storage. Duplicates (same key+type) are resolved at read time # by gstack-learnings-search ("latest winner" per key+type). set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Windows git-bash (#1950): pwd yields a POSIX path (/c/Users/...), which Bun # on Windows cannot resolve as an ES module specifier in the import below. diff --git a/bin/gstack-learnings-search b/bin/gstack-learnings-search index d0442bf2d..8e45cd768 100755 --- a/bin/gstack-learnings-search +++ b/bin/gstack-learnings-search @@ -8,7 +8,7 @@ # exits 127 and a failed read exits non-zero with its stderr (#2790): "could # not read" must never look like "nothing recorded". set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)" . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } diff --git a/bin/gstack-memorable b/bin/gstack-memorable index 391323568..035723886 100755 --- a/bin/gstack-memorable +++ b/bin/gstack-memorable @@ -32,7 +32,7 @@ # Heredoc delivery guard (see bin/gstack-settings-hook for the rationale). BASH_COMPAT=50 set -uo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" diff --git a/bin/gstack-question-log b/bin/gstack-question-log index b181e7e76..01f7b368c 100755 --- a/bin/gstack-question-log +++ b/bin/gstack-question-log @@ -26,7 +26,7 @@ # # Append-only JSONL. Dedup is at read time in gstack-question-sensitivity --read-log. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Windows git-bash (#1950): pwd yields a POSIX path (/c/Users/...), which Bun # on Windows cannot resolve as an ES module specifier in bun -e imports. diff --git a/bin/gstack-question-preference b/bin/gstack-question-preference index 4114c2147..905e46677 100755 --- a/bin/gstack-question-preference +++ b/bin/gstack-question-preference @@ -35,7 +35,7 @@ # --check already ignores those prefs; storing them made --stats lie. # always-ask on a one-way id is fine (it agrees with the safety override). set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" diff --git a/bin/gstack-relink b/bin/gstack-relink index 351cdf294..f4633fb38 100755 --- a/bin/gstack-relink +++ b/bin/gstack-relink @@ -9,7 +9,7 @@ # GSTACK_INSTALL_DIR — override gstack install directory # GSTACK_SKILLS_DIR — override target skills directory set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" GSTACK_CONFIG="${SCRIPT_DIR}/gstack-config" diff --git a/bin/gstack-review-log b/bin/gstack-review-log index ee7265b25..c77b8882a 100755 --- a/bin/gstack-review-log +++ b/bin/gstack-review-log @@ -17,7 +17,7 @@ # a model read the code. Caller-supplied binding fields are always discarded. # Plan-tier rows retain their legacy binding behavior. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac export GIT_OPTIONAL_LOCKS=0 export GIT_CONFIG_PARAMETERS="${GIT_CONFIG_PARAMETERS:+$GIT_CONFIG_PARAMETERS }'core.fsmonitor=false' 'core.untrackedCache=false'" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" diff --git a/bin/gstack-review-read b/bin/gstack-review-read index 3392693d5..2ebef6282 100755 --- a/bin/gstack-review-read +++ b/bin/gstack-review-read @@ -9,7 +9,7 @@ # rows using review_freshness; legacy or uncaptured diff evidence cannot fall # back to HEAD equality. Plan rows are unchanged. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;; diff --git a/bin/gstack-settings-hook b/bin/gstack-settings-hook index a12ab1e46..c5ca45a3b 100755 --- a/bin/gstack-settings-hook +++ b/bin/gstack-settings-hook @@ -69,7 +69,7 @@ BASH_COMPAT=50 set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac ACTION="${1:-}" SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json}" diff --git a/bin/gstack-specialist-stats b/bin/gstack-specialist-stats index 3c78d1939..144d1c317 100755 --- a/bin/gstack-specialist-stats +++ b/bin/gstack-specialist-stats @@ -8,7 +8,7 @@ # Fix-range passes (scope "fix-range", #3339) are skipped: a narrowed dispatch # says nothing about a specialist's hit rate on a whole branch. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)" . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } diff --git a/bin/gstack-team-init b/bin/gstack-team-init index 8ab51072a..b93cf25f6 100755 --- a/bin/gstack-team-init +++ b/bin/gstack-team-init @@ -18,7 +18,7 @@ BASH_COMPAT=50 set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac MODE="${1:-}" diff --git a/bin/gstack-telemetry-log b/bin/gstack-telemetry-log index 94980ca90..9d518da7b 100755 --- a/bin/gstack-telemetry-log +++ b/bin/gstack-telemetry-log @@ -21,7 +21,7 @@ # # NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero set -uo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" SCRIPT_DIR="$GSTACK_DIR/bin" diff --git a/bin/gstack-timeline-log b/bin/gstack-timeline-log index 70f6ce12c..6bf54ceda 100755 --- a/bin/gstack-timeline-log +++ b/bin/gstack-timeline-log @@ -10,7 +10,7 @@ # Optional: branch, outcome, duration_s, session, ts. # Validation failure → skip silently (non-blocking). set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)" . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } diff --git a/bin/gstack-timeline-read b/bin/gstack-timeline-read index 6f1e979a7..361af6f19 100755 --- a/bin/gstack-timeline-read +++ b/bin/gstack-timeline-read @@ -10,7 +10,7 @@ # --branch, or an unknown argument exits 2 before any read (#1723); --help # prints the usage line and exits 0. set -euo pipefail -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac USAGE='Usage: gstack-timeline-read [--since "7 days ago"|7d|YYYY-MM-DD] [--limit N] [--branch NAME]' SINCE_FORMS='accepted: " minute|hour|day|week|month[s] ago", "h|d|w", or an ISO date YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ' SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" diff --git a/gstack-upgrade/migrations/v1.86.0.0.sh b/gstack-upgrade/migrations/v1.86.0.0.sh index e128316eb..133b3e9e4 100755 --- a/gstack-upgrade/migrations/v1.86.0.0.sh +++ b/gstack-upgrade/migrations/v1.86.0.0.sh @@ -5,7 +5,7 @@ # shared old renders are retired. This post-setup pass repairs missed upgrades. # Idempotent and non-fatal: foreign entries and failed replacements stay intact. set -u -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac INSTALL_DIR="${GSTACK_INSTALL_DIR:-$HOME/.claude/skills/gstack}" if [ -f "$INSTALL_DIR/bin/gstack-migrate-claude-code" ] && command -v bun >/dev/null 2>&1; then bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$INSTALL_DIR/bin/gstack-migrate-claude-code" --install-dir "$INSTALL_DIR" || true diff --git a/hosts/claude/hooks/auq-error-fallback-hook b/hosts/claude/hooks/auq-error-fallback-hook index 586fe06ad..555e6cb55 100755 --- a/hosts/claude/hooks/auq-error-fallback-hook +++ b/hosts/claude/hooks/auq-error-fallback-hook @@ -3,6 +3,6 @@ # wrapper makes the TypeScript hook executable via bun. Settings.json # references this file directly. set -e -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac HERE="$(cd "$(dirname "$0")" && pwd)" exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/auq-error-fallback-hook.ts" diff --git a/hosts/claude/hooks/memorable-user-prompt-hook b/hosts/claude/hooks/memorable-user-prompt-hook index bc3ba711a..3e03ed734 100755 --- a/hosts/claude/hooks/memorable-user-prompt-hook +++ b/hosts/claude/hooks/memorable-user-prompt-hook @@ -12,7 +12,7 @@ # vendor process running with the prompt on its stdin. Forwarded, the signal # reaches the .ts, which kills the vendor's process group on its way out. # `<&0` keeps stdin: bash hands background jobs /dev/null otherwise. -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac HERE="$(cd "$(dirname "$0")" && pwd)" || exit 0 bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/memorable-user-prompt-hook.ts" <&0 & _child=$! diff --git a/hosts/claude/hooks/question-log-hook b/hosts/claude/hooks/question-log-hook index cdbb659f5..d39baa681 100755 --- a/hosts/claude/hooks/question-log-hook +++ b/hosts/claude/hooks/question-log-hook @@ -3,6 +3,6 @@ # wrapper makes the TypeScript hook executable via bun. Settings.json # references this file directly. set -e -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac HERE="$(cd "$(dirname "$0")" && pwd)" exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/question-log-hook.ts" diff --git a/hosts/claude/hooks/question-preference-hook b/hosts/claude/hooks/question-preference-hook index 95a3f7f4f..7feb65c79 100755 --- a/hosts/claude/hooks/question-preference-hook +++ b/hosts/claude/hooks/question-preference-hook @@ -3,6 +3,6 @@ # wrapper makes the TypeScript hook executable via bun. Settings.json # references this file directly. set -e -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac HERE="$(cd "$(dirname "$0")" && pwd)" exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/question-preference-hook.ts" diff --git a/hosts/claude/hooks/timeline-stop-hook b/hosts/claude/hooks/timeline-stop-hook index fdab076a4..71c75ae32 100755 --- a/hosts/claude/hooks/timeline-stop-hook +++ b/hosts/claude/hooks/timeline-stop-hook @@ -5,7 +5,7 @@ # # FAIL-OPEN (F5): a Stop-event telemetry repair must never block the session. # Every failure path — bun missing, script crash — still exits 0. -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac HERE="$(cd "$(dirname "$0")" && pwd)" || exit 0 bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/timeline-stop-hook.ts" || true exit 0 diff --git a/lib/bun-isolation.ts b/lib/bun-isolation.ts index bf7b388f6..c1e514a80 100644 --- a/lib/bun-isolation.ts +++ b/lib/bun-isolation.ts @@ -6,8 +6,8 @@ * code under review. Every Bun gstack starts passes --no-env-file and an empty * --config: shebangs and skill prose spell it * `bun --no-env-file --config=/dev/null`, shipped shell files spell - * `--config="${GSTACK_BUN_CONFIG:-/dev/null}"` (NUL under MINGW, MSYS and - * Cygwin, #3415), TypeScript spawns use bunIsolationArgs(). + * `--config="${GSTACK_BUN_CONFIG:-/dev/null}"` (NUL when OSTYPE is msys or + * cygwin, #3415), TypeScript spawns use bunIsolationArgs(). * test/bun-entry-isolation.test.ts holds every entry point to that form. * * POSIX passes /dev/null. Native Windows has no /dev/null and `--config=NUL` diff --git a/plan-ceo-review/bin/mode-handoff-hook b/plan-ceo-review/bin/mode-handoff-hook index 542ffe643..a2656d6f1 100755 --- a/plan-ceo-review/bin/mode-handoff-hook +++ b/plan-ceo-review/bin/mode-handoff-hook @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Bash shim — Claude Code runs hook commands through /bin/sh; this runs the # TypeScript hook with bun. -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac HERE="$(cd "$(dirname "$0")" && pwd)" exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/mode-handoff-hook.ts" diff --git a/scripts/build-cso.sh b/scripts/build-cso.sh index ff3d4e3f0..c492bd920 100644 --- a/scripts/build-cso.sh +++ b/scripts/build-cso.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -eu -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac CSO_BUILD_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" cd "$CSO_BUILD_ROOT" diff --git a/setup b/setup index 782551433..43db8ec89 100755 --- a/setup +++ b/setup @@ -1,7 +1,7 @@ #!/usr/bin/env bash # gstack setup — build browser binary + register skills with Claude Code / Codex set -e -GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac umask 077 # Restrict new files to owner-only (0o600 files, 0o700 dirs) # Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a diff --git a/test/bun-config-shell-bins.test.ts b/test/bun-config-shell-bins.test.ts index 86f10038e..01968aab6 100644 --- a/test/bun-config-shell-bins.test.ts +++ b/test/bun-config-shell-bins.test.ts @@ -1,7 +1,8 @@ /** * #3415: gstack's shell bins start Bun with * --config="${GSTACK_BUN_CONFIG:-/dev/null}", and each defines - * GSTACK_BUN_CONFIG as NUL under MINGW, MSYS and Cygwin. Native bun.exe cannot + * GSTACK_BUN_CONFIG as NUL when OSTYPE is msys or cygwin (Git Bash, MSYS2, + * Cygwin). Native bun.exe cannot * open /dev/null (EINVAL), so a Windows bash with MSYS path conversion off * (MSYS_NO_PATHCONV=1, MSYS2_ARG_CONV_EXCL=*) made the literal spelling fail: * the record bins wrote nothing, and gstack-timeline-log still exited 0. @@ -9,10 +10,10 @@ * On every platform the free lanes run, windows-latest included, the record * bins run through lib/script-invocation.ts with conversion off in a hostile * repository: each writes its record, and the repository's bunfig.toml preload - * and .env never load. On POSIX a simulated Windows bash (uname reporting - * MINGW64, a bun that rejects /dev/null the way bun.exe does) reproduces the - * bug; its controls, with uname reporting Linux, show each bin fails loudly - * with nothing written instead of exiting 0. + * and .env never load. On POSIX a simulated Windows bash (OSTYPE=msys, a bun + * that rejects /dev/null the way bun.exe does) reproduces the bug; its + * controls, without OSTYPE=msys, show each bin fails loudly with nothing + * written instead of exiting 0. */ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { spawnSync } from 'node:child_process'; @@ -29,8 +30,7 @@ let repo = ''; let stateRoot = ''; let evilRoot = ''; let preloadMarker = ''; -let fakeWindows = ''; -let fakeBunOnly = ''; +let fakeBun = ''; const BINS: Array<{ bin: string; input: string; log: string }> = [ { bin: 'gstack-review-log', log: 'main-reviews.jsonl', input: JSON.stringify({ skill: 'plan-eng-review', timestamp: '2026-10-25T00:00:00Z', status: 'clean', unresolved: 0, critical_gaps: 0, issues_found: 0, mode: 'FULL_REVIEW', commit: 'abc1234' }) }, @@ -39,16 +39,16 @@ const BINS: Array<{ bin: string; input: string; log: string }> = [ { bin: 'gstack-timeline-log', log: 'timeline.jsonl', input: JSON.stringify({ skill: 'review', event: 'started', branch: 'main' }) }, ]; -function env(pathPrefix?: string): NodeJS.ProcessEnv { +function env(pathPrefix?: string, extra: Record = {}): NodeJS.ProcessEnv { const out: NodeJS.ProcessEnv = {}; - for (const [k, v] of Object.entries(process.env)) if (!/^(GSTACK_|MSYS)/.test(k)) out[k] = v; + for (const [k, v] of Object.entries(process.env)) if (!/^(GSTACK_|MSYS|OSTYPE$)/.test(k)) out[k] = v; const PATH = Object.keys(out).find(k => k.toUpperCase() === 'PATH') ?? 'PATH'; if (pathPrefix) out[PATH] = pathPrefix + path.delimiter + (out[PATH] ?? ''); - return { ...out, HOME: path.join(tmp, 'home'), GSTACK_STATE_ROOT: stateRoot, MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*' }; + return { ...out, HOME: path.join(tmp, 'home'), GSTACK_STATE_ROOT: stateRoot, MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*', ...extra }; } -function run(bin: string, input: string, pathPrefix?: string) { - return runScriptSync(path.join(ROOT, 'bin', bin), [input], { cwd: repo, env: env(pathPrefix), encoding: 'utf8', timeout: 60_000 }); +function run(bin: string, input: string, pathPrefix?: string, extra: Record = {}) { + return runScriptSync(path.join(ROOT, 'bin', bin), [input], { cwd: repo, env: env(pathPrefix, extra), encoding: 'utf8', timeout: 60_000 }); } function records(log: string): string[] { @@ -84,26 +84,21 @@ beforeAll(() => { fs.writeFileSync(path.join(repo, 'preload.ts'), `require('fs').writeFileSync(${JSON.stringify(preloadMarker)}, 'ran');\n`); fs.writeFileSync(path.join(repo, 'bunfig.toml'), 'preload = ["./preload.ts"]\n'); if (IS_WINDOWS) return; - fakeWindows = path.join(tmp, 'fake-windows'); - fakeBunOnly = path.join(tmp, 'fake-bun-only'); - for (const dir of [fakeWindows, fakeBunOnly]) { - fs.mkdirSync(dir); - writeExec(path.join(dir, 'bun'), [ - '#!/usr/bin/env bash', - 'a=()', - 'for x in "$@"; do', - ' case "$x" in', - ' --config=/dev/null) echo "EINVAL: Invalid argument (open())" >&2; echo "while reading config \\"/dev/null\\"" >&2; exit 1 ;;', - ' --config=NUL) a+=(--config=/dev/null) ;;', - ' *) a+=("$x") ;;', - ' esac', - 'done', - `exec ${JSON.stringify(process.execPath)} "\${a[@]}"`, - '', - ].join('\n')); - } - writeExec(path.join(fakeWindows, 'uname'), '#!/usr/bin/env bash\n[ "$1" = -s ] && { echo MINGW64_NT-10.0-26100; exit 0; }\nexec /usr/bin/uname "$@"\n'); - writeExec(path.join(fakeWindows, 'cygpath'), '#!/usr/bin/env bash\nprintf "%s\\n" "$2"\n'); + fakeBun = path.join(tmp, 'fake-bun'); + fs.mkdirSync(fakeBun); + writeExec(path.join(fakeBun, 'bun'), [ + '#!/usr/bin/env bash', + 'a=()', + 'for x in "$@"; do', + ' case "$x" in', + ' --config=/dev/null) echo "EINVAL: Invalid argument (open())" >&2; echo "while reading config \\"/dev/null\\"" >&2; exit 1 ;;', + ' --config=NUL) a+=(--config=/dev/null) ;;', + ' *) a+=("$x") ;;', + ' esac', + 'done', + `exec ${JSON.stringify(process.execPath)} "\${a[@]}"`, + '', + ].join('\n')); }); afterAll(() => { if (tmp) fs.rmSync(tmp, { recursive: true, force: true }); }); @@ -143,7 +138,7 @@ describe.skipIf(IS_WINDOWS)('#3415: simulated Windows bash (POSIX hosts)', () => for (const { bin, input, log } of BINS) { test(`${bin} passes NUL and writes its record`, () => { resetState(); - const r = run(bin, input, fakeWindows); + const r = run(bin, input, fakeBun, { OSTYPE: 'msys' }); expect(r.stderr).not.toContain('EINVAL'); expect(r.status).toBe(0); expect(records(log).length).toBe(1); @@ -152,7 +147,7 @@ describe.skipIf(IS_WINDOWS)('#3415: simulated Windows bash (POSIX hosts)', () => test(`control: when Bun rejects the config, ${bin} fails loudly and writes nothing`, () => { resetState(); - const r = run(bin, input, fakeBunOnly); + const r = run(bin, input, fakeBun); expect(r.stderr).toContain('EINVAL'); expect(r.status).not.toBe(0); expect(records(log)).toEqual([]); diff --git a/test/bun-entry-isolation.test.ts b/test/bun-entry-isolation.test.ts index 1d025770d..15ad346fc 100644 --- a/test/bun-entry-isolation.test.ts +++ b/test/bun-entry-isolation.test.ts @@ -30,7 +30,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const FLAGS = '--no-env-file --config=/dev/null'; const ISOLATED_SHEBANG = `#!/usr/bin/env -S bun ${FLAGS}`; const SHELL_FLAGS = '--no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}"'; -const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac'; +const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac'; const TRACKED = spawnSync('git', ['ls-files', '-z'], { cwd: ROOT, encoding: 'utf8', timeout: 30_000 }).stdout.split('\0').filter(Boolean) .filter(f => !/(^|\/)test\//.test(f) && fs.existsSync(path.join(ROOT, f)) && fs.statSync(path.join(ROOT, f)).isFile()); diff --git a/test/bun-isolation-launch.test.ts b/test/bun-isolation-launch.test.ts index e3715b411..8ed07db1f 100644 --- a/test/bun-isolation-launch.test.ts +++ b/test/bun-isolation-launch.test.ts @@ -18,7 +18,7 @@ import { bunIsolationArgs } from '../lib/bun-isolation'; import { bashCommandInvocation } from '../lib/script-invocation'; const PRINT = 'console.log("env=" + (process.env.GSTACK_LAUNCH_PROBE ?? "unset"))'; -const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) GSTACK_BUN_CONFIG=NUL ;; esac'; +const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) GSTACK_BUN_CONFIG=NUL ;; esac'; const NO_PATHCONV = { MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*' }; let project = ''; let preloadMarker = ''; diff --git a/test/plan-mode-preamble-render.test.ts b/test/plan-mode-preamble-render.test.ts index 78a476c0f..24bedde3f 100644 --- a/test/plan-mode-preamble-render.test.ts +++ b/test/plan-mode-preamble-render.test.ts @@ -16,7 +16,7 @@ import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); const PLAN_ARG = '${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}'; -const GENERATOR = /(?:\bbun(?: --no-env-file --config=\/dev\/null)?|\bbun_cmd|_gstack_render_bun) run (?:gen:skill-docs(?::user)?|scripts\/gen-skill-docs\.ts) --[a-z]/; +const GENERATOR = /(?:\bbun(?: --no-env-file --config=(?:\/dev\/null|"\$\{GSTACK_BUN_CONFIG:-\/dev\/null\}"))?|\bbun_cmd|_gstack_render_bun) run (?:gen:skill-docs(?::user)?|scripts\/gen-skill-docs\.ts) --[a-z]/; const CONFIG_READ = '[ "$("$GSTACK_CONFIG" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"'; /** Generator invocations in a file: not comments, and not echoed or logged instructions. */