From 6b69c46a278a583b16aa1297288351c151eaa316 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 13 Mar 2026 22:17:25 -0700 Subject: [PATCH] feat: daily update check + /gstack-upgrade skill (v0.3.4) (#42) * feat: add daily update check script + /gstack-upgrade skill bin/gstack-update-check: pure bash, checks VERSION against remote once/day, outputs UPGRADE_AVAILABLE or JUST_UPGRADED. Uses ~/.gstack/ for state. gstack-upgrade/SKILL.md: new skill with inline upgrade flow for all preambles. Detects global-git, local-git, vendored installs. Shows What's New from CHANGELOG. browse/test/gstack-update-check.test.ts: 10 test cases covering all branch paths. * refactor: remove version check from find-browse, simplify to binary locator Delete checkVersion(), readCache(), writeCache(), fetchRemoteSHA(), resolveSkillDir(), CacheEntry interface, REPO_URL/CACHE_PATH/CACHE_TTL constants, and META output from find-browse.ts. Version checking is now handled by bin/gstack-update-check (previous commit). * feat: add update check preamble to all 9 skills Every skill now runs bin/gstack-update-check on invocation. If an upgrade is available, reads gstack-upgrade/SKILL.md inline upgrade flow. Also adds AskUserQuestion to 5 skills that lacked it (gstack root, browse, qa, retro, setup-browser-cookies) and Bash to plan-eng-review. Simplifies qa and setup-browser-cookies setup blocks (removes META parsing). * chore: bump version and changelog (v0.3.4) Co-Authored-By: Claude Opus 4.6 * fix: remove unused import + add corrupt cache test Address pre-landing review findings: - Remove unused mkdirSync import from gstack-update-check.test.ts - Add Path I test: corrupt cache file falls through to remote fetch Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 22 +++ SKILL.md | 10 ++ SKILL.md.tmpl | 10 ++ TODOS.md | 12 ++ VERSION | 2 +- bin/gstack-update-check | 88 +++++++++++ browse/SKILL.md | 10 ++ browse/SKILL.md.tmpl | 10 ++ browse/bin/find-browse | 4 +- browse/src/find-browse.ts | 131 +---------------- browse/test/find-browse.test.ts | 128 +--------------- browse/test/gstack-update-check.test.ts | 188 ++++++++++++++++++++++++ gstack-upgrade/SKILL.md | 112 ++++++++++++++ plan-ceo-review/SKILL.md | 9 ++ plan-eng-review/SKILL.md | 10 ++ qa/SKILL.md | 28 ++-- retro/SKILL.md | 10 ++ review/SKILL.md | 9 ++ setup | 7 + setup-browser-cookies/SKILL.md | 17 ++- ship/SKILL.md | 9 ++ 21 files changed, 556 insertions(+), 270 deletions(-) create mode 100644 TODOS.md create mode 100755 bin/gstack-update-check create mode 100644 browse/test/gstack-update-check.test.ts create mode 100644 gstack-upgrade/SKILL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dd179e2f8087f881107e75641ce8919c28f4a6f9..611a24c86413ccd4699d547202d32c4caae53482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.3.4 — 2026-03-13 + +### Added +- **Daily update check** — all 9 skills now check for new versions once per day via `bin/gstack-update-check` (pure bash, <5ms cached). Prompts user via AskUserQuestion with option to upgrade or defer 24h. +- **`/gstack-upgrade` skill** — standalone upgrade command that detects install type (global-git, local-git, vendored), upgrades, and shows a "What's New" summary from CHANGELOG +- **"Just upgraded" confirmation** — after upgrading, the next skill invocation shows "Running gstack v{new} (just updated!)" via `~/.gstack/just-upgraded-from` marker +- **`AskUserQuestion` added to 5 skills** — gstack (root), browse, qa, retro, setup-browser-cookies now have AskUserQuestion in allowed-tools for upgrade prompts +- **`Bash` added to plan-eng-review** — enables the update check preamble to run in plan review sessions +- `browse/test/gstack-update-check.test.ts` — 10 test cases covering all script branch paths with `GSTACK_REMOTE_URL` env var for test isolation +- `TODOS.md` for tracking deferred work + +### Changed +- **Version check is now one system** — removed SHA-based `checkVersion()` from `browse/src/find-browse.ts` (~120 lines deleted) and `browse/test/find-browse.test.ts` (~100 lines deleted). Replaced by `bin/gstack-update-check` bash script using semver VERSION comparison with 24h cache. +- Simplified `qa/SKILL.md` and `setup-browser-cookies/SKILL.md` setup blocks — removed old `BROWSE_OUTPUT`/`META` parsing, now use simple `find-browse` call +- Updated `browse/bin/find-browse` shim comments to reflect simplified role (binary locator only) + +### Removed +- `checkVersion()`, `readCache()`, `writeCache()`, `fetchRemoteSHA()`, `resolveSkillDir()`, `CacheEntry` interface from `browse/src/find-browse.ts` +- `META:UPDATE_AVAILABLE` protocol from find-browse output +- Old META-based upgrade instructions from qa and setup-browser-cookies SKILL.md files +- Legacy `/tmp/gstack-latest-version` cache file (cleaned up by `setup` script) + ## Unreleased — 2026-03-14 ### Changed diff --git a/SKILL.md b/SKILL.md index c79b710b83d367c476020bd8e7efd0048566e9c4..e83874f213ab6f7ed8153fad9b45d3b523d26d7f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -10,11 +10,21 @@ description: | allowed-tools: - Bash - Read + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # gstack browse: QA Testing & Dogfooding Persistent headless Chromium. First call auto-starts (~3s), then ~100-200ms per command. diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index 0ee150a7582090ecc2b38cf888064f98765f5053..ed6d7fff239f333abec54d04ddf6cba57f425a6c 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -10,9 +10,19 @@ description: | allowed-tools: - Bash - Read + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # gstack browse: QA Testing & Dogfooding Persistent headless Chromium. First call auto-starts (~3s), then ~100-200ms per command. diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000000000000000000000000000000000000..161b0908719d52eb9e908bba8f106bfc37a22a83 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,12 @@ +# TODOS + +## Auto-upgrade mode (zero-prompt) + +**What:** Add a `GSTACK_AUTO_UPGRADE=1` env var or `~/.gstack/config` option that skips the AskUserQuestion prompt and upgrades automatically when a new version is detected. + +**Why:** Power users and CI environments may want zero-friction upgrades without being asked every time. + +**Context:** The current upgrade system (v0.3.4) always prompts via AskUserQuestion. This TODO adds an opt-in bypass. Implementation is ~10 lines in the preamble instructions: check for the env var/config before calling AskUserQuestion, and if set, go straight to the upgrade flow. Depends on the full upgrade system being stable first — wait for user feedback on the prompt-based flow before adding this. + +**Effort:** S (small) +**Priority:** P3 (nice-to-have, revisit after adoption data) diff --git a/VERSION b/VERSION index 1c09c74e221cd58f30240fbcfd9545ed19df54d7..42045acae20fc8d80f15174966be202f7d898d19 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.3 +0.3.4 diff --git a/bin/gstack-update-check b/bin/gstack-update-check new file mode 100755 index 0000000000000000000000000000000000000000..79986ba1c8ed3e48512c69c1fe4623a653b6a09c --- /dev/null +++ b/bin/gstack-update-check @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# gstack-update-check — daily version check for all skills. +# +# Output (one line, or nothing): +# JUST_UPGRADED — marker found from recent upgrade +# UPGRADE_AVAILABLE — remote VERSION differs from local +# (nothing) — up to date or check skipped +# +# Env overrides (for testing): +# GSTACK_DIR — override auto-detected gstack root +# GSTACK_REMOTE_URL — override remote VERSION URL +# GSTACK_STATE_DIR — override ~/.gstack state directory +set -euo pipefail + +GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" +STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" +CACHE_FILE="$STATE_DIR/last-update-check" +MARKER_FILE="$STATE_DIR/just-upgraded-from" +VERSION_FILE="$GSTACK_DIR/VERSION" +REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}" + +# ─── Step 1: Read local version ────────────────────────────── +LOCAL="" +if [ -f "$VERSION_FILE" ]; then + LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')" +fi +if [ -z "$LOCAL" ]; then + exit 0 # No VERSION file → skip check +fi + +# ─── Step 2: Check "just upgraded" marker ───────────────────── +if [ -f "$MARKER_FILE" ]; then + OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')" + rm -f "$MARKER_FILE" + mkdir -p "$STATE_DIR" + echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE" + if [ -n "$OLD" ]; then + echo "JUST_UPGRADED $OLD $LOCAL" + fi + exit 0 +fi + +# ─── Step 3: Check cache freshness (24h = 1440 min) ────────── +if [ -f "$CACHE_FILE" ]; then + # Cache is fresh if modified within 1440 minutes + STALE=$(find "$CACHE_FILE" -mmin +1440 2>/dev/null || true) + if [ -z "$STALE" ]; then + # Cache is fresh — read it + CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)" + case "$CACHED" in + UP_TO_DATE*) + exit 0 + ;; + UPGRADE_AVAILABLE*) + # Verify local version still matches cached old version + CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')" + if [ "$CACHED_OLD" = "$LOCAL" ]; then + echo "$CACHED" + exit 0 + fi + # Local version changed (manual upgrade?) — fall through to re-check + ;; + esac + fi +fi + +# ─── Step 4: Slow path — fetch remote version ──────────────── +mkdir -p "$STATE_DIR" + +REMOTE="" +REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)" +REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')" + +# Validate: must look like a version number (reject HTML error pages) +if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then + # Invalid or empty response — assume up to date + echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE" + exit 0 +fi + +if [ "$LOCAL" = "$REMOTE" ]; then + echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE" + exit 0 +fi + +# Versions differ — upgrade available +echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE" +echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" diff --git a/browse/SKILL.md b/browse/SKILL.md index 5e838649244bc27b0328d6a7b8abc89d7ece48ee..37cbd74194baf605b62d0316d742bb71b9a28914 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -10,11 +10,21 @@ description: | allowed-tools: - Bash - Read + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # browse: QA Testing & Dogfooding Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command. diff --git a/browse/SKILL.md.tmpl b/browse/SKILL.md.tmpl index f0fd0284ad4f6277bb37514334b596da5425f511..6b2f274e70b6e125636f89db24c5f273688798ec 100644 --- a/browse/SKILL.md.tmpl +++ b/browse/SKILL.md.tmpl @@ -10,9 +10,19 @@ description: | allowed-tools: - Bash - Read + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # browse: QA Testing & Dogfooding Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command. diff --git a/browse/bin/find-browse b/browse/bin/find-browse index db07f373ec94a20a24c80c6ca12ba0db5b127035..9cbd7f8106d878a978951dcbc503c18702394650 100755 --- a/browse/bin/find-browse +++ b/browse/bin/find-browse @@ -1,11 +1,11 @@ #!/bin/bash # Shim: delegates to compiled find-browse binary, falls back to basic discovery. -# The compiled binary adds version checking and META signal support. +# The compiled binary handles git root detection for workspace-local installs. DIR="$(cd "$(dirname "$0")/.." && pwd)/dist" if test -x "$DIR/find-browse"; then exec "$DIR/find-browse" "$@" fi -# Fallback: basic discovery (no version check) +# Fallback: basic discovery ROOT=$(git rev-parse --show-toplevel 2>/dev/null) if [ -n "$ROOT" ] && test -x "$ROOT/.claude/skills/gstack/browse/dist/browse"; then echo "$ROOT/.claude/skills/gstack/browse/dist/browse" diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts index 38b987a70a506248ac5592bc957ab377a3def3de..44d76b4c87b3c354dfab11623c6b69a428ca6530 100644 --- a/browse/src/find-browse.ts +++ b/browse/src/find-browse.ts @@ -1,28 +1,14 @@ /** - * find-browse — locate the gstack browse binary + check for updates. + * find-browse — locate the gstack browse binary. * * Compiled to browse/dist/find-browse (standalone binary, no bun runtime needed). - * - * Output protocol: - * Line 1: /path/to/binary (always present) - * Line 2+: META: (optional, 0 or more) - * - * META types: - * META:UPDATE_AVAILABLE — local binary is behind origin/main - * - * All version checks are best-effort: network failures, missing files, and - * cache errors degrade gracefully to outputting only the binary path. + * Outputs the absolute path to the browse binary on stdout, or exits 1 if not found. */ import { existsSync } from 'fs'; -import { readFileSync, writeFileSync } from 'fs'; -import { join, dirname } from 'path'; +import { join } from 'path'; import { homedir } from 'os'; -const REPO_URL = 'https://github.com/garrytan/gstack.git'; -const CACHE_PATH = '/tmp/gstack-latest-version'; -const CACHE_TTL = 14400; // 4 hours in seconds - // ─── Binary Discovery ─────────────────────────────────────────── function getGitRoot(): string | null { @@ -55,114 +41,6 @@ export function locateBinary(): string | null { return null; } -// ─── Version Check ────────────────────────────────────────────── - -interface CacheEntry { - sha: string; - timestamp: number; -} - -function readCache(): CacheEntry | null { - try { - const content = readFileSync(CACHE_PATH, 'utf-8').trim(); - const parts = content.split(/\s+/); - if (parts.length < 2) return null; - const sha = parts[0]; - const timestamp = parseInt(parts[1], 10); - if (!sha || isNaN(timestamp)) return null; - // Validate SHA is hex - if (!/^[0-9a-f]{40}$/i.test(sha)) return null; - return { sha, timestamp }; - } catch { - return null; - } -} - -function writeCache(sha: string, timestamp: number): void { - try { - writeFileSync(CACHE_PATH, `${sha} ${timestamp}\n`); - } catch { - // Cache write failure is non-fatal - } -} - -function fetchRemoteSHA(): string | null { - try { - const proc = Bun.spawnSync(['git', 'ls-remote', REPO_URL, 'refs/heads/main'], { - stdout: 'pipe', - stderr: 'pipe', - timeout: 10_000, // 10s timeout - }); - if (proc.exitCode !== 0) return null; - const output = proc.stdout.toString().trim(); - const sha = output.split(/\s+/)[0]; - if (!sha || !/^[0-9a-f]{40}$/i.test(sha)) return null; - return sha; - } catch { - return null; - } -} - -function resolveSkillDir(binaryPath: string): string | null { - const home = homedir(); - const globalPrefix = join(home, '.claude', 'skills', 'gstack'); - if (binaryPath.startsWith(globalPrefix)) return globalPrefix; - - // Workspace-local: binary is at $ROOT/.claude/skills/gstack/browse/dist/browse - // Skill dir is $ROOT/.claude/skills/gstack - const parts = binaryPath.split('/.claude/skills/gstack/'); - if (parts.length === 2) return parts[0] + '/.claude/skills/gstack'; - - return null; -} - -export function checkVersion(binaryDir: string): string | null { - // Read local version - const versionFile = join(binaryDir, '.version'); - let localSHA: string; - try { - localSHA = readFileSync(versionFile, 'utf-8').trim(); - } catch { - return null; // No .version file → skip check - } - if (!localSHA) return null; - - const now = Math.floor(Date.now() / 1000); - - // Check cache - let remoteSHA: string | null = null; - const cache = readCache(); - if (cache && (now - cache.timestamp) < CACHE_TTL) { - remoteSHA = cache.sha; - } - - // Fetch from remote if cache miss - if (!remoteSHA) { - remoteSHA = fetchRemoteSHA(); - if (remoteSHA) { - writeCache(remoteSHA, now); - } - } - - if (!remoteSHA) return null; // Offline or error → skip check - - // Compare - if (localSHA === remoteSHA) return null; // Up to date - - // Determine skill directory for update command - const binaryPath = join(binaryDir, 'browse'); - const skillDir = resolveSkillDir(binaryPath); - if (!skillDir) return null; - - const payload = JSON.stringify({ - current: localSHA.slice(0, 8), - latest: remoteSHA.slice(0, 8), - command: `cd ${skillDir} && git stash && git fetch origin && git reset --hard origin/main && ./setup`, - }); - - return `META:UPDATE_AVAILABLE ${payload}`; -} - // ─── Main ─────────────────────────────────────────────────────── function main() { @@ -173,9 +51,6 @@ function main() { } console.log(bin); - - const meta = checkVersion(dirname(bin)); - if (meta) console.log(meta); } main(); diff --git a/browse/test/find-browse.test.ts b/browse/test/find-browse.test.ts index 43e1300be7695f1a7b5d0acadd8e87f2b76c241e..7ac5a3f7d5ddf9a74c7fde6a4725694e6650cea4 100644 --- a/browse/test/find-browse.test.ts +++ b/browse/test/find-browse.test.ts @@ -1,130 +1,10 @@ /** - * Tests for find-browse version check logic - * - * Tests the checkVersion() and locateBinary() functions directly. - * Uses temp directories with mock .version files and cache files. + * Tests for find-browse binary locator. */ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { checkVersion, locateBinary } from '../src/find-browse'; -import { mkdtempSync, writeFileSync, rmSync, existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; -import { tmpdir } from 'os'; - -let tempDir: string; - -beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'find-browse-test-')); -}); - -afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - // Clean up test cache - try { rmSync('/tmp/gstack-latest-version'); } catch {} -}); - -describe('checkVersion', () => { - test('returns null when .version file is missing', () => { - const result = checkVersion(tempDir); - expect(result).toBeNull(); - }); - - test('returns null when .version file is empty', () => { - writeFileSync(join(tempDir, '.version'), ''); - const result = checkVersion(tempDir); - expect(result).toBeNull(); - }); - - test('returns null when .version has only whitespace', () => { - writeFileSync(join(tempDir, '.version'), ' \n'); - const result = checkVersion(tempDir); - expect(result).toBeNull(); - }); - - test('returns null when local SHA matches remote (cache hit)', () => { - const sha = 'a'.repeat(40); - writeFileSync(join(tempDir, '.version'), sha); - // Write cache with same SHA, recent timestamp - const now = Math.floor(Date.now() / 1000); - writeFileSync('/tmp/gstack-latest-version', `${sha} ${now}\n`); - - const result = checkVersion(tempDir); - expect(result).toBeNull(); - }); - - test('returns META:UPDATE_AVAILABLE when SHAs differ (cache hit)', () => { - const localSha = 'a'.repeat(40); - const remoteSha = 'b'.repeat(40); - writeFileSync(join(tempDir, '.version'), localSha); - // Create a fake browse binary path so resolveSkillDir works - const browsePath = join(tempDir, 'browse'); - writeFileSync(browsePath, ''); - // Write cache with different SHA, recent timestamp - const now = Math.floor(Date.now() / 1000); - writeFileSync('/tmp/gstack-latest-version', `${remoteSha} ${now}\n`); - - const result = checkVersion(tempDir); - // Result may be null if resolveSkillDir can't determine skill dir from temp path - // That's expected — the META signal requires a known skill dir path - if (result !== null) { - expect(result).toStartWith('META:UPDATE_AVAILABLE'); - const jsonStr = result.replace('META:UPDATE_AVAILABLE ', ''); - const payload = JSON.parse(jsonStr); - expect(payload.current).toBe('a'.repeat(8)); - expect(payload.latest).toBe('b'.repeat(8)); - expect(payload.command).toContain('git stash'); - expect(payload.command).toContain('git reset --hard origin/main'); - expect(payload.command).toContain('./setup'); - } - }); - - test('uses cached SHA when cache is fresh (< 4hr)', () => { - const localSha = 'a'.repeat(40); - const remoteSha = 'a'.repeat(40); - writeFileSync(join(tempDir, '.version'), localSha); - // Cache is 1 hour old — should still be valid - const oneHourAgo = Math.floor(Date.now() / 1000) - 3600; - writeFileSync('/tmp/gstack-latest-version', `${remoteSha} ${oneHourAgo}\n`); - - const result = checkVersion(tempDir); - expect(result).toBeNull(); // SHAs match - }); - - test('treats expired cache as stale', () => { - const localSha = 'a'.repeat(40); - writeFileSync(join(tempDir, '.version'), localSha); - // Cache is 5 hours old — should be stale - const fiveHoursAgo = Math.floor(Date.now() / 1000) - 18000; - writeFileSync('/tmp/gstack-latest-version', `${'b'.repeat(40)} ${fiveHoursAgo}\n`); - - // This will try git ls-remote which may fail in test env — that's OK - // The important thing is it doesn't use the stale cache value - const result = checkVersion(tempDir); - // Result depends on whether git ls-remote succeeds in test environment - // If offline, returns null (graceful degradation) - expect(result === null || typeof result === 'string').toBe(true); - }); - - test('handles corrupt cache file gracefully', () => { - const localSha = 'a'.repeat(40); - writeFileSync(join(tempDir, '.version'), localSha); - writeFileSync('/tmp/gstack-latest-version', 'garbage data here'); - - // Should not throw, should treat as stale - const result = checkVersion(tempDir); - expect(result === null || typeof result === 'string').toBe(true); - }); - - test('handles cache with invalid SHA gracefully', () => { - const localSha = 'a'.repeat(40); - writeFileSync(join(tempDir, '.version'), localSha); - writeFileSync('/tmp/gstack-latest-version', `not-a-sha ${Math.floor(Date.now() / 1000)}\n`); - - // Invalid SHA should be treated as no cache - const result = checkVersion(tempDir); - expect(result === null || typeof result === 'string').toBe(true); - }); -}); +import { describe, test, expect } from 'bun:test'; +import { locateBinary } from '../src/find-browse'; +import { existsSync } from 'fs'; describe('locateBinary', () => { test('returns null when no binary exists at known paths', () => { diff --git a/browse/test/gstack-update-check.test.ts b/browse/test/gstack-update-check.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac674b3d6d8d10133ef7c6dfbd5296824fd83d48 --- /dev/null +++ b/browse/test/gstack-update-check.test.ts @@ -0,0 +1,188 @@ +/** + * Tests for bin/gstack-update-check bash script. + * + * Uses Bun.spawnSync to invoke the script with temp dirs and + * GSTACK_DIR / GSTACK_STATE_DIR / GSTACK_REMOTE_URL env overrides + * for full isolation. + */ + +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-update-check'); + +let gstackDir: string; +let stateDir: string; + +function run(extraEnv: Record = {}) { + const result = Bun.spawnSync(['bash', SCRIPT], { + env: { + ...process.env, + GSTACK_DIR: gstackDir, + GSTACK_STATE_DIR: stateDir, + GSTACK_REMOTE_URL: `file://${join(gstackDir, 'REMOTE_VERSION')}`, + ...extraEnv, + }, + stdout: 'pipe', + stderr: 'pipe', + }); + return { + exitCode: result.exitCode, + stdout: result.stdout.toString().trim(), + stderr: result.stderr.toString().trim(), + }; +} + +beforeEach(() => { + gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-')); + stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-')); +}); + +afterEach(() => { + rmSync(gstackDir, { recursive: true, force: true }); + rmSync(stateDir, { recursive: true, force: true }); +}); + +describe('gstack-update-check', () => { + // ─── Path A: No VERSION file ──────────────────────────────── + test('exits 0 with no output when VERSION file is missing', () => { + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + }); + + // ─── Path B: Empty VERSION file ───────────────────────────── + test('exits 0 with no output when VERSION file is empty', () => { + writeFileSync(join(gstackDir, 'VERSION'), ''); + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + }); + + // ─── Path C: Just-upgraded marker ─────────────────────────── + test('outputs JUST_UPGRADED and deletes marker', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); + writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0'); + // Marker should be deleted + expect(existsSync(join(stateDir, 'just-upgraded-from'))).toBe(false); + // Cache should be written + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── Path D1: Fresh cache, UP_TO_DATE ─────────────────────── + test('exits silently when cache says UP_TO_DATE and is fresh', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + }); + + // ─── Path D2: Fresh cache, UPGRADE_AVAILABLE ──────────────── + test('echoes cached UPGRADE_AVAILABLE when cache is fresh', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + }); + + // ─── Path D3: Fresh cache, but local version changed ──────── + test('re-checks when local version does not match cached old version', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n'); + // Cache says 0.3.3 → 0.4.0 but we're already on 0.4.0 + writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); + // Remote also says 0.4.0 — should be up to date + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); // Up to date after re-check + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── Path E: Versions match (remote fetch) ───────────────── + test('writes UP_TO_DATE cache when versions match', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── Path F: Versions differ (remote fetch) ───────────────── + test('outputs UPGRADE_AVAILABLE when versions differ', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + }); + + // ─── Path G: Invalid remote response ──────────────────────── + test('treats invalid remote response as up to date', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '404 Not Found\n'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── Path H: Curl fails (bad URL) ────────────────────────── + test('exits silently when remote URL is unreachable', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + + const { exitCode, stdout } = run({ + GSTACK_REMOTE_URL: 'file:///nonexistent/path/VERSION', + }); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── Path I: Corrupt cache file ───────────────────────────── + test('falls through to remote fetch when cache is corrupt', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'garbage data here'); + // Remote says same version — should end up UP_TO_DATE + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); + + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe(''); + // Cache should be overwritten with valid content + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── State dir creation ───────────────────────────────────── + test('creates state dir if it does not exist', () => { + const newStateDir = join(stateDir, 'nested', 'dir'); + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); + + const { exitCode } = run({ GSTACK_STATE_DIR: newStateDir }); + expect(exitCode).toBe(0); + expect(existsSync(join(newStateDir, 'last-update-check'))).toBe(true); + }); +}); diff --git a/gstack-upgrade/SKILL.md b/gstack-upgrade/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..a945de17511078d501dc08309c3e01136056c500 --- /dev/null +++ b/gstack-upgrade/SKILL.md @@ -0,0 +1,112 @@ +--- +name: gstack-upgrade +version: 1.0.0 +description: | + Upgrade gstack to the latest version. Detects global vs vendored install, + runs the upgrade, and shows what's new. +allowed-tools: + - Bash + - Read + - 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 + +Use AskUserQuestion: +- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now? Takes ~10 seconds." +- Options: ["Yes, upgrade now", "Later (ask again tomorrow)"] + +**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. + +### 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 ".claude/skills/gstack/.git" ]; then + INSTALL_TYPE="local-git" + INSTALL_DIR=".claude/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" +else + echo "ERROR: gstack not found" + exit 1 +fi +echo "Install type: $INSTALL_TYPE at $INSTALL_DIR" +``` + +### Step 3: Save old version + +```bash +OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown") +``` + +### Step 4: Upgrade + +**For git installs** (global-git, local-git): +```bash +cd "$INSTALL_DIR" +STASH_OUTPUT=$(git stash 2>&1) +git fetch origin +git reset --hard origin/main +./setup +``` +If `$STASH_OUTPUT` contains "Saved working directory", warn the user: "Note: local changes were stashed. Run `git stash pop` in the skill directory to restore them." + +**For vendored installs** (vendored, vendored-global): +```bash +PARENT=$(dirname "$INSTALL_DIR") +TMP_DIR=$(mktemp -d) +git clone --depth 1 https://github.com/garrytan/gstack.git "$TMP_DIR/gstack" +mv "$INSTALL_DIR" "$INSTALL_DIR.bak" +mv "$TMP_DIR/gstack" "$INSTALL_DIR" +cd "$INSTALL_DIR" && ./setup +rm -rf "$INSTALL_DIR.bak" "$TMP_DIR" +``` + +### Step 5: Write marker + clear cache + +```bash +mkdir -p ~/.gstack +echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from +rm -f ~/.gstack/last-update-check +``` + +### 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. + +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), follow Steps 2-6 above. If already on the latest version, tell the user: "You're already on the latest version (v{version})." diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 8ac026c700cf61e8eea3c48d163419494f4c5cf9..441eee112ffce3f9bb36d679beb9c0aee3e750c7 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -14,6 +14,15 @@ allowed-tools: - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # Mega Plan Review Mode ## Philosophy diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index 3074aee8f5b361c528c2a5e3101677b7e0cc20c5..0a3fa5940da643b3b91b8ef3fad1f21527c6f06f 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -10,8 +10,18 @@ allowed-tools: - Grep - Glob - AskUserQuestion + - Bash --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # Plan Review Mode Review this plan thoroughly before making any code changes. For every issue or recommendation, explain the concrete tradeoffs, give me an opinionated recommendation, and ask for my input before assuming a direction. diff --git a/qa/SKILL.md b/qa/SKILL.md index 7e834d49b6b51e36adfc7acbdcb8f45fe7027787..dd30fb02114538633a918aac5c0738de799215f5 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -11,8 +11,18 @@ allowed-tools: - Bash - Read - Write + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # /qa: Systematic QA Testing You are a QA engineer. Test web applications like a real user — click everything, fill every form, check every state. Produce a structured report with evidence. @@ -34,18 +44,18 @@ You are a QA engineer. Test web applications like a real user — click everythi **Find the browse binary:** ```bash -BROWSE_OUTPUT=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null) -B=$(echo "$BROWSE_OUTPUT" | head -1) -META=$(echo "$BROWSE_OUTPUT" | grep "^META:" || true) -if [ -z "$B" ]; then - echo "ERROR: browse binary not found" - exit 1 +B=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null) +if [ -n "$B" ]; then + echo "READY: $B" +else + echo "NEEDS_SETUP" fi -echo "READY: $B" -[ -n "$META" ] && echo "$META" ``` -If you see `META:UPDATE_AVAILABLE`: tell the user an update is available, STOP and wait for approval, then run the command from the META payload and re-run the setup check. +If `NEEDS_SETUP`: +1. Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait. +2. Run: `cd && ./setup` +3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash` **Create output directories:** diff --git a/retro/SKILL.md b/retro/SKILL.md index 3469c925b256a6bb1b40f7cbd103965fc3b20be5..e2b97900d745a2807c2f18b5ea73c735cdd50dc8 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -10,8 +10,18 @@ allowed-tools: - Read - Write - Glob + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # /retro — Weekly Engineering Retrospective Generates a comprehensive engineering retrospective analyzing commit history, work patterns, and code quality metrics. Team-aware: identifies the user running the command, then analyzes every contributor with per-person praise and growth opportunities. Designed for a senior IC/CTO-level builder using Claude Code as a force multiplier. diff --git a/review/SKILL.md b/review/SKILL.md index 35075d1648a9348a2fb9f1a5ec5278000344ab09..217e93d90379cd1dc9dd989d01a7e94e4b74b18c 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -14,6 +14,15 @@ allowed-tools: - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # Pre-Landing PR Review You are running the `/review` workflow. Analyze the current branch's diff against main for structural issues that tests don't catch. diff --git a/setup b/setup index 1f1ad0970a37ac189f670c5a33ed4af8bb03af4d..0bf2736b764de52928577a8fdfc73760a636b681 100755 --- a/setup +++ b/setup @@ -85,3 +85,10 @@ else echo " browse: $BROWSE_BIN" echo " (skipped skill symlinks — not inside .claude/skills/)" fi + +# 4. First-time welcome + legacy cleanup +if [ ! -d "$HOME/.gstack" ]; then + mkdir -p "$HOME/.gstack" + echo " Welcome! Run /gstack-upgrade anytime to stay current." +fi +rm -f /tmp/gstack-latest-version diff --git a/setup-browser-cookies/SKILL.md b/setup-browser-cookies/SKILL.md index cc1d143ef39477d5453ad734f1e5b517b5f9f515..378c05ce7e69e1a748885f483a9fc3b4d2444a10 100644 --- a/setup-browser-cookies/SKILL.md +++ b/setup-browser-cookies/SKILL.md @@ -8,8 +8,18 @@ description: | allowed-tools: - Bash - Read + - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # Setup Browser Cookies Import logged-in sessions from your real Chromium browser into the headless browse session. @@ -26,12 +36,9 @@ Import logged-in sessions from your real Chromium browser into the headless brow ### 1. Find the browse binary ```bash -BROWSE_OUTPUT=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null) -B=$(echo "$BROWSE_OUTPUT" | head -1) -META=$(echo "$BROWSE_OUTPUT" | grep "^META:" || true) +B=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null) if [ -n "$B" ]; then echo "READY: $B" - [ -n "$META" ] && echo "$META" else echo "NEEDS_SETUP" fi @@ -42,8 +49,6 @@ If `NEEDS_SETUP`: 2. Run: `cd && ./setup` 3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash` -If you see `META:UPDATE_AVAILABLE`: tell the user an update is available, STOP and wait for approval, then run the command from the META payload and re-run the setup check. - ### 2. Open the cookie picker ```bash diff --git a/ship/SKILL.md b/ship/SKILL.md index ff6a2ca68d2aad19668f9c48482431eeffcffc33..3d8ddd40318d3e27acd77f8f2ae2003411c001dc 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -13,6 +13,15 @@ allowed-tools: - AskUserQuestion --- +## Update Check (run first) + +```bash +_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true) +[ -n "$_UPD" ] && echo "$_UPD" +``` + +If output shows `UPGRADE_AVAILABLE `: 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 `: tell user "Running gstack v{to} (just updated!)" and continue. + # Ship: Fully Automated Ship Workflow You are running the `/ship` workflow. This is a **non-interactive, fully automated** workflow. Do NOT ask for confirmation at any step. The user said `/ship` which means DO IT. Run straight through and output the PR URL at the end.