From 5db711e11300e7a0edb863cf3fbeb44e304c22ae Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 22 Mar 2026 14:43:50 -0700 Subject: [PATCH] fix: avoid duplicate Codex skill discovery (#236) Adds migrate_direct_codex_install() to move old direct installs from ~/.codex/skills/gstack to ~/.gstack/repos/gstack. Adds create_codex_runtime_root() to expose only runtime assets (bin/, browse/, review files) via symlinks instead of symlinking the entire repo. Fixes #235 Co-authored-by: shichangs Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 8 +++- setup | 73 ++++++++++++++++++++++++++++++++++--- test/gen-skill-docs.test.ts | 24 ++++++++++++ 3 files changed, 97 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5a032b3e..79d61469 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,14 @@ Real files get committed to your repo (not a submodule), so `git clone` just wor gstack works on any agent that supports the [SKILL.md standard](https://github.com/anthropics/claude-code). Skills live in `.agents/skills/` and are discovered automatically. ```bash -git clone https://github.com/garrytan/gstack.git ~/.codex/skills/gstack -cd ~/.codex/skills/gstack && ./setup --host codex +git clone https://github.com/garrytan/gstack.git ~/gstack +cd ~/gstack && ./setup --host codex ``` +`setup --host codex` creates the runtime root at `~/.codex/skills/gstack` and +links the generated Codex skills at the top level. This avoids duplicate skill +discovery from the source repo checkout. + Or let setup auto-detect which agents you have installed: ```bash diff --git a/setup b/setup index ed8d5eff..1750ca06 100755 --- a/setup +++ b/setup @@ -11,6 +11,8 @@ fi GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)" SKILLS_DIR="$(dirname "$GSTACK_DIR")" BROWSE_BIN="$GSTACK_DIR/browse/dist/browse" +CODEX_SKILLS="$HOME/.codex/skills" +CODEX_GSTACK="$CODEX_SKILLS/gstack" IS_WINDOWS=0 case "$(uname -s)" in @@ -48,6 +50,32 @@ elif [ "$HOST" = "codex" ]; then INSTALL_CODEX=1 fi +migrate_direct_codex_install() { + local gstack_dir="$1" + local codex_gstack="$2" + local migrated_dir="$HOME/.gstack/repos/gstack" + + [ "$gstack_dir" = "$codex_gstack" ] || return 0 + [ -L "$gstack_dir" ] && return 0 + + mkdir -p "$(dirname "$migrated_dir")" + if [ -e "$migrated_dir" ] && [ "$migrated_dir" != "$gstack_dir" ]; then + echo "gstack setup failed: direct Codex install detected at $gstack_dir" >&2 + echo "A migrated repo already exists at $migrated_dir; move one of them aside and rerun setup." >&2 + exit 1 + fi + + echo "Migrating direct Codex install to $migrated_dir to avoid duplicate skill discovery..." + mv "$gstack_dir" "$migrated_dir" + GSTACK_DIR="$migrated_dir" + SKILLS_DIR="$(dirname "$GSTACK_DIR")" + BROWSE_BIN="$GSTACK_DIR/browse/dist/browse" +} + +if [ "$INSTALL_CODEX" -eq 1 ]; then + migrate_direct_codex_install "$GSTACK_DIR" "$CODEX_GSTACK" +fi + ensure_playwright_browser() { if [ "$IS_WINDOWS" -eq 1 ]; then # On Windows, Bun can't launch Chromium due to broken pipe handling @@ -248,6 +276,44 @@ create_agents_sidecar() { done } +# ─── Helper: create a minimal ~/.codex/skills/gstack runtime root ─────────── +# Codex scans ~/.codex/skills recursively. Exposing the whole repo here causes +# duplicate skills because source SKILL.md files and generated Codex skills are +# both discoverable. Keep this directory limited to runtime assets + root skill. +create_codex_runtime_root() { + local gstack_dir="$1" + local codex_gstack="$2" + local agents_dir="$gstack_dir/.agents/skills" + + if [ -L "$codex_gstack" ]; then + rm -f "$codex_gstack" + fi + + mkdir -p "$codex_gstack" "$codex_gstack/browse" "$codex_gstack/gstack-upgrade" "$codex_gstack/review" + + if [ -f "$agents_dir/gstack/SKILL.md" ]; then + ln -snf "$agents_dir/gstack/SKILL.md" "$codex_gstack/SKILL.md" + fi + if [ -d "$gstack_dir/bin" ]; then + ln -snf "$gstack_dir/bin" "$codex_gstack/bin" + fi + if [ -d "$gstack_dir/browse/dist" ]; then + ln -snf "$gstack_dir/browse/dist" "$codex_gstack/browse/dist" + fi + if [ -d "$gstack_dir/browse/bin" ]; then + ln -snf "$gstack_dir/browse/bin" "$codex_gstack/browse/bin" + fi + if [ -f "$agents_dir/gstack-upgrade/SKILL.md" ]; then + ln -snf "$agents_dir/gstack-upgrade/SKILL.md" "$codex_gstack/gstack-upgrade/SKILL.md" + fi + # Review runtime assets (individual files, NOT the whole review/ dir which has SKILL.md) + for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do + if [ -f "$gstack_dir/review/$f" ]; then + ln -snf "$gstack_dir/review/$f" "$codex_gstack/review/$f" + fi + done +} + # 4. Install for Claude (default) SKILLS_BASENAME="$(basename "$SKILLS_DIR")" if [ "$INSTALL_CLAUDE" -eq 1 ]; then @@ -264,14 +330,9 @@ fi # 5. Install for Codex if [ "$INSTALL_CODEX" -eq 1 ]; then - CODEX_SKILLS="$HOME/.codex/skills" - CODEX_GSTACK="$CODEX_SKILLS/gstack" mkdir -p "$CODEX_SKILLS" - # Symlink gstack source for runtime assets (bin/, browse/dist/) - if [ -L "$CODEX_GSTACK" ] || [ ! -e "$CODEX_GSTACK" ]; then - ln -snf "$GSTACK_DIR" "$CODEX_GSTACK" - fi + create_codex_runtime_root "$GSTACK_DIR" "$CODEX_GSTACK" # Install generated Codex-format skills (not Claude source dirs) link_codex_skill_dirs "$GSTACK_DIR" "$CODEX_SKILLS" diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 1e72102c..bb6d9d7e 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -853,8 +853,10 @@ describe('setup script validation', () => { setupContent.indexOf('# 5. Install for Codex'), setupContent.indexOf('# 6. Create') ); + expect(codexSection).toContain('create_codex_runtime_root'); expect(codexSection).toContain('link_codex_skill_dirs'); expect(codexSection).not.toContain('link_claude_skill_dirs'); + expect(codexSection).not.toContain('ln -snf "$GSTACK_DIR" "$CODEX_GSTACK"'); }); test('link_codex_skill_dirs reads from .agents/skills/', () => { @@ -894,6 +896,28 @@ describe('setup script validation', () => { expect(fnBody).toContain('review'); expect(fnBody).toContain('qa'); }); + + test('create_codex_runtime_root exposes only runtime assets', () => { + const fnStart = setupContent.indexOf('create_codex_runtime_root()'); + const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', setupContent.indexOf('review/', fnStart))); + const fnBody = setupContent.slice(fnStart, fnEnd); + expect(fnBody).toContain('gstack/SKILL.md'); + expect(fnBody).toContain('browse/dist'); + expect(fnBody).toContain('browse/bin'); + expect(fnBody).toContain('gstack-upgrade/SKILL.md'); + // Review runtime assets (individual files, not the whole dir) + expect(fnBody).toContain('checklist.md'); + expect(fnBody).toContain('design-checklist.md'); + expect(fnBody).toContain('greptile-triage.md'); + expect(fnBody).toContain('TODOS-format.md'); + expect(fnBody).not.toContain('ln -snf "$gstack_dir" "$codex_gstack"'); + }); + + test('direct Codex installs are migrated out of ~/.codex/skills/gstack', () => { + expect(setupContent).toContain('migrate_direct_codex_install'); + expect(setupContent).toContain('$HOME/.gstack/repos/gstack'); + expect(setupContent).toContain('avoid duplicate skill discovery'); + }); }); describe('telemetry', () => {