diff --git a/README.md b/README.md index a0d9c40e3..1b0439fef 100644 --- a/README.md +++ b/README.md @@ -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:** @@ -453,21 +460,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: diff --git a/setup b/setup index 37991eda7..a0310d636 100755 --- a/setup +++ b/setup @@ -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 diff --git a/test/claude-install-relocation.test.ts b/test/claude-install-relocation.test.ts new file mode 100644 index 000000000..6e61558b3 --- /dev/null +++ b/test/claude-install-relocation.test.ts @@ -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'); + }); +});