feat: smart update check with auto-upgrade, snooze backoff, config CLI (v0.3.9) (#62)

* feat: add bin/gstack-config CLI for reading/writing ~/.gstack/config.yaml

Simple get/set/list interface for persistent gstack configuration.
Used by update-check and upgrade skill for auto_upgrade and update_check settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: smart update check with 12h cache, snooze backoff, config disable

- Reduce cache TTL from 24h to 12h for faster update detection
- Add exponential snooze backoff: 24h → 48h → 1 week (resets on new version)
- Add update_check: false config option to disable checks entirely
- Clear snooze file on just-upgraded
- 14 new tests covering snooze levels, expiry, corruption, and config paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: upgrade skill with auto-upgrade, 4-option prompt, vendored sync

- Auto-upgrade mode via config or GSTACK_AUTO_UPGRADE=1 env var
- 4-option AskUserQuestion: upgrade once, always, not now, never
- Step 4.5: sync local vendored copy after upgrading primary install
- Snooze write with escalating backoff on "Not now"
- Update preamble text in gen-skill-docs for new upgrade flow
- Regenerate all SKILL.md files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: simplify upgrade instructions, move auto-upgrade to completed

README now points to /gstack-upgrade instead of long paste commands.
Auto-upgrade TODO moved to Completed section (v0.3.8).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump version and changelog (v0.3.9)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-14 23:28:02 -07:00
committed by GitHub
parent 41141007c1
commit bb46ca6b21
20 changed files with 588 additions and 45 deletions

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## 0.3.9 — 2026-03-15
### Added
- **`bin/gstack-config` CLI** — simple get/set/list interface for `~/.gstack/config.yaml`. Used by update-check and upgrade skill for persistent settings (auto_upgrade, update_check).
- **Smart update check** — 12h cache TTL (was 24h), exponential snooze backoff (24h → 48h → 1 week) when user declines upgrades, `update_check: false` config option to disable checks entirely. Snooze resets when a new version is released.
- **Auto-upgrade mode** — set `auto_upgrade: true` in config or `GSTACK_AUTO_UPGRADE=1` env var to skip the upgrade prompt and update automatically.
- **4-option upgrade prompt** — "Yes, upgrade now", "Always keep me up to date", "Not now" (snooze), "Never ask again" (disable).
- **Vendored copy sync** — `/gstack-upgrade` now detects and updates local vendored copies in the current project after upgrading the primary install.
- 25 new tests: 11 for gstack-config CLI, 14 for snooze/config paths in update-check.
### Changed
- README upgrade/troubleshooting sections simplified to reference `/gstack-upgrade` instead of long paste commands.
- Upgrade skill template bumped to v1.1.0 with `Write` tool permission for config editing.
- All SKILL.md preambles updated with new upgrade flow description.
## 0.3.8 — 2026-03-14 ## 0.3.8 — 2026-03-14
### Added ### Added

View File

@@ -598,18 +598,16 @@ Run `cd ~/.claude/skills/gstack && ./setup` (or `cd .claude/skills/gstack && ./s
Run `cd ~/.claude/skills/gstack && bun install && bun run build`. This compiles the browser binary. Requires Bun v1.0+. Run `cd ~/.claude/skills/gstack && bun install && bun run build`. This compiles the browser binary. Requires Bun v1.0+.
**Project copy is stale?** **Project copy is stale?**
Re-copy from global: `for s in browse plan-ceo-review plan-eng-review review ship retro qa setup-browser-cookies; do rm -f .claude/skills/$s; done && rm -rf .claude/skills/gstack && cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup` Run `/gstack-upgrade` — it updates both the global install and any vendored project copy automatically.
**`bun` not installed?** **`bun` not installed?**
Install it: `curl -fsSL https://bun.sh/install | bash` Install it: `curl -fsSL https://bun.sh/install | bash`
## Upgrading ## Upgrading
Paste this into Claude Code: Run `/gstack-upgrade` in Claude Code. It detects your install type (global or vendored), upgrades, syncs any project copies, and shows what's new.
> Update gstack: run `cd ~/.claude/skills/gstack && git fetch origin && git reset --hard origin/main && ./setup`. If this project also has gstack at .claude/skills/gstack, update it too: run `for s in browse plan-ceo-review plan-eng-review review ship retro qa setup-browser-cookies; do rm -f .claude/skills/$s; done && rm -rf .claude/skills/gstack && cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup` Or set `auto_upgrade: true` in `~/.gstack/config.yaml` to upgrade automatically whenever a new version is available.
The `setup` script rebuilds the browser binary and re-symlinks skills. It takes a few seconds.
## Uninstalling ## Uninstalling

View File

@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# gstack browse: QA Testing & Dogfooding # gstack browse: QA Testing & Dogfooding

View File

@@ -338,17 +338,6 @@
**Effort:** XS **Effort:** XS
**Priority:** P2 **Priority:** P2
### Auto-upgrade mode (zero-prompt)
**What:** `GSTACK_AUTO_UPGRADE=1` env var or `~/.gstack/config` option that skips the AskUserQuestion prompt and upgrades automatically.
**Why:** Power users and CI environments want zero-friction upgrades.
**Context:** Current upgrade system (v0.3.4) always prompts. This adds opt-in bypass. ~10 lines in preamble instructions.
**Effort:** S
**Priority:** P3
### Eval web dashboard ### Eval web dashboard
**What:** `bun run eval:dashboard` serves local HTML with charts: cost trending, detection rate, pass/fail history. **What:** `bun run eval:dashboard` serves local HTML with charts: cost trending, detection rate, pass/fail history.
@@ -395,3 +384,7 @@
### E2E test cost tracking ### E2E test cost tracking
- Track cumulative API spend, warn if over threshold - Track cumulative API spend, warn if over threshold
**Completed:** v0.3.6 **Completed:** v0.3.6
### Auto-upgrade mode + smart update check
- Config CLI (`bin/gstack-config`), auto-upgrade via `~/.gstack/config.yaml`, 12h cache TTL, exponential snooze backoff (24h→48h→1wk), "never ask again" option, vendored copy sync on upgrade
**Completed:** v0.3.8

View File

@@ -1 +1 @@
0.3.8 0.3.9

38
bin/gstack-config Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# gstack-config — read/write ~/.gstack/config.yaml
#
# Usage:
# gstack-config get <key> — read a config value
# gstack-config set <key> <value> — write a config value
# gstack-config list — show all config
#
# Env overrides (for testing):
# GSTACK_STATE_DIR — override ~/.gstack state directory
set -euo pipefail
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
CONFIG_FILE="$STATE_DIR/config.yaml"
case "${1:-}" in
get)
KEY="${2:?Usage: gstack-config get <key>}"
grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true
;;
set)
KEY="${2:?Usage: gstack-config set <key> <value>}"
VALUE="${3:?Usage: gstack-config set <key> <value>}"
mkdir -p "$STATE_DIR"
if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then
sed -i '' "s/^${KEY}:.*/${KEY}: ${VALUE}/" "$CONFIG_FILE"
else
echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE"
fi
;;
list)
cat "$CONFIG_FILE" 2>/dev/null || true
;;
*)
echo "Usage: gstack-config {get|set|list} [key] [value]"
exit 1
;;
esac

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# gstack-update-check — daily version check for all skills. # gstack-update-check — periodic version check for all skills.
# #
# Output (one line, or nothing): # Output (one line, or nothing):
# JUST_UPGRADED <old> <new> — marker found from recent upgrade # JUST_UPGRADED <old> <new> — marker found from recent upgrade
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local # UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
# (nothing) — up to date or check skipped # (nothing) — up to date, snoozed, disabled, or check skipped
# #
# Env overrides (for testing): # Env overrides (for testing):
# GSTACK_DIR — override auto-detected gstack root # GSTACK_DIR — override auto-detected gstack root
@@ -16,9 +16,65 @@ GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
CACHE_FILE="$STATE_DIR/last-update-check" CACHE_FILE="$STATE_DIR/last-update-check"
MARKER_FILE="$STATE_DIR/just-upgraded-from" MARKER_FILE="$STATE_DIR/just-upgraded-from"
SNOOZE_FILE="$STATE_DIR/update-snoozed"
VERSION_FILE="$GSTACK_DIR/VERSION" VERSION_FILE="$GSTACK_DIR/VERSION"
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}" REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
# ─── Step 0: Check if updates are disabled ────────────────────
_UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true)
if [ "$_UC" = "false" ]; then
exit 0
fi
# ─── Snooze helper ──────────────────────────────────────────
# check_snooze <remote_version>
# Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output).
#
# Snooze file format: <version> <level> <epoch>
# Level durations: 1=24h, 2=48h, 3+=7d
# New version (version mismatch) resets snooze.
check_snooze() {
local remote_ver="$1"
if [ ! -f "$SNOOZE_FILE" ]; then
return 1 # no snooze file → not snoozed
fi
local snoozed_ver snoozed_level snoozed_epoch
snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
# Validate: all three fields must be non-empty
if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
return 1 # corrupt file → not snoozed
fi
# Validate: level and epoch must be integers
case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
# New version dropped? Ignore snooze.
if [ "$snoozed_ver" != "$remote_ver" ]; then
return 1
fi
# Compute snooze duration based on level
local duration
case "$snoozed_level" in
1) duration=86400 ;; # 24 hours
2) duration=172800 ;; # 48 hours
*) duration=604800 ;; # 7 days (level 3+)
esac
local now
now="$(date +%s)"
local expires=$(( snoozed_epoch + duration ))
if [ "$now" -lt "$expires" ]; then
return 0 # still snoozed
fi
return 1 # snooze expired
}
# ─── Step 1: Read local version ────────────────────────────── # ─── Step 1: Read local version ──────────────────────────────
LOCAL="" LOCAL=""
if [ -f "$VERSION_FILE" ]; then if [ -f "$VERSION_FILE" ]; then
@@ -32,6 +88,7 @@ fi
if [ -f "$MARKER_FILE" ]; then if [ -f "$MARKER_FILE" ]; then
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')" OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
rm -f "$MARKER_FILE" rm -f "$MARKER_FILE"
rm -f "$SNOOZE_FILE"
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE" echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
if [ -n "$OLD" ]; then if [ -n "$OLD" ]; then
@@ -40,10 +97,10 @@ if [ -f "$MARKER_FILE" ]; then
exit 0 exit 0
fi fi
# ─── Step 3: Check cache freshness (24h = 1440 min) ────────── # ─── Step 3: Check cache freshness (12h = 720 min) ──────────
if [ -f "$CACHE_FILE" ]; then if [ -f "$CACHE_FILE" ]; then
# Cache is fresh if modified within 1440 minutes # Cache is fresh if modified within 720 minutes
STALE=$(find "$CACHE_FILE" -mmin +1440 2>/dev/null || true) STALE=$(find "$CACHE_FILE" -mmin +720 2>/dev/null || true)
if [ -z "$STALE" ]; then if [ -z "$STALE" ]; then
# Cache is fresh — read it # Cache is fresh — read it
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)" CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
@@ -60,6 +117,10 @@ if [ -f "$CACHE_FILE" ]; then
# Verify local version still matches cached old version # Verify local version still matches cached old version
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')" CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
if [ "$CACHED_OLD" = "$LOCAL" ]; then if [ "$CACHED_OLD" = "$LOCAL" ]; then
CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
if check_snooze "$CACHED_NEW"; then
exit 0 # snoozed — stay quiet
fi
echo "$CACHED" echo "$CACHED"
exit 0 exit 0
fi fi
@@ -90,4 +151,7 @@ fi
# Versions differ — upgrade available # Versions differ — upgrade available
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE" echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
if check_snooze "$REMOTE"; then
exit 0 # snoozed — stay quiet
fi
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"

View File

@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# browse: QA Testing & Dogfooding # browse: QA Testing & Dogfooding

View File

@@ -0,0 +1,125 @@
/**
* Tests for bin/gstack-config bash script.
*
* Uses Bun.spawnSync to invoke the script with temp dirs and
* GSTACK_STATE_DIR env override for full isolation.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-config');
let stateDir: string;
function run(args: string[] = [], extraEnv: Record<string, string> = {}) {
const result = Bun.spawnSync(['bash', SCRIPT, ...args], {
env: {
...process.env,
GSTACK_STATE_DIR: stateDir,
...extraEnv,
},
stdout: 'pipe',
stderr: 'pipe',
});
return {
exitCode: result.exitCode,
stdout: result.stdout.toString().trim(),
stderr: result.stderr.toString().trim(),
};
}
beforeEach(() => {
stateDir = mkdtempSync(join(tmpdir(), 'gstack-config-test-'));
});
afterEach(() => {
rmSync(stateDir, { recursive: true, force: true });
});
describe('gstack-config', () => {
// ─── get ──────────────────────────────────────────────────
test('get on missing file returns empty, exit 0', () => {
const { exitCode, stdout } = run(['get', 'auto_upgrade']);
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
test('get existing key returns value', () => {
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\n');
const { exitCode, stdout } = run(['get', 'auto_upgrade']);
expect(exitCode).toBe(0);
expect(stdout).toBe('true');
});
test('get missing key returns empty', () => {
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\n');
const { exitCode, stdout } = run(['get', 'nonexistent']);
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
test('get returns last value when key appears multiple times', () => {
writeFileSync(join(stateDir, 'config.yaml'), 'foo: bar\nfoo: baz\n');
const { exitCode, stdout } = run(['get', 'foo']);
expect(exitCode).toBe(0);
expect(stdout).toBe('baz');
});
// ─── set ──────────────────────────────────────────────────
test('set creates file and writes key on missing file', () => {
const { exitCode } = run(['set', 'auto_upgrade', 'true']);
expect(exitCode).toBe(0);
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
expect(content).toContain('auto_upgrade: true');
});
test('set appends new key to existing file', () => {
writeFileSync(join(stateDir, 'config.yaml'), 'foo: bar\n');
const { exitCode } = run(['set', 'auto_upgrade', 'true']);
expect(exitCode).toBe(0);
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
expect(content).toContain('foo: bar');
expect(content).toContain('auto_upgrade: true');
});
test('set replaces existing key in-place', () => {
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: false\n');
const { exitCode } = run(['set', 'auto_upgrade', 'true']);
expect(exitCode).toBe(0);
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
expect(content).toContain('auto_upgrade: true');
expect(content).not.toContain('auto_upgrade: false');
});
test('set creates state dir if missing', () => {
const nestedDir = join(stateDir, 'nested', 'dir');
const { exitCode } = run(['set', 'foo', 'bar'], { GSTACK_STATE_DIR: nestedDir });
expect(exitCode).toBe(0);
expect(existsSync(join(nestedDir, 'config.yaml'))).toBe(true);
});
// ─── list ─────────────────────────────────────────────────
test('list shows all keys', () => {
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\nupdate_check: false\n');
const { exitCode, stdout } = run(['list']);
expect(exitCode).toBe(0);
expect(stdout).toContain('auto_upgrade: true');
expect(stdout).toContain('update_check: false');
});
test('list on missing file returns empty, exit 0', () => {
const { exitCode, stdout } = run(['list']);
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
// ─── usage ────────────────────────────────────────────────
test('no args shows usage and exits 1', () => {
const { exitCode, stdout } = run([]);
expect(exitCode).toBe(1);
expect(stdout).toContain('Usage');
});
});

View File

@@ -7,7 +7,7 @@
*/ */
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs'; import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
@@ -38,6 +38,10 @@ function run(extraEnv: Record<string, string> = {}) {
beforeEach(() => { beforeEach(() => {
gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-')); gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-'));
stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-')); stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-'));
// Link real gstack-config so update_check config check works
const binDir = join(gstackDir, 'bin');
mkdirSync(binDir);
symlinkSync(join(import.meta.dir, '..', '..', 'bin', 'gstack-config'), join(binDir, 'gstack-config'));
}); });
afterEach(() => { afterEach(() => {
@@ -45,6 +49,18 @@ afterEach(() => {
rmSync(stateDir, { recursive: true, force: true }); rmSync(stateDir, { recursive: true, force: true });
}); });
function writeSnooze(version: string, level: number, epochSeconds: number) {
writeFileSync(join(stateDir, 'update-snoozed'), `${version} ${level} ${epochSeconds}`);
}
function writeConfig(content: string) {
writeFileSync(join(stateDir, 'config.yaml'), content);
}
function nowEpoch(): number {
return Math.floor(Date.now() / 1000);
}
describe('gstack-update-check', () => { describe('gstack-update-check', () => {
// ─── Path A: No VERSION file ──────────────────────────────── // ─── Path A: No VERSION file ────────────────────────────────
test('exits 0 with no output when VERSION file is missing', () => { test('exits 0 with no output when VERSION file is missing', () => {
@@ -246,4 +262,154 @@ describe('gstack-update-check', () => {
expect(third.exitCode).toBe(0); expect(third.exitCode).toBe(0);
expect(third.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); expect(third.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
}); });
// ─── Snooze tests ───────────────────────────────────────────
test('snoozed level 1 within 24h → silent (cached path)', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeSnooze('0.4.0', 1, nowEpoch() - 3600); // 1h ago (within 24h)
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
test('snoozed level 1 expired (25h ago) → outputs UPGRADE_AVAILABLE', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeSnooze('0.4.0', 1, nowEpoch() - 90000); // 25h ago
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('snoozed level 2 within 48h → silent', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeSnooze('0.4.0', 2, nowEpoch() - 86400); // 24h ago (within 48h)
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
test('snoozed level 2 expired (49h ago) → outputs', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeSnooze('0.4.0', 2, nowEpoch() - 176400); // 49h ago
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('snoozed level 3 within 7d → silent', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeSnooze('0.4.0', 3, nowEpoch() - 518400); // 6d ago (within 7d)
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
test('snoozed level 3 expired (8d ago) → outputs', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeSnooze('0.4.0', 3, nowEpoch() - 691200); // 8d ago
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('snooze ignored when version differs (new version resets snooze)', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.5.0');
// Snoozed for 0.4.0, but remote is now 0.5.0
writeSnooze('0.4.0', 3, nowEpoch() - 60); // very recent
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.5.0');
});
test('corrupt snooze file → outputs normally', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeFileSync(join(stateDir, 'update-snoozed'), 'garbage');
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('non-numeric epoch in snooze file → outputs', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeFileSync(join(stateDir, 'update-snoozed'), '0.4.0 1 abc');
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('non-numeric level in snooze file → outputs', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
writeFileSync(join(stateDir, 'update-snoozed'), `0.4.0 abc ${nowEpoch()}`);
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('snooze respected on remote fetch path (no cache)', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
// No cache file — goes to remote fetch path
writeSnooze('0.4.0', 1, nowEpoch() - 3600); // 1h ago
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('');
// Cache should still be written
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
expect(cache).toContain('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('just-upgraded clears snooze file', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n');
writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n');
writeSnooze('0.4.0', 2, nowEpoch() - 3600);
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0');
expect(existsSync(join(stateDir, 'update-snoozed'))).toBe(false);
});
// ─── Config tests ──────────────────────────────────────────
test('update_check: false disables all checks', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
writeConfig('update_check: false\n');
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('');
// No cache should be written
expect(existsSync(join(stateDir, 'last-update-check'))).toBe(false);
});
test('missing config.yaml does not crash', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
// No config file — should behave normally
const { exitCode, stdout } = run();
expect(exitCode).toBe(0);
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
}); });

View File

@@ -1,12 +1,13 @@
--- ---
name: gstack-upgrade name: gstack-upgrade
version: 1.0.0 version: 1.1.0
description: | description: |
Upgrade gstack to the latest version. Detects global vs vendored install, Upgrade gstack to the latest version. Detects global vs vendored install,
runs the upgrade, and shows what's new. runs the upgrade, and shows what's new.
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write
- AskUserQuestion - AskUserQuestion
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
@@ -20,13 +21,56 @@ Upgrade gstack to the latest version and show what's new.
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`. This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
### Step 1: Ask the user ### Step 1: Ask the user (or auto-upgrade)
Use AskUserQuestion: First, check if auto-upgrade is enabled:
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now? Takes ~10 seconds." ```bash
- Options: ["Yes, upgrade now", "Later (ask again tomorrow)"] _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 "Later":** Run `touch ~/.gstack/last-update-check` to reset the 24h timer and continue with the current skill. Do not mention the upgrade again. **If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. If `./setup` fails during auto-upgrade, restore from backup (`.bak` directory) and warn the user: "Auto-upgrade failed — restored previous version. Run `/gstack-upgrade` manually to retry."
**Otherwise**, use AskUserQuestion:
- Question: "gstack **v{new}** is available (you're on v{old}). 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
_SNOOZE_FILE=~/.gstack/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.
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 ### Step 2: Detect install type
@@ -79,12 +123,40 @@ cd "$INSTALL_DIR" && ./setup
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR" rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
``` ```
### Step 4.5: Sync local vendored copy
After upgrading the primary install, check if there's also a local copy in the current project that needs updating:
```bash
_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)
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
fi
fi
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
```
If `LOCAL_GSTACK` is non-empty, update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
```bash
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
rm -rf "$LOCAL_GSTACK/.git"
cd "$LOCAL_GSTACK" && ./setup
rm -rf "$LOCAL_GSTACK.bak"
```
Tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready."
### Step 5: Write marker + clear cache ### Step 5: Write marker + clear cache
```bash ```bash
mkdir -p ~/.gstack mkdir -p ~/.gstack
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
rm -f ~/.gstack/last-update-check rm -f ~/.gstack/last-update-check
rm -f ~/.gstack/update-snoozed
``` ```
### Step 6: Show What's New ### Step 6: Show What's New

View File

@@ -1,12 +1,13 @@
--- ---
name: gstack-upgrade name: gstack-upgrade
version: 1.0.0 version: 1.1.0
description: | description: |
Upgrade gstack to the latest version. Detects global vs vendored install, Upgrade gstack to the latest version. Detects global vs vendored install,
runs the upgrade, and shows what's new. runs the upgrade, and shows what's new.
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write
- AskUserQuestion - AskUserQuestion
--- ---
@@ -18,13 +19,56 @@ Upgrade gstack to the latest version and show what's new.
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`. This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
### Step 1: Ask the user ### Step 1: Ask the user (or auto-upgrade)
Use AskUserQuestion: First, check if auto-upgrade is enabled:
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now? Takes ~10 seconds." ```bash
- Options: ["Yes, upgrade now", "Later (ask again tomorrow)"] _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 "Later":** Run `touch ~/.gstack/last-update-check` to reset the 24h timer and continue with the current skill. Do not mention the upgrade again. **If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. If `./setup` fails during auto-upgrade, restore from backup (`.bak` directory) and warn the user: "Auto-upgrade failed — restored previous version. Run `/gstack-upgrade` manually to retry."
**Otherwise**, use AskUserQuestion:
- Question: "gstack **v{new}** is available (you're on v{old}). 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
_SNOOZE_FILE=~/.gstack/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.
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 ### Step 2: Detect install type
@@ -77,12 +121,40 @@ cd "$INSTALL_DIR" && ./setup
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR" rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
``` ```
### Step 4.5: Sync local vendored copy
After upgrading the primary install, check if there's also a local copy in the current project that needs updating:
```bash
_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)
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
fi
fi
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
```
If `LOCAL_GSTACK` is non-empty, update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
```bash
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
rm -rf "$LOCAL_GSTACK/.git"
cd "$LOCAL_GSTACK" && ./setup
rm -rf "$LOCAL_GSTACK.bak"
```
Tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready."
### Step 5: Write marker + clear cache ### Step 5: Write marker + clear cache
```bash ```bash
mkdir -p ~/.gstack mkdir -p ~/.gstack
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
rm -f ~/.gstack/last-update-check rm -f ~/.gstack/last-update-check
rm -f ~/.gstack/update-snoozed
``` ```
### Step 6: Show What's New ### Step 6: Show What's New

View File

@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# Mega Plan Review Mode # Mega Plan Review Mode

View File

@@ -22,7 +22,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# Plan Review Mode # Plan Review Mode

View File

@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# /qa: Systematic QA Testing # /qa: Systematic QA Testing

View File

@@ -22,7 +22,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# /retro — Weekly Engineering Retrospective # /retro — Weekly Engineering Retrospective

View File

@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# Pre-Landing PR Review # Pre-Landing PR Review

View File

@@ -102,7 +102,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
\`\`\` \`\`\`
If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.claude/skills/gstack/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, \`touch ~/.gstack/last-update-check\` if no). If \`JUST_UPGRADED <from> <to>\`: tell user "Running gstack v{to} (just updated!)" and continue.`; If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.claude/skills/gstack/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If \`JUST_UPGRADED <from> <to>\`: tell user "Running gstack v{to} (just updated!)" and continue.`;
} }
function generateBrowseSetup(): string { function generateBrowseSetup(): string {

View File

@@ -20,7 +20,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# Setup Browser Cookies # Setup Browser Cookies

View File

@@ -22,7 +22,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
[ -n "$_UPD" ] && echo "$_UPD" || true [ -n "$_UPD" ] && echo "$_UPD" || true
``` ```
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue. If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
# Ship: Fully Automated Ship Workflow # Ship: Fully Automated Ship Workflow