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

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