This commit is contained in:
Artur Schaback 2026-07-14 18:26:06 -07:00 committed by GitHub
commit 847bee6946
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 218 additions and 8 deletions

View File

@ -48,14 +48,21 @@ Fork it. Improve it. Make it yours. And if you want to hate on free open source
Open Claude Code and paste this. Claude does the rest.
> Install gstack: run **`git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /design-shotgun, /design-html, /review, /ship, /land-and-deploy, /canary, /benchmark, /browse, /connect-chrome, /qa, /qa-only, /design-review, /setup-browser-cookies, /setup-deploy, /setup-gbrain, /retro, /investigate, /document-release, /document-generate, /codex, /cso, /autoplan, /plan-devex-review, /devex-review, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, /learn. Then ask the user if they also want to add gstack to the current project so teammates get it.
> Install gstack: run **`git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.gstack/repos/gstack && cd ~/.gstack/repos/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /design-shotgun, /design-html, /review, /ship, /land-and-deploy, /canary, /benchmark, /browse, /connect-chrome, /qa, /qa-only, /design-review, /setup-browser-cookies, /setup-deploy, /setup-gbrain, /retro, /investigate, /document-release, /document-generate, /codex, /cso, /autoplan, /plan-devex-review, /devex-review, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, /learn. Then ask the user if they also want to add gstack to the current project so teammates get it.
The checkout lives at `~/.gstack/repos/gstack` (outside Claude's skill-discovery
root), and `~/.claude/skills/` holds the skill aliases plus a small runtime-only
`gstack/` sidecar. Existing installs at `~/.claude/skills/gstack` are migrated
automatically on the next `./setup` or `/gstack-upgrade`. This keeps recursive
skill scanners (e.g. Cursor) from indexing the repo's generated host-mirror
skills. See [#1202](https://github.com/garrytan/gstack/issues/1202).
### Step 2: Team mode — auto-update for shared repos (recommended)
From inside your repo, paste this. Switches you to team mode, bootstraps the repo so teammates get gstack automatically, and commits the change:
```bash
(cd ~/.claude/skills/gstack && ./setup --team) && ~/.claude/skills/gstack/bin/gstack-team-init required && git add .claude/ CLAUDE.md && git commit -m "require gstack for AI-assisted work"
(cd ~/.gstack/repos/gstack && ./setup --team) && ~/.gstack/repos/gstack/bin/gstack-team-init required && git add .claude/ CLAUDE.md && git commit -m "require gstack for AI-assisted work"
```
No vendored files in your repo, no version drift, no manual upgrades. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent).
@ -67,7 +74,7 @@ Swap `required` for `optional` if you'd rather nudge teammates than block them.
OpenClaw spawns Claude Code sessions via ACP, so every gstack skill just works
when Claude Code has gstack installed. Paste this to your OpenClaw agent:
> Install gstack: run `git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup` to install gstack for Claude Code. Then add a "Coding Tasks" section to AGENTS.md that says: when spawning Claude Code sessions for coding work, tell the session to use gstack skills. Include these examples — security audit: "Load gstack. Run /cso", code review: "Load gstack. Run /review", QA test a URL: "Load gstack. Run /qa https://...", build a feature end-to-end: "Load gstack. Run /autoplan, implement the plan, then run /ship", plan before building: "Load gstack. Run /office-hours then /autoplan. Save the plan, don't implement."
> Install gstack: run `git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.gstack/repos/gstack && cd ~/.gstack/repos/gstack && ./setup` to install gstack for Claude Code. Then add a "Coding Tasks" section to AGENTS.md that says: when spawning Claude Code sessions for coding work, tell the session to use gstack skills. Include these examples — security audit: "Load gstack. Run /cso", code review: "Load gstack. Run /review", QA test a URL: "Load gstack. Run /qa https://...", build a feature end-to-end: "Load gstack. Run /autoplan, implement the plan, then run /ship", plan before building: "Load gstack. Run /office-hours then /autoplan. Save the plan, don't implement."
**After setup, just talk to your OpenClaw agent naturally:**
@ -456,21 +463,21 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna
## Troubleshooting
**Skill not showing up?** `cd ~/.claude/skills/gstack && ./setup`
**Skill not showing up?** `cd ~/.gstack/repos/gstack && ./setup`
**`/browse` fails?** `cd ~/.claude/skills/gstack && bun install && bun run build`
**`/browse` fails?** `cd ~/.gstack/repos/gstack && bun install && bun run build`
**Stale install?** Run `/gstack-upgrade` — or set `auto_upgrade: true` in `~/.gstack/config.yaml`
**Want shorter commands?** `cd ~/.claude/skills/gstack && ./setup --no-prefix` — switches from `/gstack-qa` to `/qa`. Your choice is remembered for future upgrades.
**Want shorter commands?** `cd ~/.gstack/repos/gstack && ./setup --no-prefix` — switches from `/gstack-qa` to `/qa`. Your choice is remembered for future upgrades.
**Want namespaced commands?** `cd ~/.claude/skills/gstack && ./setup --prefix` — switches from `/qa` to `/gstack-qa`. Useful if you run other skill packs alongside gstack.
**Want namespaced commands?** `cd ~/.gstack/repos/gstack && ./setup --prefix` — switches from `/qa` to `/gstack-qa`. Useful if you run other skill packs alongside gstack.
**Codex says "Skipped loading skill(s) due to invalid SKILL.md"?** Your Codex skill descriptions are stale. Fix: `cd ~/.codex/skills/gstack && git pull && ./setup --host codex` — or for repo-local installs: `cd "$(readlink -f .agents/skills/gstack)" && git pull && ./setup --host codex`
**Windows users:** gstack works on Windows 11 via Git Bash or WSL. Node.js is required in addition to Bun — Bun has a known bug with Playwright's pipe transport on Windows ([bun#4253](https://github.com/oven-sh/bun/issues/4253)). The browse server automatically falls back to Node.js. Make sure both `bun` and `node` are on your PATH.
On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file copies instead of symlinks because `ln -snf` produces frozen copies that don't refresh on `git pull`. **Re-run `cd ~/.claude/skills/gstack && ./setup` after every `git pull`** so your skill files match the repo. `setup` prints a one-line note reminding you. Unix and WSL keep symlinks and don't need the re-run.
On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file copies instead of symlinks because `ln -snf` produces frozen copies that don't refresh on `git pull`. **Re-run `cd ~/.gstack/repos/gstack && ./setup` after every `git pull`** so your skill files match the repo. `setup` prints a one-line note reminding you. Unix and WSL keep symlinks and don't need the re-run.
**Claude says it can't see the skills?** Make sure your project's `CLAUDE.md` has a gstack section. Add this:

101
setup
View File

@ -249,6 +249,59 @@ if [ "$INSTALL_CODEX" -eq 1 ]; then
migrate_direct_codex_install "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
fi
# ─── Claude: relocate a direct ~/.claude/skills/gstack checkout ──────────────
# A full repo checkout sitting at ~/.claude/skills/gstack is itself under
# Claude Code's (and Cursor's) recursive skill-discovery root, so every
# generated host-mirror SKILL.md (.cursor/, .hermes/, .agents/, …) and source
# skill SKILL.md inside it gets indexed — hundreds of phantom skills. Claude
# Code drops descriptions ("N skill descriptions dropped"); Cursor's cursor-agent
# CLI hard-freezes. This mirrors migrate_direct_codex_install: move the checkout
# out of the discovery root to ~/.gstack/repos/gstack and keep ~/.claude/skills
# as the alias root (a runtime-only sidecar is created later in the Claude
# install step). See issues #1202 and #1694.
_CLAUDE_RELOCATED=0
migrate_direct_claude_install() {
local claude_gstack="$HOME/.claude/skills/gstack"
local migrated_dir="$HOME/.gstack/repos/gstack"
# Only the standard *global* layout migrates. Leave --local, symlinked
# worktree/vendored installs, and anything not living directly at
# ~/.claude/skills/gstack untouched.
[ "$LOCAL_INSTALL" -eq 1 ] && return 0
if [ "$SOURCE_GSTACK_DIR" = "$claude_gstack" ] && [ ! -L "$claude_gstack" ]; then
mkdir -p "$(dirname "$migrated_dir")"
if [ -e "$migrated_dir" ]; then
echo "gstack setup failed: direct Claude install detected at $claude_gstack" >&2
echo "A migrated repo already exists at $migrated_dir; move one aside and rerun setup." >&2
exit 1
fi
log "Migrating direct Claude install to $migrated_dir to avoid duplicate skill discovery..."
mv "$SOURCE_GSTACK_DIR" "$migrated_dir"
SOURCE_GSTACK_DIR="$migrated_dir"
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
_CLAUDE_RELOCATED=1
elif [ "$SOURCE_GSTACK_DIR" = "$migrated_dir" ]; then
# Already-relocated install re-running setup (e.g. via /gstack-upgrade,
# which cd's into the install dir and runs ./setup). Keep aliases in
# ~/.claude/skills and refresh the sidecar instead of falling through to
# the worktree-symlink branch (which would replant a full-repo symlink
# back inside the discovery root).
_CLAUDE_RELOCATED=1
else
return 0
fi
# Decouple the alias root from the checkout location: top-level skill aliases
# stay in ~/.claude/skills; the checkout lives outside the discovery root.
INSTALL_SKILLS_DIR="$HOME/.claude/skills"
INSTALL_GSTACK_DIR="$INSTALL_SKILLS_DIR/gstack"
}
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
migrate_direct_claude_install
fi
ensure_playwright_browser() {
if [ "$IS_WINDOWS" -eq 1 ]; then
# On Windows, Bun can't launch Chromium due to broken pipe handling
@ -817,6 +870,48 @@ create_codex_runtime_root() {
fi
}
# ─── Helper: create a minimal ~/.claude/skills/gstack runtime sidecar ────────
# Counterpart to create_codex_runtime_root, for relocated Claude installs.
# After the checkout moves to ~/.gstack/repos/gstack, skills and the ~9 bin/
# scripts that hard-code absolute paths like ~/.claude/skills/gstack/bin/
# gstack-config (the most-referenced path in the whole repo) still need them to
# resolve. Expose ONLY runtime assets here, and — unlike the Codex sidecar —
# NO nested SKILL.md at all: Claude's root skill is the separate _gstack-command
# alias and gstack-upgrade is a top-level skill, so the sidecar needs none. That
# keeps recursive scanners (Cursor walks ~/.claude/skills recursively) at zero
# phantom skills from this directory.
create_claude_runtime_root() {
local gstack_dir="$1"
local claude_gstack="$2"
if [ -L "$claude_gstack" ]; then
rm -f "$claude_gstack"
elif [ -d "$claude_gstack" ] && [ "$claude_gstack" != "$gstack_dir" ]; then
rm -rf "$claude_gstack"
fi
mkdir -p "$claude_gstack" "$claude_gstack/browse" "$claude_gstack/design" \
"$claude_gstack/review" "$claude_gstack/plan-devex-review"
# Whole-dir runtime assets that contain no SKILL.md
for asset in bin scripts extension browse/dist browse/bin design/dist review/specialists; do
if [ -e "$gstack_dir/$asset" ]; then
_link_or_copy "$gstack_dir/$asset" "$claude_gstack/$asset"
fi
done
# Individual runtime files (NOT the whole skill dirs that hold their SKILL.md)
for file in ETHOS.md \
review/checklist.md review/design-checklist.md \
review/greptile-triage.md review/TODOS-format.md \
plan-devex-review/dx-hall-of-fame.md \
.feature-prompted-model-overlay .feature-prompted-continuous-checkpoint; do
if [ -f "$gstack_dir/$file" ]; then
_link_or_copy "$gstack_dir/$file" "$claude_gstack/$file"
fi
done
}
create_factory_runtime_root() {
local gstack_dir="$1"
local factory_gstack="$2"
@ -1000,6 +1095,12 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then
if [ -x "$GSTACK_RELINK" ]; then
GSTACK_SKILLS_DIR="$INSTALL_SKILLS_DIR" GSTACK_INSTALL_DIR="$SOURCE_GSTACK_DIR" "$GSTACK_RELINK" >/dev/null 2>&1 || true
fi
# Relocated install: build the runtime-only sidecar at ~/.claude/skills/gstack
# so absolute asset paths (bin/gstack-config, ETHOS.md, review/*, …) resolve
# while the full checkout stays out of the recursive discovery root. #1202/#1694
if [ "$_CLAUDE_RELOCATED" -eq 1 ]; then
create_claude_runtime_root "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR/gstack"
fi
# Backwards-compat alias: /connect-chrome → /open-gstack-browser
_OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome"
if [ "$SKILL_PREFIX" -eq 1 ]; then

View File

@ -0,0 +1,102 @@
/**
* Regression coverage for #1202 / #1694 a direct ~/.claude/skills/gstack
* checkout sits inside Claude/Cursor's recursive skill-discovery root, so every
* generated host-mirror and source SKILL.md gets indexed (hundreds of phantom
* skills; Claude drops descriptions, cursor-agent hard-freezes).
*
* setup's migrate_direct_claude_install + create_claude_runtime_root move the
* checkout to ~/.gstack/repos/gstack and leave a runtime-only sidecar with
* ZERO nested SKILL.md. These tests source those two functions straight out of
* ./setup and exercise them against an isolated fake HOME no real install is
* touched and no full ./setup run (binary build / playwright) is needed.
*/
import { describe, test, expect } from 'bun:test';
import { spawnSync } from 'bun';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const SETUP = join(import.meta.dir, '..', 'setup');
// Run a bash snippet with the two setup functions sourced in, under a fresh
// fake HOME. Returns stdout (assertions are echoed as PASS/FAIL lines).
function runHarness(body: string): { out: string; code: number } {
const home = mkdtempSync(join(tmpdir(), 'gstack-reloc-'));
const script = `
set -u
export HOME='${home}'
log() { :; }
_link_or_copy() { ln -snf "$1" "$2"; }
eval "$(sed -n '/^migrate_direct_claude_install() {/,/^}/p' '${SETUP}')"
eval "$(sed -n '/^create_claude_runtime_root() {/,/^}/p' '${SETUP}')"
make_checkout() {
local r="$1"; rm -rf "$r"
mkdir -p "$r/.git" "$r/bin" "$r/scripts" "$r/browse/dist" "$r/review/specialists" "$r/plan-devex-review"
printf '#!/bin/sh\\necho stub\\n' > "$r/bin/gstack-config"; chmod +x "$r/bin/gstack-config"
: > "$r/ETHOS.md"; : > "$r/scripts/jargon-list.json"; : > "$r/review/checklist.md"
: > "$r/plan-devex-review/dx-hall-of-fame.md"
for s in office-hours ship review browse gstack-upgrade; do mkdir -p "$r/$s"; : > "$r/$s/SKILL.md"; done
mkdir -p "$r/.cursor/skills/x"; : > "$r/.cursor/skills/x/SKILL.md"
}
${body}
`;
const proc = spawnSync(['bash', '-c', script]);
rmSync(home, { recursive: true, force: true });
return { out: proc.stdout.toString(), code: proc.exitCode ?? 0 };
}
describe('migrate_direct_claude_install + create_claude_runtime_root (#1202)', () => {
test('fresh global install relocates and builds a zero-SKILL.md sidecar', () => {
const { out } = runHarness(`
mkdir -p "$HOME/.claude/skills"
make_checkout "$HOME/.claude/skills/gstack"
SOURCE_GSTACK_DIR="$HOME/.claude/skills/gstack"; INSTALL_SKILLS_DIR="$HOME/.claude/skills"
LOCAL_INSTALL=0; BROWSE_BIN=""; _CLAUDE_RELOCATED=0
migrate_direct_claude_install
create_claude_runtime_root "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR/gstack"
echo "relocated=$_CLAUDE_RELOCATED"
echo "moved=$([ -d "$HOME/.gstack/repos/gstack/.git" ] && echo 1 || echo 0)"
echo "aliasRoot=$INSTALL_SKILLS_DIR"
echo "nestedSkillMd=$(find "$HOME/.claude/skills/gstack" -name SKILL.md | wc -l | tr -d ' ')"
echo "binResolves=$([ -x "$HOME/.claude/skills/gstack/bin/gstack-config" ] && echo 1 || echo 0)"
echo "ethosResolves=$([ -e "$HOME/.claude/skills/gstack/ETHOS.md" ] && echo 1 || echo 0)"
`);
expect(out).toContain('relocated=1');
expect(out).toContain('moved=1');
expect(out).toContain(`aliasRoot=`);
expect(out).toMatch(/aliasRoot=.*\/\.claude\/skills$/m);
expect(out).toContain('nestedSkillMd=0');
expect(out).toContain('binResolves=1');
expect(out).toContain('ethosResolves=1');
});
test('re-run from the relocated path is idempotent and keeps the alias root', () => {
const { out } = runHarness(`
make_checkout "$HOME/.gstack/repos/gstack"
SOURCE_GSTACK_DIR="$HOME/.gstack/repos/gstack"; INSTALL_SKILLS_DIR="$HOME/.gstack/repos"
LOCAL_INSTALL=0; _CLAUDE_RELOCATED=0
migrate_direct_claude_install
echo "relocated=$_CLAUDE_RELOCATED"
echo "aliasRoot=$INSTALL_SKILLS_DIR"
echo "repoIntact=$([ -d "$HOME/.gstack/repos/gstack/.git" ] && echo 1 || echo 0)"
`);
expect(out).toContain('relocated=1');
expect(out).toMatch(/aliasRoot=.*\/\.claude\/skills$/m);
expect(out).toContain('repoIntact=1');
});
test('--local installs are never relocated', () => {
const { out } = runHarness(`
mkdir -p "$HOME/.claude/skills"
make_checkout "$HOME/.claude/skills/gstack"
SOURCE_GSTACK_DIR="$HOME/.claude/skills/gstack"; LOCAL_INSTALL=1; _CLAUDE_RELOCATED=0
migrate_direct_claude_install
echo "relocated=$_CLAUDE_RELOCATED"
echo "stillThere=$([ -d "$HOME/.claude/skills/gstack/.git" ] && echo 1 || echo 0)"
`);
expect(out).toContain('relocated=0');
expect(out).toContain('stillThere=1');
});
});