v1.91.227.0 fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415) (#3431)

* fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* 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>

* fix: pre-landing review fixes (#3415)

- Choose NUL only when cygpath is also on PATH, so an OSTYPE=msys inherited
  by a POSIX bash keeps /dev/null instead of a cwd-relative NUL file.
- gstack-developer-profile --log-session reports a Bun failure instead of
  skipping it as invalid input, like gstack-timeline-log.
- learnings-injection tests check that their anchored bun -e block exists.
- CHANGELOG names the callers this fix does not cover; curation reason fixed.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes, cycle 2 (#3415)

- timeline-log and developer-profile validators print their verdict; any
  non-zero Bun exit is a reported failure (a Bun crash on Windows exits 3).
- timeline-log keeps the original event when ts stamping fails.
- The definition ratchet scopes a local GSTACK_BUN_CONFIG to its function.
- The shell-bins fixture clears its preload marker per test.
- CHANGELOG states the callers that discard the report and the Bun 1.3.x case.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: CHANGELOG notes Bun 1.3.10 evidence and remaining literal /dev/null callers (#3415)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-26 04:32:27 +00:00
1 parent 8d9501373b
commit 9dc1dfb729
54 files changed
+491 -112

No files matched your search

+1 -1
View File
@@ -303,4 +303,4 @@ Agents poll `eval:bg:*` logs for the `### gstack-detach EXIT=<code> ###` 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`, shell and skill callers pass `bun --no-env-file --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` and `cygpath` is on `PATH`, 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.
+28
View File
@@ -1,5 +1,33 @@
# Changelog
## [1.91.227.0] - 2026-10-26
**Windows: gstack's record 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 and start Bun with `--config="${GSTACK_BUN_CONFIG:-/dev/null}"`. The value is `NUL` only when `OSTYPE` is `msys` or `cygwin` (Git Bash, MSYS2 and Cygwin, the check `gstack-doctor` already uses) and `cygpath` is on `PATH`; everywhere else it is `/dev/null`. The `cygpath` check is a builtin `command -v`, so it forks nothing. It means an `OSTYPE=msys` inherited by a Linux or macOS bash keeps `/dev/null`, instead of passing a relative `NUL` that Bun there resolves to a file in the repository. Bun 1.4.2 and later, the version gstack tests against, read `NUL` as an empty config whether or not conversion is on. Bun 1.3.10 on Windows exits 1 on `--config=NUL` and on the lowercase `nul` that Git Bash used to produce alike (measured from PowerShell on windows-latest; the #3085 entry recorded a panic). Versions between 1.3.3 and 1.4.1, which setup and `gstack-doctor` only warn about, were not measured. The `:-/dev/null` default covers a call that loses its definition: it 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`** and **`gstack-developer-profile --log-session`**: invalid input still exits 0 with nothing logged (the hook-path contract). Their validators now print `valid` or `invalid`, and any non-zero exit, which is Bun itself failing, is reported on stderr and returned instead of being skipped as invalid. The verdict is not an exit code because a Bun crash on Windows exits 3. When stamping a missing `ts` fails, `gstack-timeline-log` keeps the original event instead of appending an empty line. Their production callers (`gstack-skill-start`, `gstack-skill-end` and the `/office-hours` templates) still discard stderr and the exit status, so the report shows only when the bin is run directly.
- Unchanged: bun shebangs keep `--config=/dev/null` because a shebang cannot name a per-platform path, and skill prose (templates and resolvers) keeps it because `test/bun-entry-isolation.test.ts` holds skill sources to the literal spelling. TypeScript spawns keep `bunIsolationArgs()`, and `scripts/ubicloud/` keeps `/dev/null` because it runs on Linux.
- Not covered: callers that hand `bun.exe` a POSIX script path built from `pwd` or `$HOME` (the Claude hook wrappers, `autoplan/bin/phase-publication-hook`, `plan-ceo-review/bin/mode-handoff-hook`, and some `gstack-doctor`, `gstack-codex-probe`, `setup` and migration calls) only got the config spelling. With path conversion off they may still fail on the `/c/...` script path itself. The record bins convert their own paths with `cygpath -m`; the others were not verified on Windows with conversion off. Bun shebang entry points and Bun calls in skill prose still pass the literal `--config=/dev/null`, so with conversion off they fail with `EINVAL` as before. One example is the outside-voice probe in `scripts/resolvers/outside-voice.ts`, which then reports Claude as not installed.
### 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 (`OSTYPE=msys`, a `cygpath`, and a `bun` stand-in that 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.
- An inherited `OSTYPE=msys` with no `cygpath`, in a repository that commits a file named `NUL` with a preload, leaves each bin on `/dev/null`: the record is written and the preload never runs. This fails on the first version of this fix (4 failures).
- `gstack-developer-profile --log-session` reports a rejected config on stderr and exits non-zero, and still skips invalid input silently. This fails on main.
- A `bun` that exits 3 with no output, the way a Bun crash exits on Windows, makes both bins report and exit non-zero instead of skipping the event as invalid input. This fails with an exit-code verdict.
- `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. A `local` definition covers only the rest of its function. 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. The two learnings-injection files also check that the anchored `bun -e` block exists. Before, a missing anchor sliced the script's last character and the negative assertions passed without checking anything.
### Gate integrity
- `test/bun-entry-isolation.test.ts` is strengthened. On POSIX the required shell spelling still resolves to `/dev/null`.
- Now offenders: the literal `--config=/dev/null` in a shipped shell file, and an undefined or redefined `GSTACK_BUN_CONFIG`.
- The shell-caller detector also skips a `--config` value before deciding whether a call starts a program, so `bun --no-env-file --config=<anything> -e` no longer passes unchecked. The tree has no such call.
- Negative controls, each of which fails the check: the old spelling, a variable without the default, an empty `--config=`, a missing, late or altered definition, a `local` definition used from another function, and a reverted bin.
- No threshold, budget, timeout, trial count or case kind changed.
## [1.91.226.0] - 2026-10-26
**/review can re-review just its own fixes after a fixing pass, under the same rules as /ship (#3339).** Before, every /review fix cycle reran the whole review over the whole branch, so a large branch could hit the three-cycle stop on findings in code the earlier passes had already read. Now the pass after an edited pass starts with `gstack-review-log --start review --fix-range` and its checklist, specialists and QA read only what changed since that pass. The Step 4.8 adversarial review still reads the whole branch on every pass, because its rows carry their own freshness. A narrowed pass is never the final review: when it makes no edits, /review runs one full pass before writing the final record, and a fix-range record is never CURRENT. Garry's "do it" on 2026-10-24 for a fix-range re-review with a final full pass covers it.
+1 -1
View File
@@ -1 +1 @@
1.91.226.0
1.91.227.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.226.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.227.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+2 -1
View File
@@ -1,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
_AUTOPLAN_DECIDED=''
_autoplan_backstop() {
if [ -z "$_AUTOPLAN_DECIDED" ]; then
@@ -9,7 +10,7 @@ _autoplan_backstop() {
}
trap _autoplan_backstop EXIT
_AUTOPLAN_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
_AUTOPLAN_RESULT="$(bun --no-env-file --config=/dev/null "$_AUTOPLAN_HOOK_DIR/phase-publication-hook.ts")"
_AUTOPLAN_RESULT="$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$_AUTOPLAN_HOOK_DIR/phase-publication-hook.ts")"
test -n "$_AUTOPLAN_RESULT"
printf '%s\n' "$_AUTOPLAN_RESULT"
_AUTOPLAN_DECIDED=1
+2 -1
View File
@@ -77,6 +77,7 @@
BASH_COMPAT=50
set -uo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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
@@ -576,7 +577,7 @@ PYEOF
CONSENT_TS="$SCRIPT_DIR/../lib/transcript-consent.ts"
transcript_policy() {
command -v bun >/dev/null 2>&1 || return 2
GSTACK_STATE_ROOT="$GSTACK_HOME" bun --no-env-file --config=/dev/null run "$CONSENT_TS" "$@"
GSTACK_STATE_ROOT="$GSTACK_HOME" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$CONSENT_TS" "$@"
}
# 0 when staged transcript pages may sync at all.
+2 -1
View File
@@ -24,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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"
@@ -58,7 +59,7 @@ probe() {
else PROBE_OUT=$(cd "$ROOT" && node -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?; fi
elif command -v bun >/dev/null 2>&1; then
ENGINE=bun
PROBE_OUT=$(cd "$ROOT" && bun --no-env-file --config=/dev/null -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?
PROBE_OUT=$(cd "$ROOT" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?
else
fail "neither node nor bun is on PATH to launch Chromium" "install Node.js (https://nodejs.org) or Bun, then re-run $0"
fi
+2 -1
View File
@@ -157,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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
@@ -168,7 +169,7 @@ _gstack_codex_select_model() {
[ -n "$_role" ] && set -- "$@" --role "$_role"
[ -n "$_explicit" ] && set -- "$@" --explicit "$_explicit"
[ -n "$_cwd" ] && set -- "$@" --cwd "$_cwd"
_line=$(bun --no-env-file --config=/dev/null run "$_GSTACK_CODEX_BIN/../scripts/resolve-codex-generation-model.ts" "$@")
_line=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$_GSTACK_CODEX_BIN/../scripts/resolve-codex-generation-model.ts" "$@")
_rc=$?
_GSTACK_CODEX_SEL="${_line%%$'\t'*}"
_rest="${_line#*$'\t'}"
+2 -1
View File
@@ -20,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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
@@ -101,7 +102,7 @@ IMPORTED=0
SKIPPED_NO_ANSWER=0
for SESSION_FILE in "${SESSION_FILES[@]}"; do
COUNT_LINE=$(SESSION_FILE_PATH="$SESSION_FILE" QLOG_BIN="$SCRIPT_DIR/gstack-question-log" bun --no-env-file --config=/dev/null -e '
COUNT_LINE=$(SESSION_FILE_PATH="$SESSION_FILE" QLOG_BIN="$SCRIPT_DIR/gstack-question-log" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
+29 -16
View File
@@ -28,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -70,7 +71,7 @@ do_migrate() {
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
trap 'rm -f "$TMPOUT"' EXIT
cat "$LEGACY_FILE" | bun --no-env-file --config=/dev/null -e "
cat "$LEGACY_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const sessions = [];
const signalsAcc = {};
@@ -127,7 +128,7 @@ do_migrate() {
mv "$LEGACY_FILE" "$LEGACY_FILE.migrated-$TS"
local COUNT
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun --no-env-file --config=/dev/null -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
echo "MIGRATE: ok — migrated $COUNT sessions from builder-profile.jsonl"
}
@@ -175,7 +176,8 @@ EOF
# and update aggregates (signals_accumulated, resources_shown, topics).
# Fix for #1671: the writer side of the v1.0.0.0 migration. Reader and
# writer now share the same file.
# Silent skip on invalid input (matches gstack-timeline-log:22-26 pattern).
# Invalid input is skipped silently; a Bun failure is reported and returned,
# as in gstack-timeline-log (#3415).
# -----------------------------------------------------------------------
do_log_session() {
local INPUT="${1:-}"
@@ -184,12 +186,23 @@ do_log_session() {
fi
# Validate: input must be parseable JSON with required fields (date, mode).
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
const j = JSON.parse(await Bun.stdin.text());
if (!j.date || !j.mode) process.exit(1);
" 2>/dev/null; then
return 0
# The validator prints its verdict; a non-zero exit means Bun itself failed.
local VALIDATE_RC=0 VALIDATE_OUT
VALIDATE_OUT=$(printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const text = await Bun.stdin.text();
let j;
try { j = JSON.parse(text); } catch {}
console.log(j && j.date && j.mode ? 'valid' : 'invalid');
") || VALIDATE_RC=$?
if [ "$VALIDATE_RC" -ne 0 ]; then
echo "gstack-developer-profile: bun exited $VALIDATE_RC before the session was validated; nothing was logged" >&2
return "$VALIDATE_RC"
fi
case "$VALIDATE_OUT" in
valid) ;;
invalid) return 0 ;;
*) echo "gstack-developer-profile: bun printed no verdict while validating the session; nothing was logged" >&2; return 1 ;;
esac
ensure_profile
@@ -197,7 +210,7 @@ do_log_session() {
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
trap 'rm -f "$TMPOUT"' EXIT
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config=/dev/null -e "
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const fs = require('fs');
const entry = JSON.parse(process.env.RECORD_INPUT);
if (!entry.ts) entry.ts = new Date().toISOString();
@@ -244,7 +257,7 @@ do_reconcile() {
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
trap 'rm -f "$TMPOUT"' EXIT
PROFILE_FILE_PATH="$PROFILE_FILE" PROJECTS_DIR="$GSTACK_HOME/projects" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config=/dev/null -e "
PROFILE_FILE_PATH="$PROFILE_FILE" PROJECTS_DIR="$GSTACK_HOME/projects" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const fs = require('fs');
const path = require('path');
const profile = JSON.parse(fs.readFileSync(process.env.PROFILE_FILE_PATH, 'utf-8'));
@@ -314,7 +327,7 @@ do_reconcile() {
# -----------------------------------------------------------------------
do_read() {
ensure_profile
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
cat "$PROFILE_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const p = JSON.parse(await Bun.stdin.text());
const sessions = p.sessions || [];
@@ -388,7 +401,7 @@ do_profile() {
# -----------------------------------------------------------------------
do_gap() {
ensure_profile
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
cat "$PROFILE_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const p = JSON.parse(await Bun.stdin.text());
const declared = p.declared || {};
const inferred = (p.inferred && p.inferred.values) || {};
@@ -417,7 +430,7 @@ do_derive() {
fi
cd "$ROOT_DIR"
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun --no-env-file --config=/dev/null -e "
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
import('./scripts/question-registry.ts').then(async (regmod) => {
const sigmod = await import('./scripts/psychographic-signals.ts');
const fs = require('fs');
@@ -504,7 +517,7 @@ do_trace() {
return 0
fi
cd "$ROOT_DIR"
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun --no-env-file --config=/dev/null -e "
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
import('./scripts/question-registry.ts').then(async (regmod) => {
const sigmod = await import('./scripts/psychographic-signals.ts');
const fs = require('fs');
@@ -542,7 +555,7 @@ do_trace() {
# -----------------------------------------------------------------------
do_check_mismatch() {
ensure_profile
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
cat "$PROFILE_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const p = JSON.parse(await Bun.stdin.text());
const declared = p.declared || {};
const inferred = (p.inferred && p.inferred.values) || {};
@@ -586,7 +599,7 @@ do_narrative() {
do_vibe() {
ensure_profile
cd "$ROOT_DIR"
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun --no-env-file --config=/dev/null -e "
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
import('./scripts/archetypes.ts').then(async (mod) => {
const p = JSON.parse(process.env.PROFILE_DATA);
const dims = (p.inferred && p.inferred.values) || {
+3 -2
View File
@@ -23,6 +23,7 @@
# gstack-distill-apply --proposal <N> --gbrain-published true
# gstack-distill-apply --list # show pending proposals
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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
@@ -57,7 +58,7 @@ if [ ! -f "$PROPOSAL_FILE" ]; then
fi
if [ "$ACTION" = "list" ]; then
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun --no-env-file --config=/dev/null -e '
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const fs = require("fs");
const p = JSON.parse(fs.readFileSync(process.env.PROPOSAL_FILE_PATH, "utf-8"));
const proposals = p.proposals || [];
@@ -100,7 +101,7 @@ MEMORY_FILE_PATH="$MEMORY_FILE" \
PROFILE_FILE_PATH="$PROFILE_FILE" \
PREF_BIN="$SCRIPT_DIR/gstack-question-preference" \
GBRAIN_PUBLISHED="$GBRAIN_PUBLISHED" \
bun --no-env-file --config=/dev/null -e '
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const fs = require("fs");
const { spawnSync } = require("child_process");
const idx = parseInt(process.env.PROPOSAL_IDX, 10);
+6 -5
View File
@@ -20,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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
@@ -59,7 +60,7 @@ esac
# --- Status subcommand --------------------------------------------------
if [ "$MODE" = "status" ]; then
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun --no-env-file --config=/dev/null -e '
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const fs = require("fs");
const slug = process.env.SLUG_PATH;
const path = process.env.COST_LOG_PATH;
@@ -98,7 +99,7 @@ if [ ! -f "$LOG_FILE" ]; then
exit 0
fi
EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e '
EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const fs = require("fs");
const lines = fs.readFileSync(process.env.LOG_FILE_PATH, "utf-8").trim().split("\n").filter(Boolean);
const out = [];
@@ -119,7 +120,7 @@ EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e
process.stdout.write(JSON.stringify(out));
')
EVENT_COUNT=$(printf '%s' "$EVENTS_JSON" | bun --no-env-file --config=/dev/null -e 'const a = JSON.parse(await Bun.stdin.text()); console.log(a.length);')
EVENT_COUNT=$(printf '%s' "$EVENTS_JSON" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'const a = JSON.parse(await Bun.stdin.text()); console.log(a.length);')
if [ "$EVENT_COUNT" -eq 0 ]; then
echo "NO_FREE_TEXT: nothing to distill"
exit 0
@@ -172,7 +173,7 @@ if [ "$MODE" = "dry-run" ]; then
echo "$DISTILL_PROMPT"
echo
echo "=== EVENTS ($EVENT_COUNT) ==="
echo "$EVENTS_JSON" | bun --no-env-file --config=/dev/null -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()), null, 2));'
echo "$EVENTS_JSON" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()), null, 2));'
exit 0
fi
@@ -196,7 +197,7 @@ fi
RESULT=$(EVENTS_JSON="$EVENTS_JSON" DISTILL_PROMPT="$DISTILL_PROMPT" \
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" LOG_FILE_PATH="$LOG_FILE" \
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
bun --no-env-file --config=/dev/null --cwd "$ROOT_DIR" -e '
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" --cwd "$ROOT_DIR" -e '
const fs = require("fs");
const Anthropic = require("@anthropic-ai/sdk").default;
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
+2 -1
View File
@@ -40,6 +40,7 @@
BASH_COMPAT=50
set -u
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
usage() {
sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//'
@@ -512,7 +513,7 @@ check_windows_bash() {
row warn "windows bash" "not checked: bun not found" "install Bun (see the bun row), then rerun $BIN/gstack-doctor"
return
fi
out=$(bun --no-env-file --config=/dev/null "$BIN/gstack-windows-bash.ts" ${GSTACK_DOCTOR_BASH_PROBE:+--probe "$GSTACK_DOCTOR_BASH_PROBE"} </dev/null 2>&1)
out=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$BIN/gstack-windows-bash.ts" ${GSTACK_DOCTOR_BASH_PROBE:+--probe "$GSTACK_DOCTOR_BASH_PROBE"} </dev/null 2>&1)
rc=$?
out=$(printf '%s\n' "$out" | tr -d '\000-\010\013-\037')
bash=$(printf '%s\n' "$out" | sed -n 's/^WINDOWS_BASH: //p' | head -1)
+4 -3
View File
@@ -19,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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; }
@@ -213,7 +214,7 @@ _dc_check_browser() {
out=$(cd "$ROOT" && { command -v timeout >/dev/null 2>&1 && timeout 90 node -e "$script" </dev/null 2>&1 || node -e "$script" </dev/null 2>&1; }); rc=$?
elif command -v bun >/dev/null 2>&1; then
engine=bun
out=$(cd "$ROOT" && bun --no-env-file --config=/dev/null -e "$script" </dev/null 2>&1); rc=$?
out=$(cd "$ROOT" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$script" </dev/null 2>&1); rc=$?
else
_dc_row FAIL browser "neither node nor bun is on PATH to launch Chromium" "install Node.js (https://nodejs.org) or Bun, then $fix" BROWSER_UNAVAILABLE
return
@@ -329,7 +330,7 @@ _dc_check_pins() {
return
fi
node=$(node --version </dev/null 2>/dev/null | clamp 'v0-9.' 20); node="${node#v}"
out=$(bun --no-env-file --config=/dev/null "$BIN/gstack-runtime-pins.ts" --project "$project" ${_dc_bun:+--bun "$_dc_bun"} ${node:+--node "$node"} </dev/null 2>&1); rc=$?
out=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$BIN/gstack-runtime-pins.ts" --project "$project" ${_dc_bun:+--bun "$_dc_bun"} ${node:+--node "$node"} </dev/null 2>&1); rc=$?
verdict=$(printf '%s\n' "$out" | sed -n 's/^PINS_VERDICT: //p' | tail -1 | clamp 'a-z' 8)
summary=$(printf '%s\n' "$out" | sed -n 's/^PIN_SUMMARY: //p' | tr '\n' '|' | sed 's/|$//; s/|/; /g')
case "$verdict" in
@@ -357,7 +358,7 @@ _dc_check_pins() {
fi
fi
if [ -n "$_dc_ln_dir" ] && [ "$_dc_ln_ver" != "$_dc_bun" ] \
&& bun --no-env-file --config=/dev/null "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
&& bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
note="; bun $_dc_ln_ver found at $_dc_ln_dir (not on PATH) satisfies them" fix="$_dc_ln_fix"
fi
_dc_row FAIL pins "$failline$note" "$fix" PROJECT_PIN_MISMATCH ;;
+3 -2
View File
@@ -36,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
# --- defaults ---
# No version pin by default — install the latest default-branch HEAD (#1744).
@@ -100,7 +101,7 @@ fi
# bun, which this installer already requires. It used to need jq, which it
# never checked for, so a successful install reported failure (#1675).
pkg_field() {
GSTACK_PKG_JSON="$1" GSTACK_PKG_FIELD="$2" bun --no-env-file --config=/dev/null -e '
GSTACK_PKG_JSON="$1" GSTACK_PKG_FIELD="$2" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
try {
const pkg = JSON.parse(require("fs").readFileSync(process.env.GSTACK_PKG_JSON, "utf8"));
const v = process.env.GSTACK_PKG_FIELD.split(".").reduce((o, k) => (o == null ? o : o[k]), pkg);
@@ -198,7 +199,7 @@ _bun_exe_hint() {
[ "$IS_WINDOWS" -eq 1 ] || return 0
command -v bun.exe >/dev/null 2>&1 && return 0
local real_bun
real_bun=$(bun --no-env-file --config=/dev/null -e 'console.log(process.execPath)' 2>/dev/null | tr -d '\r' || true)
real_bun=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'console.log(process.execPath)' 2>/dev/null | tr -d '\r' || true)
echo " detected: bun was installed via npm — bun.exe is NOT on %PATH%, and the gbrain.exe shim needs it." >&2
if [ -n "$real_bun" ]; then
echo " fix: add bun.exe's directory to PATH (persist it in your shell profile):" >&2
+2 -1
View File
@@ -39,6 +39,7 @@
# Depends on: bun (already a gstack dependency), gbrain, git.
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
@@ -222,7 +223,7 @@ ensure_worktree() {
# is not JSON.
source_local_path() {
gbrain sources list --json 2>/dev/null \
| GB_SOURCE_ID="$1" bun --no-env-file --config=/dev/null -e '
| GB_SOURCE_ID="$1" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const raw = await Bun.stdin.text();
try {
const s = (JSON.parse(raw).sources || []).find((x) => x.id === process.env.GB_SOURCE_ID);
+2 -1
View File
@@ -37,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
LIST_ONLY=0 SETUP_FILE=""
while [ $# -gt 1 ]; do
@@ -173,7 +174,7 @@ if [ -n "$ENTRIES" ]; then
# up below (lib/path-spelling.ts shellPath; test/path-spelling.test.ts keeps
# the two equal).
# shellcheck disable=SC2086 # newline-split list (IFS is set to a newline below)
(cd "$ROOT" && IFS=$NL && "$BUN" --no-env-file --config=/dev/null -e '
(cd "$ROOT" && IFS=$NL && "$BUN" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const shellPath = (value) => process.platform === "win32"
? String(value).replace(/\\/g, "/").replace(/^([A-Za-z]):(?=\/|$)/, (_, drive) => `/${drive.toLowerCase()}`)
: String(value);
+3 -1
View File
@@ -31,6 +31,8 @@
# spawned sessions (ambient OpenClaw) only warn. GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1
# admits anyway and says so.
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
# gstack_source_root BIN_DIR — the source checkout behind an install: an
# installed runtime root's recorded .source-path (setup _link_runtime_dists),
# else BIN_DIR/.. resolved. Prints the realpath; returns 1 when unresolvable.
@@ -159,7 +161,7 @@ gstack_install_admission() {
v_bun=$(gstack_install_verdict_field "$file" bun_path)
if [ -z "$detail" ] && [ -n "$v_bun" ]; then
running=$(gstack_realpath "$where" 2>/dev/null)
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config=/dev/null -p process.execPath </dev/null 2>/dev/null | head -1)
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -p process.execPath </dev/null 2>/dev/null | head -1)
if [ -n "$running" ] && [ "$running" != "$v_bun" ]; then
_ia_status=runtime code=BUN_RESOLUTION_DRIFT
detail="bun $found at $running is running, but the install verdict recorded bun $(gstack_install_verdict_field "$file" bun_version) at $v_bun"
+2 -1
View File
@@ -21,6 +21,7 @@
# session tokens are outstanding.
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
# The --help heredoc is over 512 bytes; keep it off a pipe (test/heredoc-pipe-deadlock.test.ts).
BASH_COMPAT=50
@@ -67,4 +68,4 @@ if ! command -v bun >/dev/null 2>&1; then
exit 1
fi
exec bun --no-env-file --config=/dev/null run "$ENTRY" "$@"
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$ENTRY" "$@"
+2 -1
View File
@@ -10,6 +10,7 @@
# Allowlist file: ~/.gstack/ios-qa-allowlist.json (mode 0600).
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSTACK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -25,4 +26,4 @@ if ! command -v bun >/dev/null 2>&1; then
exit 1
fi
exec bun --no-env-file --config=/dev/null run "$ENTRY" "$@"
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$ENTRY" "$@"
+2 -1
View File
@@ -3,6 +3,7 @@
# package and the app-owned typed state accessors.
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
usage() {
cat <<'EOF'
@@ -144,7 +145,7 @@ do
fi
done
bun --no-env-file --config=/dev/null run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"
# Stamp only after successful accessor generation. ios-sync uses this marker
# to distinguish a complete current install from an interrupted regeneration.
+2 -1
View File
@@ -6,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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.
@@ -27,7 +28,7 @@ INPUT="$1"
TMPERR=$(mktemp "${TMPDIR:-/tmp}/gstack-learnings-log.XXXXXX") || { echo "gstack-learnings-log: could not create a temp file in ${TMPDIR:-/tmp}, so the learning was not recorded. Fix: point TMPDIR at a writable directory." >&2; exit 1; }
trap 'rm -f "$TMPERR"' EXIT
set +e
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config=/dev/null -e "
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
const raw = await Bun.stdin.text();
let j;
+2 -1
View File
@@ -8,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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; }
@@ -69,7 +70,7 @@ emit_tagged_file() {
emit_tagged_file cross "$f"
done
fi
} | GSTACK_SEARCH_TYPE="$TYPE" GSTACK_SEARCH_QUERY="$QUERY" GSTACK_SEARCH_LIMIT="$LIMIT" GSTACK_SEARCH_CROSS="$CROSS_PROJECT" bun --no-env-file --config=/dev/null -e "
} | GSTACK_SEARCH_TYPE="$TYPE" GSTACK_SEARCH_QUERY="$QUERY" GSTACK_SEARCH_LIMIT="$LIMIT" GSTACK_SEARCH_CROSS="$CROSS_PROJECT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const now = Date.now();
const type = process.env.GSTACK_SEARCH_TYPE || '';
+2 -1
View File
@@ -32,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -374,7 +375,7 @@ status_bridge() {
# JSON; a failed query is reported as unknown, never as an empty history.
local egress_json
if egress_json="$("$EGRESS_BIN" list --sink "$SINK" --json 2>/dev/null)"; then
n="$(printf '%s' "$egress_json" | bun --no-env-file --config=/dev/null -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
n="$(printf '%s' "$egress_json" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
case "$n" in *[!0-9]*|"") n="unknown (could not parse gstack-egress output)" ;; esac
else
n="unknown (gstack-egress list failed; run it yourself)"
+4 -3
View File
@@ -31,6 +31,7 @@
#
# Append-only JSONL. Dedup is at read time in gstack-question-sensitivity --read-log.
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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.
@@ -51,7 +52,7 @@ INPUT="$1"
TMPERR=$(mktemp "${TMPDIR:-/tmp}/gstack-question-log.XXXXXX") || { echo "gstack-question-log: could not create a temp file in ${TMPDIR:-/tmp}, so the question was not logged. Fix: point TMPDIR at a writable directory." >&2; exit 1; }
trap 'rm -f "$TMPERR"' EXIT
set +e
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" GSTACK_SCRIPTS_DIR="$SCRIPT_DIR/../scripts" bun --no-env-file --config=/dev/null -e "
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" GSTACK_SCRIPTS_DIR="$SCRIPT_DIR/../scripts" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
const { resolveOptionKey } = await import(process.env.GSTACK_SCRIPTS_DIR + '/psychographic-signals.ts');
const path = require('path');
@@ -241,7 +242,7 @@ LOG_FILE="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
# Lookup is bounded so the bin stays cheap on hot paths.
DEDUP_SKIP=""
if [ -f "$LOG_FILE" ]; then
DEDUP_SKIP=$(VALIDATED_JSON="$VALIDATED" LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e '
DEDUP_SKIP=$(VALIDATED_JSON="$VALIDATED" LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const fs = require("fs");
const j = JSON.parse(process.env.VALIDATED_JSON);
if (!j.tool_use_id) { console.log(""); process.exit(0); }
@@ -261,7 +262,7 @@ if [ -f "$LOG_FILE" ]; then
fi
if [ "$DEDUP_SKIP" = "dup" ]; then
echo "DEDUP: skipped (source=$(echo "$VALIDATED" | bun --no-env-file --config=/dev/null -e 'const j=JSON.parse(await Bun.stdin.text()); console.log(j.source);'), tool_use_id duplicate)"
echo "DEDUP: skipped (source=$(echo "$VALIDATED" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'const j=JSON.parse(await Bun.stdin.text()); console.log(j.source);'), tool_use_id duplicate)"
exit 0
fi
+5 -4
View File
@@ -35,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -105,7 +106,7 @@ do_check() {
fi
ensure_file
cd "$ROOT_DIR"
PREF_FILE_PATH="$PREF_FILE" GLOBAL_PREF_FILE_PATH="$GLOBAL_PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun --no-env-file --config=/dev/null -e "
PREF_FILE_PATH="$PREF_FILE" GLOBAL_PREF_FILE_PATH="$GLOBAL_PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
import('./scripts/one-way-doors.ts').then((oneway) => {
const fs = require('fs');
const qid = process.env.QID;
@@ -201,7 +202,7 @@ do_write() {
set +e
local RESULT
RESULT=$(cd "$ROOT_DIR" && printf '%s' "$INPUT" | PREF_FILE_PATH="$PREF_FILE" EVENT_FILE_PATH="$EVENT_FILE" FREE_TEXT="$FREE_TEXT" HAS_FREE_TEXT="$HAS_FREE_TEXT" bun --no-env-file --config=/dev/null -e "
RESULT=$(cd "$ROOT_DIR" && printf '%s' "$INPUT" | PREF_FILE_PATH="$PREF_FILE" EVENT_FILE_PATH="$EVENT_FILE" FREE_TEXT="$FREE_TEXT" HAS_FREE_TEXT="$HAS_FREE_TEXT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const fs = require('fs');
const raw = await Bun.stdin.text();
let j;
@@ -320,7 +321,7 @@ do_clear() {
echo '{}' > "$PREF_FILE"
echo "OK: cleared all preferences"
else
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun --no-env-file --config=/dev/null -e "
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const fs = require('fs');
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
if (prefs[process.env.QID] !== undefined) {
@@ -339,7 +340,7 @@ do_clear() {
# -----------------------------------------------------------------------
do_stats() {
ensure_file
(cd "$ROOT_DIR" && PREF_FILE_PATH="$PREF_FILE" bun --no-env-file --config=/dev/null -e "
(cd "$ROOT_DIR" && PREF_FILE_PATH="$PREF_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
import('./scripts/one-way-doors.ts').then((oneway) => {
const fs = require('fs');
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
+2 -1
View File
@@ -9,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSTACK_CONFIG="${SCRIPT_DIR}/gstack-config"
@@ -474,7 +475,7 @@ if { [ "$DISABLED" != " " ] || grep -q '^Disabled on this install' "$_ROUTER" 2>
_ROUTER_TMP="$(mktemp -d "${TMPDIR:-/tmp}/gstack-router.XXXXXX")" || _ROUTER_TMP=""
_DISABLED_CSV="$(printf '%s' "$DISABLED" | xargs | tr ' ' ',')"
if [ -n "$_ROUTER_TMP" ] && command -v bun >/dev/null 2>&1 \
&& (cd "$_GEN_SRC" && bun --no-env-file --config=/dev/null run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1) \
&& (cd "$_GEN_SRC" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1) \
&& [ -s "$_ROUTER_TMP/SKILL.md" ] && cp "$_ROUTER_TMP/SKILL.md" "$_ROUTER.tmp.$$" && mv "$_ROUTER.tmp.$$" "$_ROUTER"; then
_link_root_skill_alias
[ -n "$_DISABLED_CSV" ] && echo "Router: disabled skills left out of $_ROUTER"
+4 -3
View File
@@ -17,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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)"
@@ -77,7 +78,7 @@ SESSION_KIND=$("$SCRIPT_DIR/gstack-session-kind" 2>/dev/null || echo interactive
export GSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" GSTACK_STAMP_TREE="$TREE" GSTACK_STAMP_WTREE="$WTREE" GSTACK_STAMP_DIRTY="$DIRTY" GSTACK_STAMP_SESSION_KIND="$SESSION_KIND"
if [ "$INPUT" = --start ]; then
case "${3:-}" in ''|--fix-range) ;; *) echo 'Usage: gstack-review-log --start SKILL [--fix-range]' >&2; exit 2 ;; esac
GSTACK_REVIEW_SKILL="${2:-}" GSTACK_REVIEW_FIX_RANGE="${3:-}" bun --no-env-file --config=/dev/null -e '
GSTACK_REVIEW_SKILL="${2:-}" GSTACK_REVIEW_FIX_RANGE="${3:-}" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const { captureReviewStart } = await import(process.env.GSTACK_REVIEW_LIB);
try { console.log(captureReviewStart(process.env.GSTACK_REVIEW_SKILL, process.env, process.env.GSTACK_REVIEW_FIX_RANGE === "--fix-range")); }
catch (e) { console.error(e.message); process.exit(1); }
@@ -85,7 +86,7 @@ if [ "$INPUT" = --start ]; then
exit $?
fi
if [ "$INPUT" = --check-shared-libs ] && [ "$#" -eq 2 ]; then
GSTACK_REVIEW_TOKEN="$2" bun --no-env-file --config=/dev/null -e '
GSTACK_REVIEW_TOKEN="$2" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const { checkSharedLibsReuse } = await import(process.env.GSTACK_REVIEW_LIB);
console.log(JSON.stringify(checkSharedLibsReuse(JSON.parse(await Bun.stdin.text()), process.env.GSTACK_REVIEW_TOKEN)));
'
@@ -113,7 +114,7 @@ case "$INPUT" in --*) usage ;; esac
# Caller values for the binding keys are dropped before stamping. A binding or
# validation failure reaches stderr as its own line (never folded into
# "invalid JSON"), and exits 1 with nothing written.
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun --no-env-file --config=/dev/null -e "
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const { bindReview } = await import(process.env.GSTACK_REVIEW_LIB);
let rec;
try { rec = JSON.parse(await Bun.stdin.text()); } catch (e) { console.error('gstack-review-log: invalid JSON, skipping (' + e.message + ')'); process.exit(1); }
+2 -1
View File
@@ -9,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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")" ;;
@@ -19,7 +20,7 @@ gstack_state_root_select
GSTACK_HOME="$_gstack_sr_root"
WTREE=$("$SCRIPT_DIR/gstack-wtree" 2>/dev/null || echo "unknown")
if [ -f "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" ]; then
GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" GSTACK_REVIEW_WTREE="$WTREE" bun --no-env-file --config=/dev/null -e '
GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" GSTACK_REVIEW_WTREE="$WTREE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
const { reviewFreshness } = await import(process.env.GSTACK_REVIEW_LIB);
for (const line of (await Bun.stdin.text()).split("\n").filter(Boolean)) {
try {
+10 -9
View File
@@ -69,6 +69,7 @@
BASH_COMPAT=50
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
ACTION="${1:-}"
SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json}"
@@ -307,7 +308,7 @@ function gsWriteIfChanged(path, beforeText, settings, existed) {
GSTACK_SETTINGS_INPUT="$SETTINGS_FILE"
export GSTACK_SETTINGS_INPUT
SETTINGS_FILE=$(bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
SETTINGS_FILE=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
process.stdout.write(gsResolveSettingsPath(process.env.GSTACK_SETTINGS_INPUT));
});') || exit $?
@@ -405,7 +406,7 @@ case "$ACTION" in
fi
_acquire_lock || exit 5
_mutation_env
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_HOOK_CMD="$HOOK_CMD" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_HOOK_CMD="$HOOK_CMD" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const hookCmd = process.env.GSTACK_HOOK_CMD;
const loaded = gsLoadSettings(settingsPath);
@@ -435,7 +436,7 @@ case "$ACTION" in
[ -f "$SETTINGS_FILE" ] || exit 1
_acquire_lock || exit 5
_mutation_env
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const loaded = gsLoadSettings(settingsPath);
const settings = loaded.settings;
@@ -505,7 +506,7 @@ case "$ACTION" in
GSTACK_TIMEOUT="$TIMEOUT" \
GSTACK_DIFF_ONLY="$DIFF_ONLY" \
GSTACK_ENSURE="$ENSURE" \
bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const event = process.env.GSTACK_EVENT;
const cmd = process.env.GSTACK_COMMAND;
@@ -660,7 +661,7 @@ case "$ACTION" in
[ -f "$SETTINGS_FILE" ] || exit 0
_acquire_lock || exit 5
_mutation_env
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_SOURCE="$SOURCE" GSTACK_RS_EVENT="$RS_EVENT" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_SOURCE="$SOURCE" GSTACK_RS_EVENT="$RS_EVENT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const source = process.env.GSTACK_SOURCE;
const onlyEvent = process.env.GSTACK_RS_EVENT || "";
@@ -763,7 +764,7 @@ case "$ACTION" in
GSTACK_PRUNE_ALL="$PRUNE_ALL" \
GSTACK_PT_OPTOUT="$GSTACK_PT_OPTOUT" \
GSTACK_SWEEP_EXCLUDE_SOURCES="${GSTACK_SWEEP_EXCLUDE_SOURCES:-}" \
bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const root = (process.env.GSTACK_REPOINT_ROOT || "").replace(/\/+$/, "");
const all = process.env.GSTACK_PRUNE_ALL === "1";
@@ -902,7 +903,7 @@ case "$ACTION" in
fi
_mutation_env
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_RESTORE_PATH="$LATEST" \
bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
var fs = require("fs");
var target = process.env.GSTACK_SETTINGS_PATH;
var backup = process.env.GSTACK_RESTORE_PATH;
@@ -944,7 +945,7 @@ case "$ACTION" in
exit 1
fi
[ -f "$SETTINGS_FILE" ] || exit 0
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_LI_EVENT="$LI_EVENT" GSTACK_LI_OWNED_BY="$LI_OWNED_BY" GSTACK_LI_CMD_RE="$LI_CMD_RE" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_LI_EVENT="$LI_EVENT" GSTACK_LI_OWNED_BY="$LI_OWNED_BY" GSTACK_LI_CMD_RE="$LI_CMD_RE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const event = process.env.GSTACK_LI_EVENT;
const ownedBy = process.env.GSTACK_LI_OWNED_BY || "";
const reSrc = process.env.GSTACK_LI_CMD_RE || "";
@@ -982,7 +983,7 @@ case "$ACTION" in
list-sources)
[ -f "$SETTINGS_FILE" ] || { echo "(no settings file)"; exit 0; }
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
const fs = require("fs");
let settings = {};
try { settings = JSON.parse(fs.readFileSync(process.env.GSTACK_SETTINGS_PATH, "utf8")); }
+2 -1
View File
@@ -8,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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; }
@@ -33,7 +34,7 @@ if [ -z "$COMBINED" ]; then
exit 0
fi
printf '%s' "$COMBINED" | bun --no-env-file --config=/dev/null -e "
printf '%s' "$COMBINED" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const NEVER_GATE = new Set(['security', 'data-migration', 'state-machines']);
const stats = {};
+2 -1
View File
@@ -18,6 +18,7 @@
BASH_COMPAT=50
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
MODE="${1:-}"
@@ -192,7 +193,7 @@ HOOK_EOF
if ! command -v bun >/dev/null 2>&1; then
echo " ! bun not found — manually add this PreToolUse hook (matcher Skill|skill, \"onFailure\": \"block\") to .claude/settings.json:"
echo " $HOOK_COMMAND"
elif GSTACK_SETTINGS_PATH="$SETTINGS" GSTACK_HOOK_COMMAND="$HOOK_COMMAND" bun --no-env-file --config=/dev/null -e "
elif GSTACK_SETTINGS_PATH="$SETTINGS" GSTACK_HOOK_COMMAND="$HOOK_COMMAND" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const fs = require('fs');
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const handler = { type: 'command', command: process.env.GSTACK_HOOK_COMMAND, onFailure: 'block' };
+2 -1
View File
@@ -21,6 +21,7 @@
#
# NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero
set -uo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
SCRIPT_DIR="$GSTACK_DIR/bin"
@@ -205,7 +206,7 @@ ERR_FIELD="null"
# look like a JSON string, the whole message becomes null — never raw.
ERR_MSG_FIELD="null"
if [ -n "$ERROR_MESSAGE" ]; then
ERR_MSG_FIELD="$(printf '%s' "$ERROR_MESSAGE" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config=/dev/null -e "
ERR_MSG_FIELD="$(printf '%s' "$ERROR_MESSAGE" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const { redactFindingSpans } = await import(process.env.GSTACK_LIB_DIR + '/redact-engine.ts');
const input = await Bun.stdin.text();
const out = redactFindingSpans(input, { repoVisibility: 'private' });
+22 -9
View File
@@ -10,6 +10,7 @@
# Optional: branch, outcome, duration_s, session, ts.
# Validation failure → skip silently (non-blocking).
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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; }
@@ -19,21 +20,33 @@ mkdir -p "$GSTACK_HOME/projects/$SLUG"
INPUT="$1"
# Validate: input must be parseable JSON with required fields
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
const j = JSON.parse(await Bun.stdin.text());
if (!j.skill || !j.event) process.exit(1);
" 2>/dev/null; then
exit 0 # skip silently, non-blocking
# Validate: input must be parseable JSON with required fields. The validator
# prints its verdict; a non-zero exit means Bun itself failed (#3415). An exit
# code cannot carry the verdict: a Bun crash on Windows exits 3.
VALIDATE_RC=0
VALIDATE_OUT=$(printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const text = await Bun.stdin.text();
let j;
try { j = JSON.parse(text); } catch {}
console.log(j && j.skill && j.event ? 'valid' : 'invalid');
") || VALIDATE_RC=$?
if [ "$VALIDATE_RC" -ne 0 ]; then
echo "gstack-timeline-log: bun exited $VALIDATE_RC before the event was validated; nothing was logged" >&2
exit "$VALIDATE_RC"
fi
case "$VALIDATE_OUT" in
valid) ;;
invalid) exit 0 ;; # skip silently, non-blocking
*) echo "gstack-timeline-log: bun printed no verdict while validating the event; nothing was logged" >&2; exit 1 ;;
esac
# Inject timestamp if not present
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "const j=JSON.parse(await Bun.stdin.text()); if(!j.ts) process.exit(1)" 2>/dev/null; then
INPUT=$(printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
if ! printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "const j=JSON.parse(await Bun.stdin.text()); if(!j.ts) process.exit(1)" 2>/dev/null; then
STAMPED=$(printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const j = JSON.parse(await Bun.stdin.text());
j.ts = new Date().toISOString();
console.log(JSON.stringify(j));
" 2>/dev/null) || true
" 2>/dev/null) && [ -n "$STAMPED" ] && INPUT=$STAMPED || true
fi
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
+2 -1
View File
@@ -10,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
USAGE='Usage: gstack-timeline-read [--since "7 days ago"|7d|YYYY-MM-DD] [--limit N] [--branch NAME]'
SINCE_FORMS='accepted: "<N> minute|hour|day|week|month[s] ago", "<N>h|d|w", or an ISO date YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -108,7 +109,7 @@ command -v bun >/dev/null 2>&1 || {
exit 127
}
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE_MS="$SINCE_MS" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun --no-env-file --config=/dev/null -e "
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE_MS="$SINCE_MS" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const branch = process.env.GSTACK_TIMELINE_BRANCH || '';
const limit = Number(process.env.GSTACK_TIMELINE_LIMIT);
+7 -2
View File
@@ -6,17 +6,22 @@ import { spawnSync } from 'child_process';
const SCRIPT_PATH = path.join(import.meta.dir, '../../bin/gstack-learnings-search');
const SCRIPT = fs.readFileSync(SCRIPT_PATH, 'utf-8');
const BIN_DIR = path.join(import.meta.dir, '../../bin');
const BUN_BLOCK_AT = SCRIPT.indexOf('bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e');
describe('gstack-learnings-search injection safety', () => {
it('finds the bun -e block the checks below read', () => {
expect(BUN_BLOCK_AT).toBeGreaterThanOrEqual(0);
});
it('must not interpolate variables into JS string literals', () => {
const jsBlock = SCRIPT.slice(SCRIPT.indexOf('bun --no-env-file --config=/dev/null -e'));
const jsBlock = SCRIPT.slice(BUN_BLOCK_AT);
expect(jsBlock).not.toMatch(/const \w+ = '\$\{/);
expect(jsBlock).not.toMatch(/= \$\{[A-Z_]+\};/);
expect(jsBlock).not.toMatch(/'\$\{CROSS_PROJECT\}'/);
});
it('must use process.env for parameters', () => {
const jsBlock = SCRIPT.slice(SCRIPT.indexOf('bun --no-env-file --config=/dev/null -e'));
const jsBlock = SCRIPT.slice(BUN_BLOCK_AT);
expect(jsBlock).toContain('process.env');
});
});
+2 -1
View File
@@ -5,8 +5,9 @@
# 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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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=/dev/null "$INSTALL_DIR/bin/gstack-migrate-claude-code" --install-dir "$INSTALL_DIR" || true
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$INSTALL_DIR/bin/gstack-migrate-claude-code" --install-dir "$INSTALL_DIR" || true
fi
exit 0
+2 -1
View File
@@ -3,5 +3,6 @@
# wrapper makes the TypeScript hook executable via bun. Settings.json
# references this file directly.
set -e
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
HERE="$(cd "$(dirname "$0")" && pwd)"
exec bun --no-env-file --config=/dev/null "$HERE/auq-error-fallback-hook.ts"
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/auq-error-fallback-hook.ts"
@@ -12,8 +12,9 @@
# 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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
HERE="$(cd "$(dirname "$0")" && pwd)" || exit 0
bun --no-env-file --config=/dev/null "$HERE/memorable-user-prompt-hook.ts" <&0 &
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/memorable-user-prompt-hook.ts" <&0 &
_child=$!
trap 'kill -TERM "$_child" 2>/dev/null' TERM INT HUP
wait "$_child" 2>/dev/null || wait "$_child" 2>/dev/null || true
+2 -1
View File
@@ -3,5 +3,6 @@
# wrapper makes the TypeScript hook executable via bun. Settings.json
# references this file directly.
set -e
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
HERE="$(cd "$(dirname "$0")" && pwd)"
exec bun --no-env-file --config=/dev/null "$HERE/question-log-hook.ts"
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/question-log-hook.ts"
+2 -1
View File
@@ -3,5 +3,6 @@
# wrapper makes the TypeScript hook executable via bun. Settings.json
# references this file directly.
set -e
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
HERE="$(cd "$(dirname "$0")" && pwd)"
exec bun --no-env-file --config=/dev/null "$HERE/question-preference-hook.ts"
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/question-preference-hook.ts"
+2 -1
View File
@@ -5,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
HERE="$(cd "$(dirname "$0")" && pwd)" || exit 0
bun --no-env-file --config=/dev/null "$HERE/timeline-stop-hook.ts" || true
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/timeline-stop-hook.ts" || true
exit 0
+6 -4
View File
@@ -4,10 +4,12 @@
* variables, and a `bunfig.toml` preload runs code before the entry point.
* gstack runs inside the user's repository, so that directory belongs to the
* code under review. Every Bun gstack starts passes --no-env-file and an empty
* --config: shebangs and shell callers spell it
* `bun --no-env-file --config=/dev/null`, TypeScript spawns use
* bunIsolationArgs(). test/bun-entry-isolation.test.ts holds every entry point
* to that form.
* --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 when OSTYPE is msys or
* cygwin and cygpath is on PATH, #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`
* panicked Bun 1.3.10 while parsing the flag (#3085), so a TypeScript spawn on
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.226",
"version": "1.91.227",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+2 -1
View File
@@ -1,5 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
HERE="$(cd "$(dirname "$0")" && pwd)"
exec bun --no-env-file --config=/dev/null "$HERE/mode-handoff-hook.ts"
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/mode-handoff-hook.ts"
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -eu
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
CSO_BUILD_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
cd "$CSO_BUILD_ROOT"
@@ -227,7 +228,7 @@ cso_build_evaluation_candidate() {
cp -R lib scripts "$source_root/"
cp -R test/helpers "$source_root/test/"
cp "$candidate_catalog" "$source_root/lib/cso/runtime-catalog.json"
(cd "$source_root" && "$BUN_CMD" --no-env-file --config=/dev/null scripts/cso-runtime-promotion.ts check-evaluation lib/cso/runtime-catalog.json)
(cd "$source_root" && "$BUN_CMD" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" scripts/cso-runtime-promotion.ts check-evaluation lib/cso/runtime-catalog.json)
(cd "$source_root" && cso_compile_bun lib/cso/cli.ts "$CSO_EVALUATION_UNIT/gstack-cso-core")
(cd "$source_root" && cso_compile_bun scripts/cso-eval-producer.ts "$CSO_EVALUATION_UNIT/cso-eval-producer")
cso_compile_watchdog "$CSO_EVALUATION_UNIT/gstack-cso-watchdog"
+1
View File
@@ -314,6 +314,7 @@ export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [
{ file: 'test/ship-hook-windows-paths.test.ts', reason: 'runs bin/ helpers through explicit bash and Bun argv with forward-slash paths; never executes a shebang; the path-spelling simulation is skipIf win32' },
{ file: 'test/gstack-wtree-windows-paths.test.ts', reason: '#3101: runs bin/gstack-wtree through explicit bash in an os.tmpdir linked worktree with forward-slash paths; never executes a shebang; the drive-letter simulation is skipIf win32' },
{ file: 'test/setup-hook-syntax-gate.test.ts', reason: '#3088/#3100: runs bin/gstack-hook-check and the setup hook sections through explicit bash with forward-slash paths; never executes a shebang. Its Bun drive-path spelling is the Windows bug it guards' },
{ file: 'test/bun-config-shell-bins.test.ts', reason: '#3415: runs the record bins through lib/script-invocation.ts (Git Bash on Windows) with MSYS path conversion off, the case it guards; its bun and cygpath stand-ins and simulated-OSTYPE cases are skipIf win32' },
{ file: 'test/path-spelling.test.ts', reason: 'sources bin/gstack-path-spelling.sh through explicit bash and checks lib/path-spelling.ts; on Windows it exercises the real Git Bash spellings' },
{ file: 'test/state-root-parity.test.ts', reason: 'runs the bash twin and lib/state-root.ts over an env table with PATH empty; no shebang execution, raw-string comparison is platform-neutral' },
{ file: 'test/generator-eexist.test.ts',
+4 -3
View File
@@ -1,6 +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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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
@@ -362,7 +363,7 @@ _runtime_root_refused() {
# --claude-model is validated before anything is written, and persisted once
# Claude is known to be selected (below, and in the --refresh-registered parent).
if [ "$CLAUDE_MODEL_SET" -eq 1 ]; then
if ! _CLAUDE_MODEL_RESOLVED="$(cd "$SOURCE_GSTACK_DIR" && bun --no-env-file --config=/dev/null run scripts/models.ts claude-overlay "$CLAUDE_MODEL_ARG" 2>&1)"; then
if ! _CLAUDE_MODEL_RESOLVED="$(cd "$SOURCE_GSTACK_DIR" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run scripts/models.ts claude-overlay "$CLAUDE_MODEL_ARG" 2>&1)"; then
printf 'Error: --claude-model %s\n' "$_CLAUDE_MODEL_RESOLVED" | sed '2,$s/^/ /' >&2
echo " Nothing was installed or changed." >&2
exit 1
@@ -1317,7 +1318,7 @@ if [ "$LOCAL_INSTALL" -eq 0 ]; then
_CODEX_PREFLIGHT_SELECTED=1
fi
fi
bun --no-env-file --config=/dev/null "$SOURCE_GSTACK_DIR/scripts/preflight-codex-overlap.ts" \
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$SOURCE_GSTACK_DIR/scripts/preflight-codex-overlap.ts" \
--source "$SOURCE_GSTACK_DIR" --namespace "$CODEX_SKILLS" \
--selected "$_CODEX_PREFLIGHT_SELECTED" --local "$CODEX_REPO_LOCAL" \
--windows "$IS_WINDOWS" --relocation "$HOME/.gstack/repos/gstack" || exit 1
@@ -1584,7 +1585,7 @@ ensure_playwright_browser() {
echo "gstack setup failed: Node.js is required on Windows" >&2
return 1
else
probe_cmd="bun --no-env-file --config=/dev/null --eval 'import { chromium } from \"playwright\"; const browser = await chromium.launch(); await browser.close();'"
probe_cmd="bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" --eval 'import { chromium } from \"playwright\"; const browser = await chromium.launch(); await browser.close();'"
fi
(
cd "$SOURCE_GSTACK_DIR"
+204
View File
@@ -0,0 +1,204 @@
/**
* #3415: gstack's shell bins start Bun with
* --config="${GSTACK_BUN_CONFIG:-/dev/null}", and each defines
* GSTACK_BUN_CONFIG as NUL when OSTYPE is msys or cygwin and cygpath is on PATH
* (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.
*
* 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 (OSTYPE=msys, a
* cygpath, 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. An OSTYPE=msys inherited by a POSIX
* bash with no cygpath keeps /dev/null, so a file named NUL in the repository
* is never read as Bun's config.
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runScriptSync } from '../lib/script-invocation';
const ROOT = path.resolve(import.meta.dir, '..');
const IS_WINDOWS = process.platform === 'win32';
let tmp = '';
let repo = '';
let stateRoot = '';
let evilRoot = '';
let preloadMarker = '';
let fakeBun = '';
let crashBun = '';
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' }) },
{ bin: 'gstack-question-log', log: 'question-log.jsonl', input: JSON.stringify({ skill: 'ship', question_id: 'ship-test-failure-triage', question_summary: 'Tests failed', options_count: 3, user_choice: 'fix-now', recommended: 'fix-now', session_id: 's1' }) },
{ bin: 'gstack-learnings-log', log: 'learnings.jsonl', input: JSON.stringify({ skill: 'review', type: 'pitfall', key: 'n-plus-one', insight: 'fixture insight', confidence: 8, source: 'observed' }) },
{ bin: 'gstack-timeline-log', log: 'timeline.jsonl', input: JSON.stringify({ skill: 'review', event: 'started', branch: 'main' }) },
];
function env(pathPrefix?: string, extra: Record<string, string> = {}): NodeJS.ProcessEnv {
const out: NodeJS.ProcessEnv = {};
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: '*', ...extra };
}
function run(bin: string, input: string, pathPrefix?: string, extra: Record<string, string> = {}, rest: string[] = []) {
return runScriptSync(path.join(ROOT, 'bin', bin), [input, ...rest], { cwd: repo, env: env(pathPrefix, extra), encoding: 'utf8', timeout: 60_000 });
}
function records(log: string): string[] {
const projects = path.join(stateRoot, 'projects');
if (!fs.existsSync(projects)) return [];
return fs.readdirSync(projects).flatMap(slug => {
const file = path.join(projects, slug, log);
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean) : [];
});
}
function resetState(): void {
fs.rmSync(stateRoot, { recursive: true, force: true });
fs.rmSync(preloadMarker, { force: true });
fs.mkdirSync(stateRoot, { recursive: true });
}
function writeExec(file: string, text: string): void {
fs.writeFileSync(file, text);
fs.chmodSync(file, 0o755);
}
beforeAll(() => {
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-bunconfig-')));
repo = path.join(tmp, 'repo');
stateRoot = path.join(tmp, 'state');
evilRoot = path.join(tmp, 'evil');
preloadMarker = path.join(tmp, 'preload-ran');
for (const d of [repo, stateRoot, path.join(tmp, 'home')]) fs.mkdirSync(d, { recursive: true });
for (const args of [['init', '-q', '-b', 'main'], ['-c', 'user.email=t@example.com', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init']]) {
expect(spawnSync('git', args, { cwd: repo, timeout: 30_000 }).status).toBe(0);
}
fs.writeFileSync(path.join(repo, '.env'), `GSTACK_STATE_ROOT=${evilRoot}\nGSTACK_HOME=${evilRoot}\n`);
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;
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'));
crashBun = path.join(tmp, 'crash-bun');
fs.mkdirSync(crashBun);
writeExec(path.join(crashBun, 'bun'), '#!/usr/bin/env bash\nexit 3\n');
writeExec(path.join(fakeBun, 'cygpath'), '#!/usr/bin/env bash\nprintf \'%s\\n\' "${@: -1}"\n');
});
afterAll(() => { if (tmp) fs.rmSync(tmp, { recursive: true, force: true }); });
describe('#3415: record bins start Bun on a Windows bash with path conversion off', () => {
test('fixture control: a plain bun in the repository runs its bunfig preload', () => {
const r = spawnSync(process.execPath, ['-e', '0'], { cwd: repo, encoding: 'utf8', timeout: 60_000 });
expect(r.status).toBe(0);
expect(fs.existsSync(preloadMarker)).toBe(true);
fs.rmSync(preloadMarker);
});
for (const { bin, input, log } of BINS) {
test(`${bin} writes its record and ignores the repository bunfig.toml and .env`, () => {
resetState();
const r = run(bin, input);
expect(r.stderr).not.toContain('EINVAL');
expect(r.status).toBe(0);
expect(records(log).length).toBe(1);
expect(fs.existsSync(preloadMarker)).toBe(false);
expect(fs.existsSync(evilRoot)).toBe(false);
});
}
test('gstack-timeline-log still skips invalid input silently with exit 0', () => {
resetState();
for (const bad of ['not json', '{"skill":"review"}', 'null']) {
const r = run('gstack-timeline-log', bad);
expect(r.status).toBe(0);
expect(r.stderr).toBe('');
}
expect(records('timeline.jsonl')).toEqual([]);
});
});
describe.skipIf(IS_WINDOWS)('#3415: simulated Windows bash (POSIX hosts)', () => {
test('gstack-developer-profile --log-session reports a Bun failure instead of skipping it, and still skips invalid input silently', () => {
resetState();
const session = JSON.stringify({ date: '2026-10-25', mode: 'startup' });
const failed = run('gstack-developer-profile', '--log-session', fakeBun, {}, [session]);
expect(failed.stderr).toContain('EINVAL');
expect(failed.stderr).toContain('nothing was logged');
expect(failed.status).not.toBe(0);
for (const bad of ['not json', '{"date":"2026-10-25"}', 'null']) {
const r = run('gstack-developer-profile', '--log-session', undefined, {}, [bad]);
expect(r.status).toBe(0);
expect(r.stderr).toBe('');
}
});
test('a Bun that dies with exit 3 (how a Bun crash exits on Windows) is reported, never read as invalid input', () => {
resetState();
const timeline = run('gstack-timeline-log', JSON.stringify({ skill: 'review', event: 'started' }), crashBun);
expect(timeline.status).not.toBe(0);
expect(timeline.stderr).toContain('bun exited 3');
expect(records('timeline.jsonl')).toEqual([]);
const profile = run('gstack-developer-profile', '--log-session', crashBun, {}, [JSON.stringify({ date: '2026-10-25', mode: 'startup' })]);
expect(profile.status).not.toBe(0);
expect(profile.stderr).toContain('bun exited 3');
});
for (const { bin, input, log } of BINS) {
test(`${bin} passes NUL and writes its record`, () => {
resetState();
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);
expect(fs.existsSync(preloadMarker)).toBe(false);
});
test(`an inherited OSTYPE=msys without cygpath keeps /dev/null: ${bin} never reads a repository file named NUL`, () => {
resetState();
const nul = path.join(repo, 'NUL');
fs.writeFileSync(nul, 'preload = ["./preload.ts"]\n');
try {
const r = run(bin, input, undefined, { OSTYPE: 'msys' });
expect(r.status).toBe(0);
expect(records(log).length).toBe(1);
expect(fs.existsSync(preloadMarker)).toBe(false);
} finally {
fs.rmSync(nul, { force: true });
fs.rmSync(preloadMarker, { force: true });
}
});
test(`control: when Bun rejects the config, ${bin} fails loudly and writes nothing`, () => {
resetState();
const r = run(bin, input, fakeBun);
expect(r.stderr).toContain('EINVAL');
expect(r.status).not.toBe(0);
expect(records(log)).toEqual([]);
});
}
});
+60 -8
View File
@@ -6,9 +6,11 @@
* kinds are held to the isolated form:
* 1. bun shebangs: exactly ISOLATED_SHEBANG (`bun run <file>` ignores the
* shebang, so callers carry the flags too);
* 2. shell callers in shipped shell files, skill templates, generated skill
* docs and resolvers: `bun --no-env-file --config=/dev/null` before
* `run`, `-e`/`-p` or a script path;
* 2. shell callers: skill templates, generated skill docs and resolvers
* spell `bun --no-env-file --config=/dev/null` before `run`, `-e`/`-p`
* or a script path; shipped shell files spell SHELL_FLAGS after
* CONFIG_DEF, so a Windows bash with MSYS path conversion off passes
* NUL instead of a /dev/null native Bun cannot open (#3415);
* 3. TypeScript spawns of a Bun: bunIsolationArgs() (or browse's
* bunChildFlags()) in the argument list;
* 4. scriptInvocation, which launches gstack's own bun bins;
@@ -27,6 +29,8 @@ import { emptyBunConfig as browseEmptyBunConfig } from '../browse/src/bun-child-
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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; 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());
@@ -43,7 +47,7 @@ const BUN_WORD = /(^|[\s;&|({`"']|\$\()(bun|"\$\{?BUN(?:_[A-Z]+)?\}?"|\$\{?BUN(?
/** Whether the text after a `bun` word starts a program: -e/-p, a script path, `run <var>` or a bin path. A package script (`bun run build`) is not one. */
function runsCode(rest: string): boolean {
const r = rest.replace(/^\s+/, '').replace(/^(?:--(?:no-env-file|cwd[= ]\S+)\s+)*/, '');
const r = rest.replace(/^\s+/, '').replace(/^(?:--(?:no-env-file|cwd[= ]\S+|config(?:=\S*|\s+\S+))\s+)*/, '');
return /^(?:run\s+)?(?:-e|-p|--eval|--print)(?:\s|=|$)/.test(r)
|| /^(?:run\s+)?["']?[^\s"'|;&)]*\.(?:ts|js|mjs|cjs|tsx)(?:["'\s)|;&\\`]|$)/.test(r)
|| /^run\s+["']?\$\{?[A-Za-z_]/.test(r)
@@ -51,14 +55,14 @@ function runsCode(rest: string): boolean {
}
/** Bun invocations on one shell line that start a program without the isolation flags. */
function unisolatedBunCalls(line: string): string[] {
function unisolatedBunCalls(line: string, flags = FLAGS): string[] {
if (/^\s*(#|\/\/|\*)/.test(line)) return [];
const out: string[] = [];
for (const m of line.matchAll(BUN_WORD)) {
const at = m.index! + m[1]!.length;
if (/\bln\s+-\w+\s+$/.test(line.slice(0, at))) continue;
const rest = line.slice(at + m[2]!.length);
if (!rest.startsWith(` ${FLAGS}`) && runsCode(rest)) out.push(line.slice(at, at + 80));
if (!rest.startsWith(` ${flags}`) && runsCode(rest)) out.push(line.slice(at, at + 80));
}
return out;
}
@@ -69,6 +73,26 @@ const SHELL_EXEMPT: Record<string, string> = {
'skillify/SKILL.md': 'generated from skillify/SKILL.md.tmpl',
};
/** Run only on Linux (the Ubicloud VM or its Linux-only local fallback), where /dev/null is the spelling; remote command strings expand there. */
const SHELL_LINUX_ONLY = (f: string) => f.startsWith('scripts/ubicloud/');
/** Lines of shell text that use GSTACK_BUN_CONFIG with no CONFIG_DEF line before them, or define it any other way. A `local` definition covers only the rest of its function. */
function configDefinitionGaps(text: string): string[] {
const gaps: string[] = [];
let defined = false;
let definedInFunction = false;
text.split('\n').forEach((line, i) => {
if (/^\s*(?:local\s+)?GSTACK_BUN_CONFIG=/.test(line)) {
const local = /^\s*local\s/.test(line);
if (line.trim().replace(/^local\s+/, '') !== CONFIG_DEF) gaps.push(`${i + 1}: redefined: ${line.trim()}`);
else if (local) definedInFunction = true;
else defined = true;
} else if (/^}/.test(line)) definedInFunction = false;
else if (!defined && !definedInFunction && line.includes('${GSTACK_BUN_CONFIG')) gaps.push(`${i + 1}: used before CONFIG_DEF`);
});
return gaps;
}
const isShell = (f: string) => f.endsWith('.sh') || /^#!.*\b(?:ba|z)?sh\b/.test(firstLine(f));
const isSkillSource = (f: string) => f.endsWith('.tmpl') || /^scripts\/resolvers\/.+\.ts$/.test(f)
|| /(^|\/)SKILL\.md$/.test(f) || /\/sections\/[^/]+\.md$/.test(f);
@@ -116,13 +140,41 @@ describe('GSTA-56: gstack never takes configuration from the audited repository'
]) expect(unisolatedBunCalls(ok)).toEqual([]);
});
test('in shipped shell files the detector requires the Windows-safe, fail-closed config spelling (#3415)', () => {
for (const bad of [
`printf x | bun ${FLAGS} -e "console.log(1)"`,
'bun --no-env-file --config="$GSTACK_BUN_CONFIG" -e 1',
'bun --no-env-file --config= -e 1',
'bun --no-env-file --config=NUL run "$X/bin/gstack-design-detect.ts"',
]) expect(unisolatedBunCalls(bad, SHELL_FLAGS)).not.toEqual([]);
for (const ok of [
`printf x | bun ${SHELL_FLAGS} -e "console.log(1)"`,
`exec bun ${SHELL_FLAGS} "$HERE/question-log-hook.ts"`,
`probe_cmd="bun ${SHELL_FLAGS} --eval 'import x from \\"y\\"'"`,
]) expect(unisolatedBunCalls(ok, SHELL_FLAGS)).toEqual([]);
});
test('the config-definition check flags a missing, late or altered GSTACK_BUN_CONFIG definition', () => {
const use = `bun ${SHELL_FLAGS} -e 1`;
expect(configDefinitionGaps(`#!/usr/bin/env bash\n${CONFIG_DEF}\n${use}\n`)).toEqual([]);
expect(configDefinitionGaps(`f() {\n local ${CONFIG_DEF}\n ${use}\n}\n`)).toEqual([]);
expect(configDefinitionGaps(`f() {\n local ${CONFIG_DEF}\n}\ng() {\n ${use}\n}\n`)).toEqual(['5: used before CONFIG_DEF']);
expect(configDefinitionGaps(`#!/usr/bin/env bash\n${use}\n`)).toEqual(['2: used before CONFIG_DEF']);
expect(configDefinitionGaps(`${use}\n${CONFIG_DEF}\n`)).toEqual(['1: used before CONFIG_DEF']);
expect(configDefinitionGaps(`GSTACK_BUN_CONFIG=./bunfig.toml\n${use}\n`)).toEqual(['1: redefined: GSTACK_BUN_CONFIG=./bunfig.toml', '2: used before CONFIG_DEF']);
expect(configDefinitionGaps(`${CONFIG_DEF}\nGSTACK_BUN_CONFIG=\n${use}\n`)).toEqual(['2: redefined: GSTACK_BUN_CONFIG=']);
});
test('shell callers, skill templates, generated skill docs and resolvers pass the isolation flags', () => {
const offenders: string[] = [];
for (const f of TRACKED) {
if (f.startsWith('.github/') || SHELL_EXEMPT[f] || !(isSkillSource(f) || isShell(f))) continue;
fs.readFileSync(path.join(ROOT, f), 'utf8').split('\n').forEach((line, i) => {
for (const call of unisolatedBunCalls(line)) offenders.push(`${f}:${i + 1}: ${call}`);
const flags = isSkillSource(f) || SHELL_LINUX_ONLY(f) ? FLAGS : SHELL_FLAGS;
const text = fs.readFileSync(path.join(ROOT, f), 'utf8');
text.split('\n').forEach((line, i) => {
for (const call of unisolatedBunCalls(line, flags)) offenders.push(`${f}:${i + 1}: ${call}`);
});
if (flags === SHELL_FLAGS) for (const gap of configDefinitionGaps(text)) offenders.push(`${f}:${gap}`);
}
expect(offenders).toEqual([]);
});
+17
View File
@@ -5,6 +5,9 @@
* --config=nul for the native bun.exe, which Bun 1.4.2 reads as an empty
* config; a native Windows spawn has no /dev/null, so TypeScript callers use
* bunIsolationArgs() (lib/bun-isolation.ts) instead of spelling the flag.
* With conversion off (MSYS_NO_PATHCONV=1) bun.exe gets /dev/null itself and
* fails with EINVAL, so gstack's shell files resolve GSTACK_BUN_CONFIG to NUL
* there (#3415).
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
@@ -15,6 +18,8 @@ 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 "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac';
const NO_PATHCONV = { MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*' };
let project = '';
let preloadMarker = '';
@@ -61,6 +66,18 @@ describe('GSTA-56: isolated Bun launches on this platform', () => {
expectIsolated(bash('bun --no-env-file --config=/dev/null run probe.ts'));
});
test('a shipped shell file spelling, with MSYS path conversion off', () => {
expectIsolated(bash(`${CONFIG_DEF}; bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" -e '${PRINT}'`, NO_PATHCONV));
expectIsolated(bash(`${CONFIG_DEF}; bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" run probe.ts`, NO_PATHCONV));
});
test.if(process.platform === 'win32')('control: with MSYS path conversion off, bun.exe cannot open --config=/dev/null', () => {
const r = bash(`bun --no-env-file --config=/dev/null -e '${PRINT}'`, NO_PATHCONV);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('EINVAL');
expect(fs.existsSync(preloadMarker)).toBe(false);
});
test('a TypeScript spawn with bunIsolationArgs()', () => {
expectIsolated(spawnSync(process.execPath, [...bunIsolationArgs(), 'probe.ts'], { cwd: project, encoding: 'utf8', timeout: 60_000 }));
});
+8 -3
View File
@@ -6,10 +6,15 @@ const SCRIPT = path.join(import.meta.dir, "..", "bin", "gstack-learnings-search"
describe("gstack-learnings-search injection prevention", () => {
const script = readFileSync(SCRIPT, "utf-8");
const bunBlockAt = script.indexOf('bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "');
test("the bun -e block the checks below read is present", () => {
expect(bunBlockAt).toBeGreaterThanOrEqual(0);
});
test("no shell interpolation inside bun -e string", () => {
// Extract the bun -e block (everything between `bun -e "` and the closing `"`)
const bunBlock = script.slice(script.indexOf('bun --no-env-file --config=/dev/null -e "'));
const bunBlock = script.slice(bunBlockAt);
// Should NOT contain ${VAR} patterns (shell interpolation)
// These are RCE vectors: a malicious learnings entry with '; rm -rf / ;' in the
@@ -27,7 +32,7 @@ describe("gstack-learnings-search injection prevention", () => {
});
test("uses process.env for all user-controlled values", () => {
const bunBlock = script.slice(script.indexOf('bun --no-env-file --config=/dev/null -e "'));
const bunBlock = script.slice(bunBlockAt);
// Must use process.env for TYPE, QUERY, LIMIT.
// SLUG and CROSS are no longer threaded as env vars inside the bun
@@ -52,7 +57,7 @@ describe("gstack-learnings-search injection prevention", () => {
});
test("current vs cross-project rows distinguished by inline tags, not SLUG env (#1619)", () => {
const bunBlock = script.slice(script.indexOf('bun --no-env-file --config=/dev/null -e "'));
const bunBlock = script.slice(bunBlockAt);
// The bun block must inspect the per-line tag to mark cross-project rows.
// The current shape emits `current\t<json>` or `cross\t<json>` from the
// upstream pipe (via emit_tagged_file). Inside the bun block, the script
+1 -1
View File
@@ -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. */
+2 -2
View File
@@ -210,8 +210,8 @@ describe('setup: heal-first ordering + visibility', () => {
describe('gstack-settings-hook: shared prelude (dedupe key == prune predicate)', () => {
test('every bun script call site uses the shared JS prelude concatenation', () => {
const codeLines = hookBinSrc.split('\n').filter((l) => !l.trim().startsWith('#'));
const bunCalls = codeLines.filter((l) => l.includes('bun --no-env-file --config=/dev/null -e '));
const preludeCalls = codeLines.filter((l) => l.includes(`bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'`));
const bunCalls = codeLines.filter((l) => l.includes('bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '));
const preludeCalls = codeLines.filter((l) => l.includes(`bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'`));
expect(bunCalls.length).toBeGreaterThanOrEqual(6);
expect(preludeCalls.length).toBe(bunCalls.length);
});