forked from garrytan/gstack
-
Notifications
You must be signed in to change notification settings - Fork 0
fix(update-check): default to swxtchio/gstack fork, not public garrytan/gstack #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
swx-geeter
wants to merge
7
commits into
swxtch
Choose a base branch
from
fix/update-check-fork-source
base: swxtch
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b205cd8
feat(gemini): add Google Gemini CLI second-opinion skill
brentyates-swx fdbbdc1
chore: add gstack-safe-update — non-destructive upgrade+rebase wrapper
brentyates-swx 1effcef
fix(safe-update): run ./setup + migrations on the custom branch after…
brentyates-swx 7bf6724
SWX: timeout wrapper escalates to SIGKILL (--kill-after) to reap SIGT…
brentyates-swx 1a9ab15
SWX: gstack-review-sandbox — timeout/lockup-robust full-context revie…
brentyates-swx edafffd
SWX: gemini/codex reviews — prefer full-context sandbox over diff-only
brentyates-swx 97e457f
fix(update-check): default to swxtchio/gstack fork, not public garryt…
swx-geeter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| #!/usr/bin/env bash | ||
| # gstack-gemini-probe: shared helper for /gemini and outside-voice skills. | ||
| # Sourced from template bash blocks; never execute directly. | ||
| # | ||
| # Functions (all prefixed with _gstack_gemini_ for namespace hygiene): | ||
| # _gstack_gemini_auth_probe — auth check (login state + ~/.gemini/ files) | ||
| # _gstack_gemini_version_check — warn on known-bad gemini CLI versions | ||
| # _gstack_gemini_timeout_wrapper — gtimeout -> timeout -> unwrapped fallback | ||
| # _gstack_gemini_log_event — telemetry emission to ~/.gstack/analytics/ | ||
| # | ||
| # Hygiene rules (mirror gstack-codex-probe): | ||
| # - Never set -e / set -u / trap / IFS= / PATH= in this file. | ||
| # - All internal vars prefix with _GSTACK_GEMINI_. | ||
| # - All functions prefix with _gstack_gemini_. | ||
| # - No command execution at source time (only function defs). | ||
|
|
||
| # --- Auth probe ------------------------------------------------------------- | ||
|
|
||
| _gstack_gemini_auth_probe() { | ||
| # Gemini CLI authenticates via Google OAuth (interactive browser flow on | ||
| # first run) and stores tokens under one of: | ||
| # ~/.gemini/ (newer CLI default) | ||
| # ~/.config/google-gemini/ (alternate location) | ||
| # ~/.config/gemini/ (older alternate) | ||
| # Or via env var GEMINI_API_KEY for headless / CI use. | ||
| # | ||
| # We probe in order: env var, then any of the credential file locations. | ||
| # Multi-signal so we don't false-negative for users authenticated via env. | ||
| local _k1 | ||
| _k1=$(printf '%s' "${GEMINI_API_KEY:-}" | tr -d '[:space:]') | ||
| if [ -n "$_k1" ]; then | ||
| echo "AUTH_OK" | ||
| return 0 | ||
| fi | ||
| if [ -d "$HOME/.gemini" ] && [ -n "$(ls -A "$HOME/.gemini" 2>/dev/null)" ]; then | ||
| echo "AUTH_OK" | ||
| return 0 | ||
| fi | ||
| if [ -d "$HOME/.config/google-gemini" ] && [ -n "$(ls -A "$HOME/.config/google-gemini" 2>/dev/null)" ]; then | ||
| echo "AUTH_OK" | ||
| return 0 | ||
| fi | ||
| if [ -d "$HOME/.config/gemini" ] && [ -n "$(ls -A "$HOME/.config/gemini" 2>/dev/null)" ]; then | ||
| echo "AUTH_OK" | ||
| return 0 | ||
| fi | ||
| echo "AUTH_FAILED" | ||
| return 1 | ||
| } | ||
|
|
||
| # --- Version check ---------------------------------------------------------- | ||
|
|
||
| _gstack_gemini_version_check() { | ||
| # Warn on known-bad gemini CLI versions. Anchored regex prevents false | ||
| # positives. Update this list when a new gemini CLI version regresses. | ||
| # | ||
| # Currently no known-bad releases; entries should be added when issues | ||
| # are discovered (e.g., stdin deadlocks, auth regressions, breaking | ||
| # output-format changes). | ||
| local _ver | ||
| _ver=$(gemini --version 2>/dev/null | head -1) | ||
| [ -z "$_ver" ] && return 0 | ||
| # Example pattern (commented out — fill in when needed): | ||
| # if echo "$_ver" | grep -Eq '(^|[^0-9.])0\.38\.(0)([^0-9.]|$)'; then | ||
| # echo "WARN: gemini CLI $_ver has known issues. Upgrade: npm install -g @google/gemini-cli@latest" | ||
| # _gstack_gemini_log_event "gemini_version_warning" | ||
| # fi | ||
| return 0 | ||
| } | ||
|
|
||
| # --- Timeout wrapper -------------------------------------------------------- | ||
|
|
||
| _gstack_gemini_timeout_wrapper() { | ||
| # Resolve wrapper binary: prefer gtimeout (Homebrew coreutils on macOS), | ||
| # fall back to timeout (Linux), else run unwrapped. Arguments: $1 is the | ||
| # duration in seconds; rest is the command to run. | ||
| # | ||
| # --kill-after=10: at $_duration, timeout sends SIGTERM; if the child is still | ||
| # alive 10s later it sends SIGKILL. The gemini node CLI IGNORES SIGTERM, so a | ||
| # plain `timeout $_duration` never reaps a hung review (observed: ran 553s past | ||
| # a 330s cap). The escalation to SIGKILL guarantees the wrapper actually fires. | ||
| local _duration="$1" | ||
| shift | ||
| local _to | ||
| _to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "") | ||
| if [ -n "$_to" ]; then | ||
| "$_to" --kill-after=10 "$_duration" "$@" | ||
| else | ||
| "$@" | ||
| fi | ||
| } | ||
|
|
||
| # --- Telemetry event -------------------------------------------------------- | ||
|
|
||
| _gstack_gemini_log_event() { | ||
| # Emit a telemetry event to ~/.gstack/analytics/skill-usage.jsonl. | ||
| # Gated on $_TEL != "off" (caller sets this from gstack-config). | ||
| # Event types: gemini_timeout, gemini_auth_failed, gemini_cli_missing, | ||
| # gemini_version_warning. | ||
| # Payload schema: {skill, event, duration_s, ts}. NEVER includes prompt | ||
| # content, env var values, or auth tokens. | ||
| local _event="$1" | ||
| local _duration="${2:-0}" | ||
| [ "${_TEL:-off}" = "off" ] && return 0 | ||
| local _ts | ||
| _ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) | ||
| mkdir -p "$HOME/.gstack/analytics" 2>/dev/null | ||
| printf '{"skill":"gemini","event":"%s","duration_s":"%s","ts":"%s"}\n' \ | ||
| "$_event" "$_duration" "$_ts" \ | ||
| >> "$HOME/.gstack/analytics/skill-usage.jsonl" 2>/dev/null || true | ||
| } | ||
|
|
||
| # --- Hang detection helper -------------------------------------------------- | ||
|
|
||
| _gstack_gemini_log_hang() { | ||
| # Called when timeout fires. Emits a structured event for postmortem. | ||
| local _mode="$1" # review | challenge | consult | ||
| local _stderr_bytes="$2" # bytes captured before timeout | ||
| _gstack_gemini_log_event "gemini_hang_${_mode}" "$_stderr_bytes" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| #!/usr/bin/env bash | ||
| # gstack-review-sandbox — sourceable helpers for full-context, sandboxed, | ||
| # timeout-robust cross-model reviews (codex / gemini). | ||
| # | ||
| # WHY: a diff is too small a lens — reviewers must read the whole repo (and the | ||
| # web) or they manufacture confident-wrong "breaking change / field missing" | ||
| # findings (the proving context is in unchanged code, absent from the diff). | ||
| # Codex gets this via `-s read-only`; gemini has no read-only-with-tools mode | ||
| # (its `--approval-mode plan` disables tools entirely), so gemini must run | ||
| # tool-enabled inside a throwaway git worktree off a clean, pushed branch — the | ||
| # worktree + hard timeout + guaranteed cleanup is what makes the unsafe mode | ||
| # safe. The headless tool path is also flaky, so every call is timeout-bounded | ||
| # with a stall watchdog and one bounded retry. | ||
| # | ||
| # Public API: | ||
| # gstack_review_setup_worktree <repo_root> <branch> -> sets GR_WT GR_TIP GR_OUTDIR GR_PGIDFILE | ||
| # gstack_run_reviewer <name> <hard_s> <stall_s> <retries> -- <cmd...> | ||
| # -> prints "<name>:<STATUS>" | ||
| # findings in $GR_OUTDIR/<name>.out | ||
| # gstack_review_cleanup -> trap handler (idempotent) | ||
| # | ||
| # STATUS: OK (output) | CLEAN (exit 0, no output) | TIMEOUT (hung/stalled/killed) | ||
| # | CRASH (nonzero exit / OOM) | ||
| # | ||
| # Robustness: each reviewer runs in its OWN session (setsid) so the whole | ||
| # process tree is killable; `timeout --kill-after` converts a lockup into a | ||
| # bounded exit (TERM, then KILL); a stall watchdog kills early when output | ||
| # stops growing. PGIDs are tracked in a FILE (flock-appended) so tracking | ||
| # survives command-substitution, backgrounding and concurrency — cleanup reads | ||
| # that file and kills every surviving group on EVERY exit path, then removes | ||
| # the worktree. A reviewer that deliberately `setsid`-escapes its own group is | ||
| # an accepted edge (codex/gemini don't in normal use); the worktree removal is | ||
| # the file-level backstop. | ||
| # | ||
| # USAGE (self-contained orchestrator — run both reviewers concurrently in ONE | ||
| # process so the EXIT trap reaps everything): | ||
| # source gstack-review-sandbox | ||
| # trap gstack_review_cleanup EXIT INT TERM | ||
| # gstack_review_setup_worktree "$REPO" "$BRANCH" || exit 1 | ||
| # gstack_run_reviewer codex 480 180 1 -- codex exec "$P" -C "$GR_WT" -s read-only ... >"$GR_OUTDIR/codex.status" & | ||
| # gstack_run_reviewer gemini 480 180 1 -- gemini -y -p "$P" ...(cwd=$GR_WT)... >"$GR_OUTDIR/gemini.status" & | ||
| # wait | ||
| # # read $GR_OUTDIR/{codex,gemini}.{status,out}; synthesize. Trap cleans up. | ||
|
|
||
| GR_WT="" | ||
| GR_TIP="" | ||
| GR_REPO="" | ||
| GR_OUTDIR="" | ||
| GR_PGIDFILE="" | ||
|
|
||
| _gstack_review_timeout_bin() { | ||
| command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || true | ||
| } | ||
|
|
||
| _gstack_review_track_pgid() { | ||
| # Append a PGID to the shared file under an flock so concurrent reviewers | ||
| # don't interleave. The file (not a shell array) is the source of truth so | ||
| # tracking survives $(...) subshells and backgrounding. | ||
| local pgid="$1" | ||
| [ -n "$pgid" ] && [ -n "$GR_PGIDFILE" ] || return 0 | ||
| ( flock 9; printf '%s\n' "$pgid" >>"$GR_PGIDFILE" ) 9>"${GR_PGIDFILE}.lock" 2>/dev/null || \ | ||
| printf '%s\n' "$pgid" >>"$GR_PGIDFILE" 2>/dev/null || true | ||
| } | ||
|
|
||
| gstack_review_setup_worktree() { | ||
| local repo="$1" branch="$2" | ||
| if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then | ||
| echo "gstack-review-sandbox: '$repo' is not a git repo" >&2; return 1 | ||
| fi | ||
| if [ -n "$(git -C "$repo" status --porcelain 2>/dev/null)" ]; then | ||
| echo "gstack-review-sandbox: '$repo' has uncommitted changes — commit/push first" >&2; return 1 | ||
| fi | ||
| GR_TIP="$(git -C "$repo" rev-parse "$branch" 2>/dev/null)" || { | ||
| echo "gstack-review-sandbox: cannot resolve branch '$branch'" >&2; return 1; } | ||
| if git -C "$repo" rev-parse "$branch@{upstream}" >/dev/null 2>&1; then | ||
| local ahead; ahead=$(git -C "$repo" rev-list --count "$branch@{upstream}..$branch" 2>/dev/null || echo 0) | ||
| [ "${ahead:-0}" != "0" ] && echo "gstack-review-sandbox: WARN '$branch' is $ahead commit(s) ahead of upstream (not pushed)" >&2 | ||
| else | ||
| echo "gstack-review-sandbox: WARN '$branch' has no upstream (not pushed)" >&2 | ||
| fi | ||
| GR_REPO="$repo" | ||
| GR_OUTDIR="$(mktemp -d "${TMPDIR:-/tmp}/gstack-review-out-XXXXXX")" | ||
| GR_PGIDFILE="$GR_OUTDIR/.pgids"; : > "$GR_PGIDFILE" | ||
| GR_WT="$(mktemp -d "${TMPDIR:-/tmp}/gstack-review-wt-XXXXXX")/wt" | ||
| if ! git -C "$repo" worktree add --detach "$GR_WT" "$GR_TIP" >/dev/null 2>&1; then | ||
| echo "gstack-review-sandbox: failed to create review worktree" >&2; GR_WT=""; return 1 | ||
| fi | ||
| printf '%s\n' "$GR_WT" | ||
| } | ||
|
|
||
| # Internal: run the command once, fully bounded. Echoes the STATUS token. | ||
| _gstack_review_run_once() { | ||
| local name="$1" hard="$2" stall="$3" out="$4" err="$5"; shift 5 | ||
| local pidf rcf to launch pgid rc | ||
| pidf="$(mktemp)"; rcf="$(mktemp)" | ||
| : > "$out"; : > "$err" | ||
| to="$(_gstack_review_timeout_bin)" | ||
|
|
||
| # Launch in a NEW session (setsid) so the whole tree shares one PGID. The | ||
| # bash wrapper records its own PID (== PGID after setsid) and the real exit | ||
| # code, so classification is correct whether setsid forks or execs. | ||
| if [ -n "$to" ]; then | ||
| setsid bash -c 'echo "$$" >"$1"; "$4" --kill-after=10 "$3" "${@:5}"; echo "$?" >"$2"' \ | ||
| _ "$pidf" "$rcf" "$hard" "$to" "$@" >"$out" 2>"$err" & | ||
| else | ||
| setsid bash -c 'echo "$$" >"$1"; "${@:3}"; echo "$?" >"$2"' \ | ||
| _ "$pidf" "$rcf" "$@" >"$out" 2>"$err" & | ||
| fi | ||
| launch=$! | ||
|
|
||
| pgid=""; local i=0 | ||
| while [ -z "$pgid" ] && [ "$i" -lt 40 ]; do | ||
| pgid="$(cat "$pidf" 2>/dev/null)"; [ -n "$pgid" ] && break | ||
| sleep 0.05; i=$((i+1)) | ||
| done | ||
| _gstack_review_track_pgid "$pgid" | ||
|
|
||
| local killed_stall=0 last_size=-1 stall_acc=0 tick=3 | ||
| while :; do | ||
| [ -s "$rcf" ] && break | ||
| if [ -n "$pgid" ]; then | ||
| kill -0 "-$pgid" 2>/dev/null || break | ||
| else | ||
| kill -0 "$launch" 2>/dev/null || break | ||
| fi | ||
| sleep "$tick" | ||
| if [ "$stall" -gt 0 ]; then | ||
| local sz; sz="$(stat -c %s "$out" 2>/dev/null || wc -c <"$out" 2>/dev/null || echo 0)" | ||
| if [ "$sz" = "$last_size" ]; then | ||
| stall_acc=$((stall_acc + tick)) | ||
| if [ "$stall_acc" -ge "$stall" ]; then | ||
| [ -n "$pgid" ] && kill -KILL "-$pgid" 2>/dev/null | ||
| killed_stall=1; break | ||
| fi | ||
| else | ||
| last_size="$sz"; stall_acc=0 | ||
| fi | ||
| fi | ||
| done | ||
| wait "$launch" 2>/dev/null || true | ||
| rc="$(cat "$rcf" 2>/dev/null || echo "")" | ||
| rm -f "$pidf" "$rcf" | ||
|
|
||
| if [ "$killed_stall" = 1 ]; then echo "TIMEOUT"; return 0; fi | ||
| case "$rc" in | ||
| 0) if [ -s "$out" ]; then echo "OK"; else echo "CLEAN"; fi ;; | ||
| 124|137) echo "TIMEOUT" ;; | ||
| "") echo "TIMEOUT" ;; | ||
| *) echo "CRASH" ;; | ||
| esac | ||
| } | ||
|
|
||
| gstack_run_reviewer() { | ||
| local name="$1" hard="$2" stall="$3" retries="$4"; shift 4 | ||
| [ "$1" = "--" ] && shift | ||
| local out="$GR_OUTDIR/$name.out" err="$GR_OUTDIR/$name.err" | ||
| local attempt=0 status="" | ||
| while :; do | ||
| attempt=$((attempt + 1)) | ||
| status="$(_gstack_review_run_once "$name" "$hard" "$stall" "$out" "$err" "$@")" | ||
| case "$status" in | ||
| OK|CLEAN) break ;; | ||
| TIMEOUT|CRASH) | ||
| [ "$attempt" -gt "$retries" ] && break | ||
| echo "gstack-review-sandbox: $name $status (attempt $attempt) — retrying" >&2 | ||
| ;; | ||
| esac | ||
| done | ||
| printf '%s:%s\n' "$name" "$status" | ||
| } | ||
|
|
||
| gstack_review_cleanup() { | ||
| # Kill every tracked process group FIRST (read from the file — survives | ||
| # subshells/concurrency), so nothing holds the worktree busy, then discard + | ||
| # remove the worktree. Idempotent. | ||
| if [ -n "$GR_PGIDFILE" ] && [ -f "$GR_PGIDFILE" ]; then | ||
| local g | ||
| while IFS= read -r g; do | ||
| [ -n "$g" ] && kill -KILL "-$g" 2>/dev/null || true | ||
| done < "$GR_PGIDFILE" | ||
| : > "$GR_PGIDFILE" | ||
| fi | ||
| if [ -n "$GR_WT" ] && [ -d "$GR_WT" ]; then | ||
| local dirty; dirty="$(git -C "$GR_WT" status --porcelain 2>/dev/null)" | ||
| if [ -n "$dirty" ]; then | ||
| echo "gstack-review-sandbox: reviewer wrote to the worktree (discarding):" >&2 | ||
| printf '%s\n' "$dirty" | sed 's/^/ /' >&2 | ||
| fi | ||
| if [ -n "$GR_REPO" ]; then | ||
| git -C "$GR_REPO" worktree remove --force "$GR_WT" >/dev/null 2>&1 || rm -rf "$(dirname "$GR_WT")" | ||
| else | ||
| rm -rf "$(dirname "$GR_WT")" | ||
| fi | ||
| GR_WT="" | ||
| fi | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_SHA_URLis hard-coded toswxtchio/gstack, so if someone overridesGSTACK_REMOTE_REPO(without also settingGSTACK_REMOTE_URL) the SHA-pinned fetch can consult the wrong repo (or fail) and make the update check inconsistent with the override. Other locations where this applies: bin/gstack-update-check:197.Severity: medium
Other Locations
bin/gstack-update-check:197🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.