Initial import from garrytan/gstack@026751e (main snapshot via local relay)
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled

Source: https://github.com/garrytan/gstack/commit/026751e
This commit is contained in:
Rocky
2026-05-19 21:18:17 +02:00
commit 834c6db075
797 changed files with 267839 additions and 0 deletions

279
gstack-upgrade/SKILL.md Normal file
View File

@@ -0,0 +1,279 @@
---
name: gstack-upgrade
version: 1.1.0
description: |
Upgrade gstack to the latest version. 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".
triggers:
- upgrade gstack
- update gstack version
- get latest gstack
allowed-tools:
- Bash
- Read
- Write
- AskUserQuestion
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
# /gstack-upgrade
Upgrade gstack to the latest version and show what's new.
## Inline upgrade flow
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
### Step 1: Ask the user (or auto-upgrade)
First, check if auto-upgrade is enabled:
```bash
_AUTO=""
[ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
[ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true)
echo "AUTO_UPGRADE=$_AUTO"
```
**If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. 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="$HOME/.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
```bash
if [ -d "$HOME/.claude/skills/gstack/.git" ]; then
INSTALL_TYPE="global-git"
INSTALL_DIR="$HOME/.claude/skills/gstack"
elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then
INSTALL_TYPE="global-git"
INSTALL_DIR="$HOME/.gstack/repos/gstack"
elif [ -d ".claude/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d ".agents/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".agents/skills/gstack"
elif [ -d ".claude/skills/gstack" ]; then
INSTALL_TYPE="vendored"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d "$HOME/.claude/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.claude/skills/gstack"
else
echo "ERROR: gstack not found"
exit 1
fi
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
```
The install type and directory path printed above will be used in all subsequent steps.
### Step 3: Save old version
Use the install directory from Step 2's output below:
```bash
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
```
### Step 4: Upgrade
Use the install type and directory detected in Step 2:
**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 4.5: Handle local vendored copy
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
```bash
_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
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
echo "TEAM_MODE=$_TEAM_MODE"
```
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
```bash
cd "$_ROOT"
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
echo '.claude/skills/gstack/' >> .gitignore
fi
rm -rf "$LOCAL_GSTACK"
```
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
```bash
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."
If `./setup` fails, restore from backup and warn the user:
```bash
rm -rf "$LOCAL_GSTACK"
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK"
```
Tell user: "Sync failed — restored previous version at `$LOCAL_GSTACK`. Run `/gstack-upgrade` manually to retry."
### Step 4.75: Run version migrations
After `./setup` completes, run any migration scripts for versions between the old
and new version. Migrations handle state fixes that `./setup` alone can't cover
(stale config, orphaned files, directory structure changes).
```bash
MIGRATIONS_DIR="$INSTALL_DIR/gstack-upgrade/migrations"
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..."
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 5: Write marker + clear cache
```bash
mkdir -p ~/.gstack
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
rm -f ~/.gstack/last-update-check
rm -f ~/.gstack/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.
Format:
```
gstack v{new} — upgraded from v{old}!
What's new:
- [bullet 1]
- [bullet 2]
- ...
Happy shipping!
```
### Step 7: Continue
After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
---
## Standalone usage
When invoked directly as `/gstack-upgrade` (not from a preamble):
1. Force a fresh update check (bypass cache):
```bash
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
```
Use the output to determine if an upgrade is available.
2. If `UPGRADE_AVAILABLE <old> <new>`: follow Steps 2-6 above.
3. If no output (primary is up to date): check for a stale local vendored copy.
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
```bash
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
LOCAL_VER=$(cat "$LOCAL_GSTACK/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."

View File

@@ -0,0 +1,281 @@
---
name: gstack-upgrade
version: 1.1.0
description: |
Upgrade gstack to the latest version. 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:
- "upgrade the tools"
- "update the tools"
- "gee stack upgrade"
- "g stack upgrade"
triggers:
- upgrade gstack
- update gstack version
- get latest gstack
allowed-tools:
- Bash
- Read
- Write
- AskUserQuestion
---
# /gstack-upgrade
Upgrade gstack to the latest version and show what's new.
## Inline upgrade flow
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
### Step 1: Ask the user (or auto-upgrade)
First, check if auto-upgrade is enabled:
```bash
_AUTO=""
[ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
[ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true)
echo "AUTO_UPGRADE=$_AUTO"
```
**If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. 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="$HOME/.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
```bash
if [ -d "$HOME/.claude/skills/gstack/.git" ]; then
INSTALL_TYPE="global-git"
INSTALL_DIR="$HOME/.claude/skills/gstack"
elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then
INSTALL_TYPE="global-git"
INSTALL_DIR="$HOME/.gstack/repos/gstack"
elif [ -d ".claude/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d ".agents/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".agents/skills/gstack"
elif [ -d ".claude/skills/gstack" ]; then
INSTALL_TYPE="vendored"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d "$HOME/.claude/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.claude/skills/gstack"
else
echo "ERROR: gstack not found"
exit 1
fi
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
```
The install type and directory path printed above will be used in all subsequent steps.
### Step 3: Save old version
Use the install directory from Step 2's output below:
```bash
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
```
### Step 4: Upgrade
Use the install type and directory detected in Step 2:
**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 4.5: Handle local vendored copy
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
```bash
_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
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
echo "TEAM_MODE=$_TEAM_MODE"
```
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
```bash
cd "$_ROOT"
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
echo '.claude/skills/gstack/' >> .gitignore
fi
rm -rf "$LOCAL_GSTACK"
```
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
```bash
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."
If `./setup` fails, restore from backup and warn the user:
```bash
rm -rf "$LOCAL_GSTACK"
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK"
```
Tell user: "Sync failed — restored previous version at `$LOCAL_GSTACK`. Run `/gstack-upgrade` manually to retry."
### Step 4.75: Run version migrations
After `./setup` completes, run any migration scripts for versions between the old
and new version. Migrations handle state fixes that `./setup` alone can't cover
(stale config, orphaned files, directory structure changes).
```bash
MIGRATIONS_DIR="$INSTALL_DIR/gstack-upgrade/migrations"
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..."
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 5: Write marker + clear cache
```bash
mkdir -p ~/.gstack
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
rm -f ~/.gstack/last-update-check
rm -f ~/.gstack/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.
Format:
```
gstack v{new} — upgraded from v{old}!
What's new:
- [bullet 1]
- [bullet 2]
- ...
Happy shipping!
```
### Step 7: Continue
After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
---
## Standalone usage
When invoked directly as `/gstack-upgrade` (not from a preamble):
1. Force a fresh update check (bypass cache):
```bash
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
```
Use the output to determine if an upgrade is available.
2. If `UPGRADE_AVAILABLE <old> <new>`: follow Steps 2-6 above.
3. If no output (primary is up to date): check for a stale local vendored copy.
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
```bash
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
LOCAL_VER=$(cat "$LOCAL_GSTACK/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."

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Migration: v0.15.2.0 — Fix skill directory structure for unprefixed discovery
#
# What changed: setup now creates real directories with SKILL.md symlinks
# inside instead of directory symlinks. The old pattern (qa -> gstack/qa)
# caused Claude Code to auto-prefix skills as "gstack-qa" even with
# --no-prefix, because Claude sees the symlink target's parent dir name.
#
# What this does: runs gstack-relink to recreate all skill entries using
# the new real-directory pattern. Idempotent — safe to run multiple times.
#
# Affected: users who installed gstack before v0.15.2.0 with --no-prefix
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
if [ -x "$SCRIPT_DIR/bin/gstack-relink" ]; then
echo " [v0.15.2.0] Fixing skill directory structure..."
"$SCRIPT_DIR/bin/gstack-relink" 2>/dev/null || true
fi

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Migration: v0.16.2.0 — Merge per-project resource logs into builder profile
#
# What changed: resource dedup moved from per-project resources-shown.jsonl to
# the global builder-profile.jsonl (single source of truth for all closing state).
#
# What this does: finds all per-project resources-shown.jsonl files and merges
# their URLs into a stub builder-profile entry so existing users don't lose
# their dedup history. Idempotent — safe to run multiple times.
#
# Affected: users who ran /office-hours before this version
set -euo pipefail
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl"
# Find all per-project resource logs
RESOURCE_FILES=$(find "$GSTACK_HOME/projects" -name "resources-shown.jsonl" 2>/dev/null || true)
if [ -z "$RESOURCE_FILES" ]; then
# No per-project resource files exist — clean install, nothing to migrate
exit 0
fi
echo " [v0.16.2.0] Migrating per-project resource logs to builder profile..."
# Collect all unique URLs from all per-project files
ALL_URLS=$(echo "$RESOURCE_FILES" | while read -r f; do
[ -f "$f" ] && cat "$f" 2>/dev/null || true
done | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//' | sort -u)
if [ -z "$ALL_URLS" ]; then
exit 0
fi
# Check if builder-profile already has resource data (idempotency)
if [ -f "$PROFILE_FILE" ] && grep -q "resources_shown" "$PROFILE_FILE" 2>/dev/null; then
# Already has resource data, check if it includes the migrated URLs
EXISTING_URLS=$(grep -o '"resources_shown":\[[^]]*\]' "$PROFILE_FILE" 2>/dev/null | grep -o 'https://[^"]*' | sort -u)
NEW_URLS=$(comm -23 <(echo "$ALL_URLS") <(echo "$EXISTING_URLS") 2>/dev/null || echo "$ALL_URLS")
if [ -z "$NEW_URLS" ]; then
# All URLs already present — nothing to do
exit 0
fi
fi
# Build JSON array of URLs
URL_ARRAY=$(echo "$ALL_URLS" | awk 'BEGIN{printf "["} NR>1{printf ","} {printf "\"%s\"", $0} END{printf "]"}')
# Append a migration stub entry to the builder profile
mkdir -p "$GSTACK_HOME"
echo "{\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"mode\":\"migration\",\"project_slug\":\"_migrated\",\"signal_count\":0,\"signals\":[],\"design_doc\":\"\",\"assignment\":\"\",\"resources_shown\":$URL_ARRAY,\"topics\":[]}" >> "$PROFILE_FILE"
echo " [v0.16.2.0] Migrated $(echo "$ALL_URLS" | wc -l | tr -d ' ') resource URLs to builder profile."

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Migration: v1.0.0.0 — V1 writing style prompt
#
# What changed: tier-≥2 skills default to ELI10 writing style (jargon glossed on
# first use, outcome-framed questions, short sentences). Power users who prefer
# the older V0 prose can set `gstack-config set explain_level terse`.
#
# What this does: writes a "pending prompt" flag file. On the first tier-≥2 skill
# invocation after upgrade, the preamble reads the flag and asks the user once
# whether to keep the new default or opt into terse mode. Flag file is deleted
# after the user answers. Idempotent — safe to run multiple times.
#
# Affected: every user on v0.19.x and below who upgrades to v1.x
set -euo pipefail
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
PROMPTED_FLAG="$GSTACK_HOME/.writing-style-prompted"
PENDING_FLAG="$GSTACK_HOME/.writing-style-prompt-pending"
mkdir -p "$GSTACK_HOME"
# If the user has already answered the prompt at any point, skip.
if [ -f "$PROMPTED_FLAG" ]; then
exit 0
fi
# If the user has already explicitly set explain_level (either way), count that
# as an answer — they've made their choice, don't ask again.
EXPLAIN_LEVEL_SET="$("${HOME}/.claude/skills/gstack/bin/gstack-config" get explain_level 2>/dev/null || true)"
if [ -n "$EXPLAIN_LEVEL_SET" ]; then
touch "$PROMPTED_FLAG"
exit 0
fi
# Write the pending flag — preamble will see it on the first tier-≥2 skill invocation.
touch "$PENDING_FLAG"
echo " [v1.0.0.0] V1 writing style: you'll see a one-time prompt on your next skill run asking if you want the new default (glossed jargon, outcome framing) or the older terse prose."

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Migration: v1.1.3.0 — Remove stale /checkpoint skill installs
#
# Claude Code ships /checkpoint as a native alias for /rewind, which was
# shadowing the gstack checkpoint skill. The skill has been split into
# /context-save + /context-restore. This migration removes the old on-disk
# install so Claude Code's native /checkpoint is no longer shadowed.
#
# Ownership guard: the script only removes the install IF it owns it —
# i.e., the directory or its SKILL.md is a symlink resolving inside
# ~/.claude/skills/gstack/. A user's own /checkpoint skill (regular file,
# or symlink pointing elsewhere) is preserved.
#
# Three supported install shapes to handle:
# 1. ~/.claude/skills/checkpoint is a directory symlink into gstack.
# 2. ~/.claude/skills/checkpoint is a regular directory whose ONLY file
# is a SKILL.md symlink into gstack (gstack's prefix-install shape).
# 3. Anything else → leave alone, print notice.
#
# Idempotent: missing paths are no-ops.
set -euo pipefail
# Guard: refuse to run if HOME is unset or empty. With `set -u`, unset HOME
# errors out, but HOME="" (possible under sudo-without-H, systemd units, some
# CI runners) survives and produces dangerous absolute paths like
# "/.claude/skills/...". Abort cleanly.
if [ -z "${HOME:-}" ]; then
echo " [v1.1.3.0] HOME is unset or empty — skipping migration." >&2
exit 0
fi
SKILLS_DIR="${HOME}/.claude/skills"
OLD_TOPLEVEL="${SKILLS_DIR}/checkpoint"
OLD_NAMESPACED="${SKILLS_DIR}/gstack/checkpoint"
GSTACK_ROOT_REAL=""
# Helper: canonical-path a target (symlink-safe). Prints the resolved path, or
# empty on failure (broken symlink, ENOENT, ELOOP). Both realpath AND the python3
# fallback are tried — a single tool failure shouldn't defeat the ownership
# check. Returns empty string if both fail.
resolve_real() {
local target="$1"
local out=""
if command -v realpath >/dev/null 2>&1; then
out=$(realpath "$target" 2>/dev/null || true)
fi
if [ -z "$out" ] && command -v python3 >/dev/null 2>&1; then
out=$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$target" 2>/dev/null || true)
fi
printf '%s' "$out"
}
# Resolve the canonical path of the gstack skills root. If gstack isn't
# installed here, there's nothing to migrate.
if [ -d "${SKILLS_DIR}/gstack" ]; then
GSTACK_ROOT_REAL=$(resolve_real "${SKILLS_DIR}/gstack")
fi
# Helper: does $1 (canonical path) live inside $2 (canonical path)?
path_inside() {
local inner="$1"
local outer="$2"
[ -n "$inner" ] && [ -n "$outer" ] || return 1
case "$inner" in
"$outer"|"$outer"/*) return 0;;
*) return 1;;
esac
}
removed_any=0
# --- Shape 1: top-level ~/.claude/skills/checkpoint
if [ -L "$OLD_TOPLEVEL" ]; then
# Directory symlink (or file symlink). Canonicalize and check ownership.
target_real=$(resolve_real "$OLD_TOPLEVEL")
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
rm -- "$OLD_TOPLEVEL"
echo " [v1.1.3.0] Removed stale /checkpoint symlink (was shadowing Claude Code's /rewind alias)."
removed_any=1
else
echo " [v1.1.3.0] Leaving $OLD_TOPLEVEL alone — symlink target is outside gstack (or unresolvable)."
fi
elif [ -d "$OLD_TOPLEVEL" ]; then
# Regular directory. Only remove if it contains exactly one file named
# SKILL.md that's a symlink into gstack (gstack's prefix-install shape).
# Use find to count real files, ignoring .DS_Store (macOS sidecars).
file_count=$(find "$OLD_TOPLEVEL" -maxdepth 1 -type f -not -name '.DS_Store' -not -name '._*' 2>/dev/null | wc -l | tr -d ' ')
symlink_count=$(find "$OLD_TOPLEVEL" -maxdepth 1 -type l 2>/dev/null | wc -l | tr -d ' ')
if [ "$file_count" = "0" ] && [ "$symlink_count" = "1" ] && [ -L "$OLD_TOPLEVEL/SKILL.md" ]; then
target_real=$(resolve_real "$OLD_TOPLEVEL/SKILL.md")
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
# Strip macOS sidecars first (not user content), then remove the dir.
find "$OLD_TOPLEVEL" -maxdepth 1 \( -name '.DS_Store' -o -name '._*' \) -type f -delete 2>/dev/null || true
rm -r -- "$OLD_TOPLEVEL"
echo " [v1.1.3.0] Removed stale /checkpoint install directory (gstack prefix-mode)."
removed_any=1
else
echo " [v1.1.3.0] Leaving $OLD_TOPLEVEL alone — SKILL.md symlink target is outside gstack."
fi
else
echo " [v1.1.3.0] Leaving $OLD_TOPLEVEL alone — not a gstack-owned install (has custom content)."
fi
fi
# Missing → no-op (idempotency).
# --- Shape 2: ~/.claude/skills/gstack/checkpoint/
# Ownership guard applies here too: only remove if this path resolves inside the
# gstack skills root. If a user replaced the directory with a symlink pointing
# elsewhere (e.g., at their own fork), respect it.
if [ -L "$OLD_NAMESPACED" ]; then
target_real=$(resolve_real "$OLD_NAMESPACED")
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
rm -- "$OLD_NAMESPACED"
echo " [v1.1.3.0] Removed stale ~/.claude/skills/gstack/checkpoint symlink."
removed_any=1
else
echo " [v1.1.3.0] Leaving $OLD_NAMESPACED alone — symlink target is outside gstack."
fi
elif [ -d "$OLD_NAMESPACED" ]; then
# Regular directory. This is the gstack-prefix install location. Check that
# it resolves to a path inside the gstack root (it should, unless someone
# hand-edited the tree).
target_real=$(resolve_real "$OLD_NAMESPACED")
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
rm -rf -- "$OLD_NAMESPACED"
echo " [v1.1.3.0] Removed stale ~/.claude/skills/gstack/checkpoint/ (replaced by context-save + context-restore)."
removed_any=1
else
echo " [v1.1.3.0] Leaving $OLD_NAMESPACED alone — resolves outside gstack."
fi
fi
if [ "$removed_any" = "1" ]; then
echo " [v1.1.3.0] /checkpoint is now Claude Code's native /rewind alias. Use /context-save to save state and /context-restore to resume."
fi
exit 0

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Migration: v1.17.0.0 — Wire existing brain-sync repos as gbrain federated sources
#
# Pre-1.17.0.0 /setup-gbrain wrote ~/.gstack/consumers.json with a placeholder
# `status: "pending"` and an empty `ingest_url`, expecting a gbrain HTTP
# /ingest-repo endpoint that never shipped. This migration runs the real
# wireup (gbrain sources add + worktree + initial sync) for users who
# already opted into brain-sync but never got the gbrain side connected.
#
# Idempotent: safe to re-run. Skips when:
# - User never opted into brain-sync (gbrain_sync_mode = off or unset)
# - No ~/.gstack/.git (brain-init never ran)
# - The wireup helper is missing on disk (broken install — defensive)
#
# Failure mode: invokes the helper WITHOUT --strict, so a missing/old gbrain
# CLI is a benign skip rather than blocking the rest of /gstack-upgrade.
set -euo pipefail
if [ -z "${HOME:-}" ]; then
echo " [v1.17.0.0] HOME is unset or empty — skipping migration." >&2
exit 0
fi
SKILLS_DIR="${HOME}/.claude/skills"
BIN_DIR="${SKILLS_DIR}/gstack/bin"
CONFIG_BIN="${BIN_DIR}/gstack-config"
WIREUP_BIN="${BIN_DIR}/gstack-gbrain-source-wireup"
# Skip if user never opted into brain-sync.
SYNC_MODE=""
if [ -x "$CONFIG_BIN" ]; then
# Trim whitespace defensively: gstack-config can emit trailing newlines,
# which would mis-classify "off\n" as a non-empty non-off mode.
SYNC_MODE=$("$CONFIG_BIN" get gbrain_sync_mode 2>/dev/null | tr -d '[:space:]' || echo "")
fi
if [ "$SYNC_MODE" = "off" ] || [ -z "$SYNC_MODE" ]; then
exit 0
fi
# Skip if no brain-sync git repo exists.
if [ ! -d "${HOME}/.gstack/.git" ]; then
exit 0
fi
# Skip if helper missing (defensive — should always be present post-upgrade).
if [ ! -x "$WIREUP_BIN" ]; then
echo " [v1.17.0.0] $WIREUP_BIN missing or non-executable — skipping wireup." >&2
exit 0
fi
echo " [v1.17.0.0] Wiring brain-sync repo into gbrain (federated source + initial sync)..."
# No --strict: missing/old gbrain is a benign skip during a batch upgrade.
"$WIREUP_BIN" || {
echo " [v1.17.0.0] Wireup exited non-zero — re-run manually with: $WIREUP_BIN" >&2
}

View File

@@ -0,0 +1,344 @@
#!/usr/bin/env bash
# Migration: v1.27.0.0 — rename gstack-brain-* → gstack-artifacts-*
#
# Phase C of the v1.27.0.0 plan. Hard-rename, no compat shim. Steps:
# 1. gh_repo_renamed — gh/glab repo rename gstack-brain-$USER →
# gstack-artifacts-$USER (skipped on user opt-out)
# 2. remote_txt_renamed — mv ~/.gstack-brain-remote.txt → artifacts-remote.txt
# 3. config_key_renamed — rewrite gbrain_sync_mode → artifacts_sync_mode
# in ~/.gstack/config.yaml
# 4. claude_md_block_rewritten — find-and-replace any existing GBrain
# Configuration block that references "Memory sync"
# 5. sources_swapped — gbrain sources add new (verify) → remove old
# (codex Finding #6: add-before-remove ordering)
# 6. done — write touchfile, delete journal
#
# Interruption-safe via journal at ~/.gstack/.migrations/v1.27.0.0.journal:
# each step writes its name on success; re-entry resumes from the next un-done
# step. Done touchfile at ~/.gstack/.migrations/v1.27.0.0.done.
#
# Three host-mode branches per the plan:
# Local CLI + GitHub — all steps run automatically
# Local CLI + GitLab — same with glab repo rename
# Remote MCP only — steps 1-4 still run; step 5 prints commands for
# the brain admin to run on the brain host
#
# All steps are idempotent. Re-running after partial completion is safe.
set -euo pipefail
if [ -z "${HOME:-}" ]; then
echo " [v1.27.0.0] HOME is unset — skipping migration." >&2
exit 0
fi
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
GSTACK_HOME="${HOME}/.gstack"
SKILLS_DIR="${HOME}/.claude/skills"
BIN_DIR="${SKILLS_DIR}/gstack/bin"
CONFIG_BIN="${BIN_DIR}/gstack-config"
URL_BIN="${BIN_DIR}/gstack-artifacts-url"
MIGRATION_DIR="${GSTACK_HOME}/.migrations"
JOURNAL="${MIGRATION_DIR}/v1.27.0.0.journal"
DONE="${MIGRATION_DIR}/v1.27.0.0.done"
SKIPPED="${MIGRATION_DIR}/v1.27.0.0.skipped-by-user"
USER_NAME="${USER:-$(whoami 2>/dev/null || echo unknown)}"
OLD_REPO_NAME="gstack-brain-${USER_NAME}"
NEW_REPO_NAME="gstack-artifacts-${USER_NAME}"
OLD_REMOTE_TXT="${HOME}/.gstack-brain-remote.txt"
NEW_REMOTE_TXT="${HOME}/.gstack-artifacts-remote.txt"
OLD_SOURCE_ID="${OLD_REPO_NAME}"
NEW_SOURCE_ID="${NEW_REPO_NAME}"
# ---------------------------------------------------------------------------
# Journal helpers
# ---------------------------------------------------------------------------
mkdir -p "$MIGRATION_DIR"
# Already done? exit silently.
[ -f "$DONE" ] && exit 0
# User opted out previously? exit silently. (Re-invoke via
# `/setup-gbrain --rerun-migration` removes this marker.)
[ -f "$SKIPPED" ] && exit 0
journal_done() {
# Returns 0 if the named step is recorded as complete in the journal.
local step="$1"
[ -f "$JOURNAL" ] && grep -q "^${step}$" "$JOURNAL" 2>/dev/null
}
mark_done() {
local step="$1"
echo "$step" >> "$JOURNAL"
}
# ---------------------------------------------------------------------------
# Detect environment + ask once if there's anything to migrate
# ---------------------------------------------------------------------------
# Has the user ever opted into brain sync? Two signals:
# - presence of ~/.gstack-brain-remote.txt (legacy file)
# - presence of ~/.gstack/.git (brain-init ever ran)
HAS_LEGACY_STATE=0
[ -f "$OLD_REMOTE_TXT" ] && HAS_LEGACY_STATE=1
[ -d "$GSTACK_HOME/.git" ] && HAS_LEGACY_STATE=1
# If nothing to migrate, finalize silently.
if [ "$HAS_LEGACY_STATE" = "0" ]; then
echo " [v1.27.0.0] no legacy gstack-brain state detected — nothing to migrate." >&2
touch "$DONE"
rm -f "$JOURNAL" 2>/dev/null || true
exit 0
fi
# Ask once (idempotent: if journal exists from a prior partial run, skip ask).
if [ ! -f "$JOURNAL" ]; then
cat >&2 <<EOF
[v1.27.0.0] gstack-brain has been renamed to gstack-artifacts.
This is a clearer name for what it actually holds: CEO plans, designs,
/investigate reports, retros (i.e. artifacts, not behavioral memory).
This migration will:
1. Rename your private GitHub/GitLab repo "$OLD_REPO_NAME" → "$NEW_REPO_NAME"
2. mv ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt
3. Rename gbrain_sync_mode → artifacts_sync_mode in ~/.gstack/config.yaml
4. Update any "## GBrain Configuration" block in CLAUDE.md
5. Update gbrain federated source registration (local CLI mode)
OR print commands for your brain admin (remote MCP mode)
Each step is journaled so a Ctrl-C mid-flight is safe to re-run.
EOF
if [ -t 0 ]; then
printf " Proceed? [Y/n/skip-for-now]: " >&2
read -r REPLY || REPLY=""
case "$REPLY" in
n|N|no|No|NO)
echo " Skipping migration. Re-run via /setup-gbrain --rerun-migration." >&2
touch "$SKIPPED"
exit 0
;;
skip|skip-for-now|s)
echo " Skipping for now. Will ask again next upgrade." >&2
# Don't write SKIPPED — leave both old + new state untouched, ask again next time.
exit 0
;;
esac
else
# Non-interactive (CI, scripted upgrade): proceed automatically.
echo " (non-interactive: proceeding automatically)" >&2
fi
fi
# ---------------------------------------------------------------------------
# Detect host (gh / glab / manual) for steps 1 + 5
# ---------------------------------------------------------------------------
detect_host() {
# Read the canonical-form remote URL (the legacy file in the migration window).
local url=""
if [ -f "$OLD_REMOTE_TXT" ]; then
url=$(head -1 "$OLD_REMOTE_TXT" 2>/dev/null | tr -d '[:space:]' || echo "")
elif [ -f "$NEW_REMOTE_TXT" ]; then
url=$(head -1 "$NEW_REMOTE_TXT" 2>/dev/null | tr -d '[:space:]' || echo "")
fi
if echo "$url" | grep -q 'github\.com'; then
echo "github"
elif echo "$url" | grep -q 'gitlab'; then
echo "gitlab"
else
echo "manual"
fi
}
HOST=$(detect_host)
# ---------------------------------------------------------------------------
# Detect MCP mode (so step 5 knows whether to execute or print)
# ---------------------------------------------------------------------------
detect_mcp_mode() {
# Cheap probe: ~/.claude.json type field. Defense-in-depth tier 3 only;
# the migration script avoids invoking `claude` to keep upgrade fast.
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.claude.json" ]; then
local t
t=$(jq -r '.mcpServers.gbrain.type // .mcpServers.gbrain.transport // empty' "$HOME/.claude.json" 2>/dev/null)
case "$t" in
url|http|sse) echo "remote-http"; return ;;
stdio) echo "local-stdio"; return ;;
esac
fi
echo "none"
}
MCP_MODE=$(detect_mcp_mode)
# ---------------------------------------------------------------------------
# Step 1: gh/glab repo rename
# ---------------------------------------------------------------------------
if ! journal_done "gh_repo_renamed"; then
echo " [v1.27.0.0] step 1: rename remote repo $OLD_REPO_NAME$NEW_REPO_NAME" >&2
case "$HOST" in
github)
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
# Idempotent: if new name already exists, treat as success.
if gh repo view "$NEW_REPO_NAME" >/dev/null 2>&1; then
echo " repo already named $NEW_REPO_NAME on GitHub — no-op" >&2
mark_done "gh_repo_renamed"
else
if gh repo rename "$NEW_REPO_NAME" --repo "$OLD_REPO_NAME" --yes 2>/dev/null \
|| gh repo edit "$OLD_REPO_NAME" --name "$NEW_REPO_NAME" 2>/dev/null; then
echo " renamed on GitHub" >&2
mark_done "gh_repo_renamed"
else
echo " WARNING: gh rename failed (repo may not exist or permission denied)" >&2
echo " skipping step 1; subsequent steps still run" >&2
mark_done "gh_repo_renamed"
fi
fi
else
echo " gh CLI not available — skipping rename step (manual: gh repo rename ...)" >&2
mark_done "gh_repo_renamed"
fi
;;
gitlab)
if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then
if glab repo view "$NEW_REPO_NAME" >/dev/null 2>&1; then
echo " repo already named $NEW_REPO_NAME on GitLab — no-op" >&2
mark_done "gh_repo_renamed"
else
# GitLab CLI doesn't have a direct rename; user has to do it via API.
echo " glab repo rename isn't a single command on GitLab." >&2
echo " Manual: visit your GitLab project Settings → General → Advanced → Rename" >&2
echo " or use: glab api projects/:id -X PUT -f name=$NEW_REPO_NAME -f path=$NEW_REPO_NAME" >&2
mark_done "gh_repo_renamed"
fi
else
echo " glab not available — manual rename required" >&2
mark_done "gh_repo_renamed"
fi
;;
manual|*)
echo " unknown host (not github/gitlab) — manual rename required" >&2
mark_done "gh_repo_renamed"
;;
esac
fi
# ---------------------------------------------------------------------------
# Step 2: rename ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt
# ---------------------------------------------------------------------------
if ! journal_done "remote_txt_renamed"; then
echo " [v1.27.0.0] step 2: rename ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt" >&2
if [ -f "$OLD_REMOTE_TXT" ] && [ ! -f "$NEW_REMOTE_TXT" ]; then
# Update the URL inside if the rename happened on the host: replace
# gstack-brain-$USER with gstack-artifacts-$USER in the URL.
OLD_URL=$(head -1 "$OLD_REMOTE_TXT" 2>/dev/null)
NEW_URL=$(echo "$OLD_URL" | sed "s|/${OLD_REPO_NAME}|/${NEW_REPO_NAME}|; s|:${OLD_REPO_NAME}|:${NEW_REPO_NAME}|")
echo "$NEW_URL" > "$NEW_REMOTE_TXT"
chmod 600 "$NEW_REMOTE_TXT"
rm -f "$OLD_REMOTE_TXT"
echo " moved + URL rewritten: $OLD_URL$NEW_URL" >&2
elif [ -f "$NEW_REMOTE_TXT" ]; then
echo " new file already exists — no-op" >&2
rm -f "$OLD_REMOTE_TXT" 2>/dev/null || true
else
echo " no $OLD_REMOTE_TXT to migrate — no-op" >&2
fi
mark_done "remote_txt_renamed"
fi
# ---------------------------------------------------------------------------
# Step 3: rename gbrain_sync_mode → artifacts_sync_mode in config.yaml
# ---------------------------------------------------------------------------
if ! journal_done "config_key_renamed"; then
echo " [v1.27.0.0] step 3: rename gbrain_sync_mode → artifacts_sync_mode in config.yaml" >&2
CFG="$GSTACK_HOME/config.yaml"
if [ -f "$CFG" ]; then
# Atomic in-place rewrite with a tmpfile.
TMP=$(mktemp "${CFG}.v1.27.0.0.XXXXXX")
sed -e 's/^gbrain_sync_mode:/artifacts_sync_mode:/' \
-e 's/^gbrain_sync_mode_prompted:/artifacts_sync_mode_prompted:/' \
"$CFG" > "$TMP" && mv "$TMP" "$CFG"
echo " rewritten in place" >&2
else
echo " no $CFG to migrate — no-op" >&2
fi
mark_done "config_key_renamed"
fi
# ---------------------------------------------------------------------------
# Step 4: rewrite CLAUDE.md "## GBrain Configuration" block fields
# ---------------------------------------------------------------------------
if ! journal_done "claude_md_block_rewritten"; then
echo " [v1.27.0.0] step 4: rewrite CLAUDE.md GBrain Configuration block fields" >&2
# Look in cwd's CLAUDE.md (where /setup-gbrain wrote it) and ~/.gstack/CLAUDE.md
# if it exists. We can't know every project's CLAUDE.md; users rerunning
# /setup-gbrain in any project will overwrite that block fresh anyway.
for CMD in "$PWD/CLAUDE.md" "$GSTACK_HOME/CLAUDE.md"; do
[ -f "$CMD" ] || continue
if grep -q "## GBrain Configuration" "$CMD"; then
TMP=$(mktemp "${CMD}.v1.27.0.0.XXXXXX")
sed -e 's/^- Memory sync:/- Artifacts sync:/' "$CMD" > "$TMP" && mv "$TMP" "$CMD"
echo " rewritten field in $CMD" >&2
fi
done
mark_done "claude_md_block_rewritten"
fi
# ---------------------------------------------------------------------------
# Step 5: gbrain sources swap (add-new before remove-old per codex Finding #6)
# ---------------------------------------------------------------------------
if ! journal_done "sources_swapped"; then
echo " [v1.27.0.0] step 5: gbrain federated source rename" >&2
if [ "$MCP_MODE" = "remote-http" ]; then
# Print commands for the brain admin; we can't execute them locally.
cat >&2 <<EOF
Remote MCP detected. The local gbrain CLI can't update the brain's
federated source registration. Send this to your brain admin:
gbrain sources add ${NEW_SOURCE_ID} --path <new-clone-path> --federated
# verify the new source is searching as expected, then:
gbrain sources remove ${OLD_SOURCE_ID} --yes
(Add-new before remove-old keeps search uninterrupted.)
EOF
mark_done "sources_swapped"
elif command -v gbrain >/dev/null 2>&1 && [ -d "$GSTACK_HOME/.git" ]; then
# Local CLI mode. Sources point at the worktree path; rename the source
# ID add-then-remove. The actual on-disk worktree path stays the same.
WORKTREE="${GSTACK_BRAIN_WORKTREE:-$HOME/.gstack-brain-worktree}"
if gbrain sources list 2>/dev/null | grep -q "$OLD_SOURCE_ID"; then
if gbrain sources add "$NEW_SOURCE_ID" --path "$WORKTREE" --federated 2>/dev/null; then
echo " added $NEW_SOURCE_ID" >&2
if gbrain sources remove "$OLD_SOURCE_ID" --yes 2>/dev/null; then
echo " removed $OLD_SOURCE_ID" >&2
else
echo " WARNING: failed to remove $OLD_SOURCE_ID; both registered. Run manually:" >&2
echo " gbrain sources remove $OLD_SOURCE_ID --yes" >&2
fi
else
echo " WARNING: failed to add $NEW_SOURCE_ID. Old source still registered." >&2
fi
else
echo " no $OLD_SOURCE_ID source registered — no-op" >&2
fi
mark_done "sources_swapped"
else
echo " gbrain CLI not available or no ~/.gstack/.git — skipping" >&2
mark_done "sources_swapped"
fi
fi
# ---------------------------------------------------------------------------
# Step 6: finalize (touchfile + clear journal)
# ---------------------------------------------------------------------------
touch "$DONE"
rm -f "$JOURNAL"
echo " [v1.27.0.0] migration complete." >&2
exit 0

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Migration: v1.37.0.0 — split-engine gbrain (remote MCP brain + optional
# local PGLite for code search per worktree).
#
# Per plan D5: prints a ONE-TIME discoverability notice for existing
# Path 4 users who don't yet have a local engine. They learn that
# symbol-aware code search (gbrain code-def / code-refs / code-callers)
# is now available via /setup-gbrain Step 4.5 if they want it.
#
# When to print the notice (state match — all conditions must hold):
# - ~/.claude.json declares mcpServers.gbrain.{type|transport} = http|sse|url
# OR mcpServers.gbrain.url is set (remote-http MCP active)
# - ~/.gbrain/config.json is absent (no local engine yet)
# - User has not previously opted out via:
# ~/.claude/skills/gstack/bin/gstack-config set local_code_index_offered true
#
# When silent: anything else (Path 1/2/3 users, anyone already on PGLite,
# anyone who opted out, anyone without remote-http MCP).
#
# Idempotency: writes a touchfile at ~/.gstack/.migrations/v1.37.0.0.done
# on completion. Re-running this script is silent if the touchfile exists,
# OR if local_code_index_offered=true.
set -euo pipefail
if [ -z "${HOME:-}" ]; then
echo " [v1.37.0.0] HOME is unset — skipping migration." >&2
exit 0
fi
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
MIGRATIONS_DIR="$GSTACK_HOME/.migrations"
DONE_TOUCH="$MIGRATIONS_DIR/v1.37.0.0.done"
CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config"
CLAUDE_JSON="$HOME/.claude.json"
GBRAIN_CONFIG="$HOME/.gbrain/config.json"
mkdir -p "$MIGRATIONS_DIR"
# Idempotency: already-ran skips silently.
if [ -f "$DONE_TOUCH" ]; then
exit 0
fi
# User opt-out skips silently AND records done.
if [ -x "$CONFIG_BIN" ]; then
if [ "$("$CONFIG_BIN" get local_code_index_offered 2>/dev/null)" = "true" ]; then
touch "$DONE_TOUCH"
exit 0
fi
fi
# State match: remote-http MCP active?
is_remote_http_mcp() {
[ -f "$CLAUDE_JSON" ] || return 1
command -v jq >/dev/null 2>&1 || return 1
local mtype murl
mtype=$(jq -r '.mcpServers.gbrain.type // .mcpServers.gbrain.transport // empty' "$CLAUDE_JSON" 2>/dev/null)
murl=$(jq -r '.mcpServers.gbrain.url // empty' "$CLAUDE_JSON" 2>/dev/null)
case "$mtype" in
url|http|sse) return 0 ;;
esac
[ -n "$murl" ] && return 0
return 1
}
# State match: local engine absent?
is_local_engine_missing() {
[ ! -f "$GBRAIN_CONFIG" ]
}
if is_remote_http_mcp && is_local_engine_missing; then
cat <<'NOTICE'
┌──────────────────────────────────────────────────────────────────┐
│ gstack v1.37.0.0 — split-engine gbrain │
│ │
│ Symbol-aware code search is now available on this machine. │
│ Your remote brain at gbrain MCP keeps working as today; you can │
│ add a tiny local PGLite (~30s, no accounts) for `gbrain │
│ code-def` / `code-refs` / `code-callers` queries per worktree. │
│ │
│ Run /setup-gbrain to opt in at Step 4.5. Or skip this notice │
│ permanently: │
│ gstack-config set local_code_index_offered true │
└──────────────────────────────────────────────────────────────────┘
NOTICE
fi
# Always touch done so we don't print again, regardless of state-match outcome.
touch "$DONE_TOUCH"

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Migration: v1.38.1.0 — add root-level design + test-plan patterns to
# .brain-allowlist, .brain-privacy-map.json, and .gitattributes (#1452).
#
# Why a migration: gstack-artifacts-init regenerates these files but also
# does `git commit + push` on ~/.gstack/, which would clobber user state on
# upgrade. Instead, we do targeted per-file in-place repairs.
#
# Per-file independent — if one file is missing we still repair the others.
#
# Idempotent: each insertion is gated on `not already present` so re-running
# the migration is a no-op.
# No `set -e` — we intentionally tolerate per-file failures so other repairs
# still run. `set -u` is fine.
set -u
GSTACK_HOME="${HOME}/.gstack"
ALLOWLIST="${GSTACK_HOME}/.brain-allowlist"
PRIVACY="${GSTACK_HOME}/.brain-privacy-map.json"
GITATTRS="${GSTACK_HOME}/.gitattributes"
MIGRATION_DIR="${GSTACK_HOME}/.migrations"
DONE="${MIGRATION_DIR}/v1.38.1.0.done"
mkdir -p "${MIGRATION_DIR}" 2>/dev/null || true
if [ -f "${DONE}" ]; then
exit 0
fi
NEW_PATTERNS=(
'projects/*/*-design-*.md'
'projects/*/*-test-plan-*.md'
)
added_any=0
# ----- .brain-allowlist ---------------------------------------------------
if [ -f "${ALLOWLIST}" ]; then
for PATTERN in "${NEW_PATTERNS[@]}"; do
if ! grep -Fq -- "${PATTERN}" "${ALLOWLIST}" 2>/dev/null; then
# Insert before USER ADDITIONS marker. BSD sed (-i.bak) compat for macOS;
# the backup file is removed afterward.
if grep -q '^# ---- USER ADDITIONS BELOW' "${ALLOWLIST}" 2>/dev/null; then
sed -i.bak "/^# ---- USER ADDITIONS BELOW/i\\
${PATTERN}
" "${ALLOWLIST}" && rm -f "${ALLOWLIST}.bak"
added_any=1
else
# Marker missing — append at end of file as a fallback. User may have
# custom-edited the file; better to add than skip silently.
printf '%s\n' "${PATTERN}" >> "${ALLOWLIST}"
added_any=1
fi
fi
done
fi
# ----- .brain-privacy-map.json -------------------------------------------
# Uses jq to preserve JSON validity. Skips with a warning if jq is missing.
if [ -f "${PRIVACY}" ]; then
if command -v jq >/dev/null 2>&1; then
for PATTERN in "${NEW_PATTERNS[@]}"; do
if ! jq -e --arg p "${PATTERN}" 'map(select(.pattern == $p)) | length > 0' "${PRIVACY}" >/dev/null 2>&1; then
if jq --arg p "${PATTERN}" '. += [{"pattern": $p, "class": "artifact"}]' "${PRIVACY}" > "${PRIVACY}.tmp" 2>/dev/null; then
mv "${PRIVACY}.tmp" "${PRIVACY}"
added_any=1
else
rm -f "${PRIVACY}.tmp"
echo " [v1.38.1.0] WARN: jq failed to patch ${PRIVACY}; skipping pattern ${PATTERN}." >&2
fi
fi
done
else
echo " [v1.38.1.0] WARN: jq not found; skipping privacy-map repair. Install jq and re-run gstack-upgrade, or run gstack-artifacts-init manually." >&2
fi
fi
# ----- .gitattributes -----------------------------------------------------
if [ -f "${GITATTRS}" ]; then
for PATTERN in "${NEW_PATTERNS[@]}"; do
RULE="${PATTERN} merge=union"
if ! grep -Fq -- "${RULE}" "${GITATTRS}" 2>/dev/null; then
printf '%s\n' "${RULE}" >> "${GITATTRS}"
added_any=1
fi
done
fi
# Mark done. Even if no patches were applied (already-current install), we
# write the touchfile so the migration runs once.
touch "${DONE}"
if [ "${added_any}" = "1" ]; then
echo " [v1.38.1.0] allowlist/privacy-map/gitattributes patched for root-level design + test-plan artifacts (idempotent)" >&2
fi
# NEVER `git commit + push` from this migration. The user controls when the
# patches ship into their federated artifacts repo (next gstack-brain-sync
# --once or a manual commit).
exit 0

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Migration: v1.40.0.0 — add eng-review-test-plan project-root pattern to
# .brain-allowlist, .brain-privacy-map.json, and .gitattributes (#1452 follow-on).
#
# Why a second migration: v1.38.1.0 shipped two of three filenames for #1452
# (`*-design-*.md` and `*-test-plan-*.md`) but missed `/plan-eng-review`'s
# actual filename: `*-eng-review-test-plan-*.md`. The v1.38.1.0 migration has
# a done-marker, so a "fix v1.38.1.0 and re-run" approach silently no-ops on
# existing users. v1.40.0.0 needs its own migration to patch installs that
# already ran v1.38.1.0.
#
# Per-file independent — if one file is missing we still repair the others.
#
# Idempotent: each insertion is gated on `not already present` so re-running
# the migration is a no-op.
set -u
GSTACK_HOME="${HOME}/.gstack"
ALLOWLIST="${GSTACK_HOME}/.brain-allowlist"
PRIVACY="${GSTACK_HOME}/.brain-privacy-map.json"
GITATTRS="${GSTACK_HOME}/.gitattributes"
MIGRATION_DIR="${GSTACK_HOME}/.migrations"
DONE="${MIGRATION_DIR}/v1.40.0.0.done"
mkdir -p "${MIGRATION_DIR}" 2>/dev/null || true
if [ -f "${DONE}" ]; then
exit 0
fi
NEW_PATTERNS=(
'projects/*/*-eng-review-test-plan-*.md'
)
added_any=0
# ----- .brain-allowlist ---------------------------------------------------
if [ -f "${ALLOWLIST}" ]; then
for PATTERN in "${NEW_PATTERNS[@]}"; do
if ! grep -Fq -- "${PATTERN}" "${ALLOWLIST}" 2>/dev/null; then
if grep -q '^# ---- USER ADDITIONS BELOW' "${ALLOWLIST}" 2>/dev/null; then
sed -i.bak "/^# ---- USER ADDITIONS BELOW/i\\
${PATTERN}
" "${ALLOWLIST}" && rm -f "${ALLOWLIST}.bak"
added_any=1
else
printf '%s\n' "${PATTERN}" >> "${ALLOWLIST}"
added_any=1
fi
fi
done
fi
# ----- .brain-privacy-map.json -------------------------------------------
if [ -f "${PRIVACY}" ]; then
if command -v jq >/dev/null 2>&1; then
for PATTERN in "${NEW_PATTERNS[@]}"; do
if ! jq -e --arg p "${PATTERN}" 'map(select(.pattern == $p)) | length > 0' "${PRIVACY}" >/dev/null 2>&1; then
if jq --arg p "${PATTERN}" '. += [{"pattern": $p, "class": "artifact"}]' "${PRIVACY}" > "${PRIVACY}.tmp" 2>/dev/null; then
mv "${PRIVACY}.tmp" "${PRIVACY}"
added_any=1
else
rm -f "${PRIVACY}.tmp"
echo " [v1.40.0.0] WARN: jq failed to patch ${PRIVACY}; skipping pattern ${PATTERN}." >&2
fi
fi
done
else
echo " [v1.40.0.0] WARN: jq not found; skipping privacy-map repair. Install jq and re-run gstack-upgrade, or run gstack-artifacts-init manually." >&2
fi
fi
# ----- .gitattributes -----------------------------------------------------
if [ -f "${GITATTRS}" ]; then
for PATTERN in "${NEW_PATTERNS[@]}"; do
RULE="${PATTERN} merge=union"
if ! grep -Fq -- "${RULE}" "${GITATTRS}" 2>/dev/null; then
printf '%s\n' "${RULE}" >> "${GITATTRS}"
added_any=1
fi
done
fi
# Mark done even if no patches needed — a fresh-init user's
# bin/gstack-artifacts-init now writes the pattern directly, so re-runs
# should no-op. The touchfile keeps the migration runner from looping.
touch "${DONE}"
if [ "${added_any}" = "1" ]; then
echo " [v1.40.0.0] allowlist/privacy-map/gitattributes patched for /plan-eng-review test plans (idempotent)" >&2
fi
# NEVER `git commit + push` from this migration. The user controls when the
# patches ship into their federated artifacts repo.
exit 0