* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(restamp): gate-ahead keeps the lane CHANGELOG entry when both PRs took the same tentative stamp (GBRA-102 diagnosis fixture) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit43c75a6bb6) * test(tree-receipt): a real gbrain stamp pair is same-modulo-stamps under gbrain committed policy (W1 acceptance) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit6cf53f9fed) * feat(ship-policy): gate_command, full_e2e and min_gstack_version; unknown keys stay fail-closed; validate prints introducing versions Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit878a7123d7) * feat(ship-queue): gstack-ship-queue ready, stamp, verify and gate-ahead (WIP, tests next) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit409a5957ad) * test(ship-queue): stamp journal and resume, verify from a separate clone, gate-ahead attempts, ready Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitade554087b) * docs(ship-policy): Run a shared merge queue; new keys and compatibility rule; troubleshooting anchors; /ship pointer Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7fc73e8892) * fix(ship): shared-queue pointer rides the show-effective ship: line (ship skeleton has 2 bytes of parity headroom) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit5b33d4a015) * test(windows): curate the shared-queue tests out of the Windows subset (shebang spawns) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit8deee14728) * test(fixtures): register the gbrain stamp-pair fixture as free-only Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit563a07aa03) * test(ship-queue): receipt digest contract names lib/ship-queue.ts for its pregate lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit501637c258) * feat(pregate): timing check flags elapsed-time bounds and sleep-then-assert in added test lines (W4) lib/pregate/checks/timing.ts prints TIMING_ASSERTION: <file>:<line> <pattern> and a TIMING_ASSERTIONS summary; gstack-timing-ok: <reason> in any comment syntax on the line or the line above escapes one. It warns (PREGATE_TIMING_ASSERTION): measured on gstack (9 of 47 test-touching commits, 263 hits in the tree) and gbrain (37 of 95 commits; 31 of 38 sites of the five timing-flake fixes), blocking would stop a fifth to two fifths of test changes. Adds the /review checklist rule and the /investigate timing-flake playbook. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita3b86b10b2) * feat(update-check): an equal VERSION with a moved main is an upgrade, via the compare API (W12) When VERSION matches but ls-remote's main SHA differs from this checkout's HEAD, bin/gstack-update-check asks the GitHub compare API (status, ahead_by) through the existing receipted curl and prints UPGRADE_AVAILABLE <ver> <ver>+<n> <sha>; a diverged or unreadable compare is CHECK_FAILED (unknown), and behind/identical is up to date. Never fetches. The cache gains an IDENTITY line (head, compared remote SHA, upstream): an outcome replays only for the same HEAD and upstream, a same-remote refresh reuses it without another compare, and a git checkout ignores a legacy one-line cache (non-git installs keep the one-line format). The snooze is keyed by the SHA. gstack-upgrade's question, snooze note and What's New step handle the four-field line; gstack-skill-end reports it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit421e66d2a4) * fix(pregate): treat planning and evidence markdown as an empty lane A docs-only diff with no .gstack/pregate.json failed lanes as PREGATE_NO_LANE. Planning and evidence Markdown is now a docs/no-tests empty lane. Unmapped source files still fail closed. Co-authored-by: Nick <surlymo@apache.org> (cherry picked from commite8793e59e8) * fix(pregate): keep test-read markdown on its test lane Unmarked Markdown that no test depends on is a docs/no-tests empty lane. A file a test reads, including a fixture, still selects that test through the import graph. Unmapped source and unreferenced test-tree files still fail closed. Co-authored-by: Nick <surlymo@apache.org> (cherry picked from commit7aabbddf3d) * test(pregate): reviewer edges for the docs/no-tests empty lane (#3215) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2fb3fa8613) * test(pregate): requires-remote on a gbrain-shaped repo (ci:ubicloud, UBI_OWNER) (W10) A base pregate.json with gbrain's dependencies and remote_gate "bun run ci:ubicloud" (a package script whose stub needs UBICLOUD_API_KEY, names VMs from UBI_OWNER and prints PREGATE_REMOTE). Under 8 cores the test tier is one obligation naming the command; --remote passes the caller's UBI_OWNER and key through and clears it on the report; without the key the gate's own error shows and the obligation stays uncleared. #3190's path needed no change. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit4d69ff02df) * test(pregate): the ship receipt counts the timing check (7 checks) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitcb03ef67e7) * test(context-budget): gstack-upgrade eager ceiling for the W12 equal-version upgrade question (ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(approval-classes): gstack-approval-classes and ship-receipt citation enforcement (W2, WIP) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit847c7dae3d) * feat(approval-classes): tests, docs, /review and /ship wiring (W2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit269bd5b8b5) * feat(review): state-machines specialist and gstack-state-matrix round-trip check with a captured #6792 regression (W3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit1a634979c6) * fix(review-army): keep the --simplification force flag pinned; approval test timeouts Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit789d392bce) * test(free-fixtures): register the state-matrix-6792 fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit1bacef296d) * test(state-matrix): cover state-machines NEVER_GATE in gstack-specialist-stats Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit859063d56f) * test(fixtures): register the pregate-timing fixtures as free-only (W4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-queue): full_e2e needs bound gate evidence in ready and verify, CI reads every check-run page and commit status, next: quotes its paths (review #3235 P1-1, P1-5, P2-1) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): timing reads a native diff and never passes an unread one The timing check's git diff honored diff.external, textconv and diff.noprefix, so a configured driver hid every added line and the check passed. Run it with --no-ext-diff --no-textconv and explicit a/ b/ prefixes, and report a git diff that fails or does not finish as incomplete (PREGATE_INCOMPLETE), which a warn downgrade does not soften. Review of #3235, sibling of P1-4 (lib/pregate/checks/timing.ts:189). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): only affirmative APPROVED evidence counts, explicit tokens are authoritative, native diffs fail closed (review #3235 P1-2, P1-3, P1-4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(state-matrix): detect reads the native diff, ignoring diff.external and textconv (review #3235 P1-4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(e2e): review-army-state-matrix behavior case on the #6792 fixture (field list 2, W3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review-army): tighten the State Machines dispatch line so /ship stays under its parity size ratio with main's #3231 review-army text Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): only top-level approval lines count; quoted, fenced, indented-code and commented text never does (review #3235 r2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): approval evidence is the first non-blank line only, at column 0 (review #3235 r3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: keep gstack-upgrade under its size budget, exempt state-matrix's fetch hint, fit the periodic plan's 37 slices Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the scope-signal clarity check follows helper-printed signals; the hermetic update-check fixture writes the identity cache Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor: move the release-path result codes to lib/result-codes-release.ts (module-size ratchet); fit the periodic plan's 38 slices Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave B Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.116.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: copy lib/result-codes-release.ts wherever a fixture copies lib/result-codes.ts; the scope-signal check requires the helper's computed emission Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: state-matrix stats test resolves its slug under its own state root (home containment) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate pregate-docs-lane-review out of the Windows subset (runBin shebang, like the other pregate fixtures) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate pregate-remote-gbrain out of the Windows subset (runBin shebang, like pregate-remote) 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> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Nick <surlymo@apache.org>
24 KiB
name, version, description, triggers, allowed-tools
| name | version | description | triggers | allowed-tools | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| gstack-upgrade | 1.1.0 | Upgrade gstack to the latest version. |
|
|
When to invoke this skill
Detects global vs vendored install, runs the upgrade, and shows what's new. Use when asked to "upgrade gstack", "update gstack", or "get latest version".
Voice triggers (speech-to-text aliases): "upgrade the tools", "update the tools", "gee stack upgrade", "g stack upgrade".
/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:
_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 <old> <old>+<n> <sha>, 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":
~/.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.
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":
~/.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
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:
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:
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 <old> <new> refs/gstack/rescue/<oid> 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:
- Run
git status --porcelainandgit rev-list origin/main..HEAD --onelinein$INSTALL_DIR. - If BOTH are empty, the reset is provably safe — run the fallback block below without asking.
- 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.
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):
(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:
_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.
(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):
(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).
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.
INSTALL_DIR_PLACEHOLDER="<install dir from Step 2>"
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 <install dir from Step 2> with the actual install directory before
running. Interpret the DAEMON_CHECK result:
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."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 withbrowse stop— or force it immediately withbrowse --force-restart stop(loses that session's tabs/cookies/logins)."none/dead/current: Nothing to do — say nothing.
Step 5: Write marker + clear cache
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 <PRE_UPGRADE_COMMIT>..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):
- Force a fresh update check (bypass cache):
~/.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.
-
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. -
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:
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."