fix: top-level skill dirs so Claude discovers unprefixed names (#761)

* fix: top-level skill dirs so Claude discovers unprefixed names

Replace directory symlinks (gstack/qa → qa) with real directories
containing a SKILL.md symlink. Claude Code auto-prefixes skills nested
under a parent dir symlink, so /plan-ceo-review became "Unknown skill"
even with skill_prefix=false. Real dirs fix this.

Also syncs package.json version to match VERSION file and updates
test assertions to match the new mkdir + ln approach.

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

* docs: update symlink references to new top-level directory pattern

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

* test: regression tests for top-level skill directory structure

Verifies the invariant that setup/relink creates real directories (not
symlinks) at the top level, with SKILL.md symlinks inside. This prevents
Claude Code from auto-prefixing skills with gstack- when using --no-prefix.

Tests added:
- unprefixed skills must be real dirs with SKILL.md symlinks
- prefixed skills must also be real dirs with SKILL.md symlinks
- old directory symlinks get upgraded to real directories
- cleanup functions handle both old symlinks and new dir pattern
- link function removes old directory symlinks before mkdir

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

* test: namespace isolation tests for first install + mode switching

Verifies the core invariant: when you pick a prefix mode, ONLY that
mode's entries exist. Zero pollution from the other mode.

- first install --no-prefix: only flat names, zero gstack-* leaks
- first install --prefix: only gstack-* names, zero flat leaks
- non-TTY defaults to flat names
- switching prefix→no-prefix removes ALL gstack-* entries
- switching no-prefix→prefix removes ALL flat entries

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

* feat: upgrade migration system — versioned fix scripts for broken state

Adds gstack-upgrade/migrations/ directory with version-keyed bash scripts
that run automatically during /gstack-upgrade (Step 4.75, after ./setup).
Each script is idempotent and handles state fixes that setup alone can't
cover. First migration: v0.15.2.0.sh runs gstack-relink to fix stale
directory symlinks from pre-v0.15.2.0 installs.

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

* test: migration script validation + v0.15.2.0 end-to-end fix test

Tests that migration scripts are executable, parse without syntax errors,
follow the v{VERSION}.sh naming convention, and that v0.15.2.0 actually
fixes stale directory symlinks by converting them to real directories.

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

* docs: upgrade migration guide in CONTRIBUTING.md + CLAUDE.md pointer

CONTRIBUTING.md: new "Upgrade migrations" section documenting when and
how to add migration scripts for broken on-disk state.

CLAUDE.md: added note under vendored symlink awareness pointing to
CONTRIBUTING.md's migration section when worried about broken installs.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-02 18:34:00 -07:00
committed by GitHub
parent 6169273d16
commit 4fc64f7f96
11 changed files with 474 additions and 40 deletions
+26
View File
@@ -170,6 +170,32 @@ 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
+26
View File
@@ -168,6 +168,32 @@ 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
+20
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