--- name: gstack-upgrade version: 1.1.0 description: Upgrade gstack to the latest version. triggers: - upgrade gstack - update gstack version - get latest gstack allowed-tools: - Bash - Read - Write - AskUserQuestion --- # /gstack-upgrade Upgrade gstack to the latest version and show what's new. ## Inline upgrade flow This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`. ### Step 1: Ask the user (or auto-upgrade) First, check if auto-upgrade is enabled: ```bash _AUTO="" [ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true" [ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true) echo "AUTO_UPGRADE=$_AUTO" ``` **If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. On setup failure, follow Step 4's install-specific recovery: vendored installs restore their backup; git installs stop with the pre-upgrade commit recorded, without a destructive reset. Never claim restoration unless it actually succeeded. **Otherwise**, use AskUserQuestion: - Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now?" Four fields (`UPGRADE_AVAILABLE + `, main moved): ask "gstack main is **{n} commit(s)** past v{old} (at {sha}). Upgrade now?" - Options: ["Yes, upgrade now", "Always keep me up to date", "Not now", "Never ask again"] **If "Yes, upgrade now":** Proceed to Step 2. **If "Always keep me up to date":** ```bash ~/.claude/skills/gstack/bin/gstack-config set auto_upgrade true ``` Tell user: "Auto-upgrade enabled. Future updates will install automatically." Then proceed to Step 2. **If "Not now":** Write snooze state with escalating backoff (first snooze = 24h, second = 48h, third+ = 1 week), then continue with the current skill. Do not mention the upgrade again. ```bash GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" _SNOOZE_FILE="$GSTACK_STATE_ROOT/update-snoozed" _REMOTE_VER="{new}" _CUR_LEVEL=0 if [ -f "$_SNOOZE_FILE" ]; then _SNOOZED_VER=$(awk '{print $(1)}' "$_SNOOZE_FILE") if [ "$_SNOOZED_VER" = "$_REMOTE_VER" ]; then _CUR_LEVEL=$(awk '{print $(2)}' "$_SNOOZE_FILE") case "$_CUR_LEVEL" in *[!0-9]*) _CUR_LEVEL=0 ;; esac fi fi _NEW_LEVEL=$((_CUR_LEVEL + 1)) [ "$_NEW_LEVEL" -gt 3 ] && _NEW_LEVEL=3 echo "$_REMOTE_VER $_NEW_LEVEL $(date +%s)" > "$_SNOOZE_FILE" ``` Note: `{new}` is the remote version from the `UPGRADE_AVAILABLE` output — substitute it from the update check result. On a four-field line use the SHA (fourth field) as `{new}`, so the snooze ends when main moves. Tell user the snooze duration: "Next reminder in 24h" (or 48h or 1 week, depending on level). Tip: "Set `auto_upgrade: true` in `~/.gstack/config.yaml` for automatic upgrades." **If "Never ask again":** ```bash ~/.claude/skills/gstack/bin/gstack-config set update_check false ``` Tell user: "Update checks disabled. Run `~/.claude/skills/gstack/bin/gstack-config set update_check true` to re-enable." Continue with the current skill. ### Step 2: Detect install type ```bash if [ -d "$HOME/.claude/skills/gstack/.git" ]; then INSTALL_TYPE="global-git" INSTALL_DIR="$HOME/.claude/skills/gstack" elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then INSTALL_TYPE="global-git" INSTALL_DIR="$HOME/.gstack/repos/gstack" elif [ -d ".claude/skills/gstack/.git" ]; then INSTALL_TYPE="local-git" INSTALL_DIR=".claude/skills/gstack" elif [ -d ".agents/skills/gstack/.git" ]; then INSTALL_TYPE="local-git" INSTALL_DIR=".agents/skills/gstack" elif [ -d ".claude/skills/gstack" ]; then INSTALL_TYPE="vendored" INSTALL_DIR=".claude/skills/gstack" elif [ -d "$HOME/.claude/skills/gstack" ]; then INSTALL_TYPE="vendored-global" INSTALL_DIR="$HOME/.claude/skills/gstack" elif _SRC=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT 2>/dev/null) && _SRC=$(awk -F '\t' '$(1) == "claude" && $(6) != "-" { print $(6); exit }' "$_SRC/installs.tsv" 2>/dev/null) && [ -d "$_SRC/.git" ]; then # The install registry names the checkout setup activated for this host (e.g. a ~/gstack clone). INSTALL_TYPE="global-git" INSTALL_DIR="$_SRC" else echo "ERROR: gstack not found" exit 1 fi INSTALL_DIR=$(cd -- "$INSTALL_DIR" && pwd -P) || { echo "ERROR: cannot enter the gstack install directory" >&2; exit 1; } echo "Install type: $INSTALL_TYPE at $INSTALL_DIR" ``` The install type and the absolute directory printed above are used in all subsequent steps. Carry `INSTALL_TYPE`, `INSTALL_DIR`, `OLD_VERSION`, and later `NEW_VERSION` forward explicitly: tool calls may run in fresh shells, so start each later block by assigning them from the captured output (for example `INSTALL_DIR=/abs/path/printed/above`). Do not rely on a prior call's working directory or shell variables. Every block that changes files first checks `INSTALL_DIR` and stops before any git command when it is unset, empty, missing, unreadable, or not gstack's own checkout. ### Step 3: Save old version Use the install directory from Step 2's output below: ```bash OLD_VERSION=$(cat "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/VERSION" 2>/dev/null || echo "unknown") echo "OLD_VERSION=$OLD_VERSION" ``` ### Step 4: Upgrade Use the install type and directory detected in Step 2: **For git installs** (global-git, local-git): Fast-forward first, through `bin/gstack-ff-update`, the same code the session-update auto-upgrade uses: local edits that merge cleanly ride over the update, and an edit that conflicts (or a staged change, or an unmerged file) is saved in a rescue ref instead of being left as conflict markers: ```bash cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1 { [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ] && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $INSTALL_DIR is not a gstack checkout; nothing was changed. Re-run Step 2." >&2; exit 1; } git fetch origin # Same pre-advance check as the auto-updater: the incoming release's Bun floor, # judged by bash whatever this tool shell is (zsh does not word-split). HOLD=$(bash -c '. bin/gstack-bun-version.sh && gstack_bun_incoming_hold . origin/main' 2>/dev/null) && { case "$HOLD" in bun-version-unreadable*) echo "BUN_VERSION_UNREADABLE: $HOLD; nothing was changed" >&2 ;; *) echo "BUN_TOO_OLD: $HOLD; nothing was changed" >&2 ;; esac; exit 1; } PRE_UPGRADE_COMMIT=$(git rev-parse HEAD) echo "PRE_UPGRADE_COMMIT=$PRE_UPGRADE_COMMIT" FF=$(bash bin/gstack-ff-update . origin/main); FF_RC=$? echo "$FF" case "$FF_RC" in 0) if ./setup --refresh-registered; then echo "FF_OK"; else echo "SETUP_FAILED: git update succeeded; stop and inspect setup output (previous commit: $PRE_UPGRADE_COMMIT)" >&2; exit 1; fi ;; 4) echo "FF_REFUSED" ;; *) echo "FF_STOPPED: $FF" >&2; exit 1 ;; esac ``` Setup's `--refresh-registered` refreshes every install this checkout registered (each host, global and this project's), prints the source first, and ends with an `Upgrade summary` of one row per install. Relay those rows as printed. A `failed` row means that host kept its previous install; give its retry command and never say every host was refreshed. A `skipped` row belongs to another checkout or project; pass on its command, do not run it. If the output ends with `FF_OK`, the upgrade is done — skip the fallback below entirely. When the line before it reads `rescued refs/gstack/rescue/ unmerged=<0|1>`, tell the user their conflicting local edits were saved there and relay the `Inspect`, `Export` and `Delete` commands the script printed (export into a scratch checkout with `git apply -3`, never `stash apply` in the install; `unmerged=1` means the rescue needs manual resolution). On `FF_STOPPED`, STOP and relay the reason: nothing was changed. `ff-busy` means another gstack update is running (retry in a minute); `autostash-unrecoverable` names what is in the way (an untracked file the release adds, or another git command); `index-unreadable` means the checkout's index is damaged. On `BUN_TOO_OLD`, STOP: tell the user to run `bun upgrade`, then /gstack-upgrade again. On `BUN_VERSION_UNREADABLE`, STOP: `bun --version` did not print a version gstack can read, so `bun upgrade` may not run either; tell the user to reinstall Bun from https://bun.sh (`curl -fsSL https://bun.sh/install | bash` in Git Bash or a macOS/Linux terminal), check that `bun --version` prints a version such as 1.4.2, then run /gstack-upgrade again (docs/troubleshooting.md#bun-version-unreadable). On `SETUP_FAILED`, STOP; keep user changes and report the recovery commit. There is no `.bak` on the git path. Do not enter the divergence fallback merely because setup failed. Enter it only on `FF_REFUSED` (local commits or a diverged history); network/auth failures stop for repair, not reset. **Fallback (ff-only refused — local commits or divergence).** `git reset --hard` DESTROYS things: a clean tree with unpushed local commits still loses those commits. Gate it: 1. Run `git status --porcelain` and `git rev-list origin/main..HEAD --oneline` in `$INSTALL_DIR`. 2. If BOTH are empty, the reset is provably safe — run the fallback block below without asking. 3. Otherwise ask via AskUserQuestion (one-way door — destructive), listing exactly what will be discarded: each dirty file and each unpushed commit by hash + subject. Options: **A)** Discard them and upgrade (reset) — requires the explicit letter; **B)** Abort the upgrade so the user can rescue their work first (recommended when local commits exist). Never proceed on a vague reply. ```bash cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1 { [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ] && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $INSTALL_DIR is not a gstack checkout; nothing was changed. Re-run Step 2." >&2; exit 1; } # Regenerable render-footprint dirt (setup re-renders it) never goes into the stash. git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' 2>/dev/null || true STASH_OUTPUT=$(git stash 2>&1) git reset --hard origin/main ./setup --refresh-registered ``` If `$STASH_OUTPUT` contains "Saved working directory", warn the user: "Note: local changes were stashed (any modified generated SKILL.md/sections files were discarded first — they regenerate on setup). Run `git stash pop` in the skill directory to restore your own changes." **For vendored installs** (vendored, vendored-global): ```bash (cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: INSTALL_DIR=${INSTALL_DIR:-} is not a gstack install; nothing was changed. Re-run Step 2." >&2; exit 1; } # A stale .bak from a previously crashed upgrade would make the mv below NEST # the live install inside it and the failure-restore arm would "restore" the # stale backup. It may also be the only good copy from that crashed run — # abort and let the human inspect, never delete it silently. [ -e "$INSTALL_DIR.bak" ] && { echo "ERROR: stale backup exists at $INSTALL_DIR.bak (from a previous failed upgrade?) — inspect it, salvage/remove it, then re-run." >&2; exit 1; } TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX") || { echo "ERROR: mktemp failed — aborting upgrade (install untouched)." >&2; exit 1; } git clone --depth 1 https://github.com/garrytan/gstack.git "$TMP_DIR/gstack" || { echo "ERROR: clone failed — aborting upgrade (install untouched)." >&2; rm -rf "$TMP_DIR"; exit 1; } mv "$INSTALL_DIR" "$INSTALL_DIR.bak" || { rm -rf "$TMP_DIR"; exit 1; } if mv "$TMP_DIR/gstack" "$INSTALL_DIR"; then if (cd "$INSTALL_DIR" && ./setup --refresh-registered); then rm -rf "$INSTALL_DIR.bak" "$TMP_DIR" else rm -rf "$INSTALL_DIR" mv "$INSTALL_DIR.bak" "$INSTALL_DIR" || { echo "ERROR: restore failed; backup retained." >&2; exit 1; } rm -rf "$TMP_DIR" echo "ERROR: setup failed; previous install restored." >&2 exit 1 fi else mv "$INSTALL_DIR.bak" "$INSTALL_DIR" echo "ERROR: swap failed — previous install restored; upgrade aborted." >&2 rm -rf "$TMP_DIR" exit 1 fi ``` ### Step 4.5: Handle local vendored copy Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active: ```bash _RESOLVED_PRIMARY=$(cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && pwd -P) || exit 1 _ROOT=$(git rev-parse --show-toplevel 2>/dev/null) LOCAL_GSTACK="" if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then _RESOLVED_LOCAL=$(cd "$_ROOT/.claude/skills/gstack" && pwd -P) if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then LOCAL_GSTACK="$_ROOT/.claude/skills/gstack" fi fi _TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false") echo "LOCAL_GSTACK=$LOCAL_GSTACK" echo "TEAM_MODE=$_TEAM_MODE" ``` **If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth. ```bash (cd -- "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the detection block above and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: LOCAL_GSTACK=${LOCAL_GSTACK:-} is not a gstack copy; nothing was changed." >&2; exit 1; } _ROOT=${LOCAL_GSTACK%/.claude/skills/gstack} { [ "$_ROOT" != "$LOCAL_GSTACK" ] && cd -- "${_ROOT:?}" && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $LOCAL_GSTACK is not the vendored copy at the root of a git repository; nothing was changed." >&2; exit 1; } git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then echo '.claude/skills/gstack/' >> .gitignore fi rm -rf "$LOCAL_GSTACK" ``` Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready." **If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install): ```bash (cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: INSTALL_DIR=${INSTALL_DIR:-} is not a gstack install; nothing was changed." >&2; exit 1; } (cd -- "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the detection block above and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: LOCAL_GSTACK=${LOCAL_GSTACK:-} is not a gstack copy; nothing was changed." >&2; exit 1; } [ -e "$LOCAL_GSTACK.bak" ] && { echo "ERROR: stale vendored backup; inspect it before retrying." >&2; exit 1; } mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak" || exit 1 if cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK" && rm -rf "$LOCAL_GSTACK/.git" && (cd "$LOCAL_GSTACK" && ./setup --refresh-registered); then rm -rf "$LOCAL_GSTACK.bak" echo "LOCAL_SYNC_OK" else rm -rf "$LOCAL_GSTACK" mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK" || { echo "ERROR: restore failed; backup retained." >&2; exit 1; } echo "ERROR: sync failed; previous vendored copy restored." >&2 exit 1 fi ``` Only on `LOCAL_SYNC_OK`, tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready." Otherwise stop and report the recovery outcome; do not continue migrations or announce success. ### Step 4.75: Run version migrations After `./setup` completes, run any migration scripts for versions between the old and new version. Migrations handle state fixes that `./setup` alone can't cover (stale config, orphaned files, directory structure changes). ```bash MIGRATIONS_DIR="${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/gstack-upgrade/migrations" : "${OLD_VERSION:?OLD_VERSION is not set: substitute the value Step 3 printed}" if [ -d "$MIGRATIONS_DIR" ]; then for migration in $(find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V); do # Extract version from filename: v0.15.2.0.sh → 0.15.2.0 m_ver="$(basename "$migration" .sh | sed 's/^v//')" # Run if this migration version is newer than old version # (simple string compare works for dotted versions with same segment count) if [ "$OLD_VERSION" != "unknown" ] && [ "$(printf '%s\n%s' "$OLD_VERSION" "$m_ver" | sort -V | head -1)" = "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$m_ver" ]; then echo "Running migration $m_ver..." # GSTACK_INSTALL_DIR: migrations that clean the INSTALL (not just # ~/.gstack state) default to ~/.claude/skills/gstack when unset — # a repo-local install would silently no-op without this. GSTACK_INSTALL_DIR="$INSTALL_DIR" bash "$migration" || echo " Warning: migration $m_ver had errors (non-fatal)" fi done fi ``` Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is named `v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md for how to add new migrations. ### Step 4.8: Stop any stale daemon (unconditional) A browse daemon started before the upgrade keeps serving the OLD binary's code until it is stopped — it survives `git reset --hard` and `./setup` because the running process holds the old executable. Always run this step, using the install directory detected in Step 2. ```bash INSTALL_DIR_PLACEHOLDER="" NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "") _STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}" if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then echo "DAEMON_CHECK=none (no state file or no fresh build hash)" else DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1) DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1) OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1) if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then echo "DAEMON_CHECK=dead (no live daemon to stop)" elif [ "$OLD_HASH" = "$NEW_HASH" ]; then echo "DAEMON_CHECK=current (daemon already runs the new binary)" elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH" "$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes" else echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH" fi fi ``` Replace `` with the actual install directory before running. Interpret the `DAEMON_CHECK` result: 1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command starts a fresh daemon on the new binary." 2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is busy right now. When it finishes, stop it with `browse stop` — or force it immediately with `browse --force-restart stop` (loses that session's tabs/cookies/logins)." 3. **`none` / `dead` / `current`:** Nothing to do — say nothing. ### Step 5: Write marker + clear cache ```bash GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" mkdir -p "$GSTACK_STATE_ROOT" echo "$OLD_VERSION" > "$GSTACK_STATE_ROOT"/just-upgraded-from rm -f "$GSTACK_STATE_ROOT"/last-update-check rm -f "$GSTACK_STATE_ROOT"/update-snoozed ``` ### Step 6: Show What's New Read `$INSTALL_DIR/CHANGELOG.md`. Find all version entries between the old version and the new version. Summarize as 5-7 bullets grouped by theme. Don't overwhelm — focus on user-facing changes. Skip internal refactors unless they're significant. Four fields: summarize `git -C "$INSTALL_DIR" log --oneline ..HEAD` (≤7 bullets) as "gstack v{old}, main at {sha}". Format: ``` gstack v{new} — upgraded from v{old}! What's new: - [bullet 1] - [bullet 2] - ... Happy shipping! ``` ### Step 7: Continue After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed. --- ## Standalone usage When invoked directly as `/gstack-upgrade` (not from a preamble): 1. Force a fresh update check (bypass cache): ```bash ~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \ .claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true ``` Use the output to determine if an upgrade is available. 2. If `UPGRADE_AVAILABLE` (three or four fields): follow Steps 2-6 above. If `CHECK_FAILED ...`: the remote version could not be read, so the update status is unknown. Show the line (it names the URL that failed) and tell the user the check failed; never report "already on the latest version". Stop. 3. If no output (primary is up to date): check for a stale local vendored copy. Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`). **If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})." **If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready." **If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions: ```bash PRIMARY_VER=$(cat "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/VERSION" 2>/dev/null || echo "unknown") LOCAL_VER=$(cat "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the Step 4.5 detection block}/VERSION" 2>/dev/null || echo "unknown") echo "PRIMARY=$PRIMARY_VER LOCAL=$LOCAL_VER" ``` **If versions differ:** follow the Step 4.5 sync bash block above to update the local copy from the primary. Tell user: "Global v{PRIMARY_VER} is up to date. Updated local vendored copy from v{LOCAL_VER} → v{PRIMARY_VER}. Commit `.claude/skills/gstack/` when you're ready." **If versions match:** tell the user "You're on the latest version (v{PRIMARY_VER}). Global and local vendored copy are both up to date."