mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-02 00:01:37 +02:00
46c1fae7f1
* feat(test): transcript-section-logger + ship-action fingerprint (T10) Pure-analysis module over a SkillTestResult/NDJSON transcript: - extractSectionReads(): which sections/*.md a run opened (post-carve check) - extractShipActions(): observable action fingerprint (merge/test/bump/ changelog/commit/push/pr) that works on the MONOLITH too, so a baseline captured before the carve can detect a sectioned-ship regression - baseline read/write + compareShipActions() for baseline-first dogf(T10) Baseline-first answers the Codex outside-voice critique that a logger in the same PR as the carve is post-failure telemetry without a pre-carve reference. 11 unit tests, all green. Paid monolith baseline capture runs separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(pipeline): section discovery + generation machinery (T9) - discover-skills.ts: discoverSectionTemplates() scans <skill>/sections/*.md.tmpl - gen-skill-docs.ts: extract resolvePlaceholders + applyHostRewrites + buildContext as shared helpers (processTemplate and the new processSectionTemplate both call them, so a sanitization/rewrite fix can't miss sections) [C1] - processSectionTemplate: body-fragment generation (no frontmatter/catalog/voice), parent-skill TemplateContext (skillName pinned to parent, not 'sections', so appliesTo gating + tier behave identically), per-host output routing - --host all now fails the build on ANY host failure, not just claude, so a stale external-host output can't slip the freshness gate [Codex outside-voice #9] Inert until a skill is carved (no sections/ dirs exist yet). Refactor is output-neutral: gen:skill-docs --dry-run --host all reports 0 STALE. 5 discovery unit tests + 389 gen-skill-docs tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): install sections/ for cherry-pick targets (claude + kiro) (T9) Two install targets cherry-pick SKILL.md and would leave a carved skill's sections/ behind, 404ing a runtime 'Read sections/<name>.md': - link_claude_skill_dirs: link the sections/ subdir via _link_or_copy (windows gets a fresh copy on every ./setup) - kiro per-skill loop: sed-rewrite + copy each sections/* so paths resolve under ~/.kiro, not ~/.codex/~/.claude codex/factory/opencode link the whole generated dir, so sections ride free. Addresses Codex outside-voice #4/#6 (runtime pathing landmine). Inert until a skill is carved. Static-tripwire test + windows-fallback invariant green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ship): gstack-version-bump CLI — tested idempotency classify + write (T9) Hybrid CLI extraction (CM1): the deterministic core of ship Step 12 becomes a tested CLI instead of bash prose the agent re-derives each run. - classify: FRESH/ALREADY_BUMPED/DRIFT_STALE_PKG/DRIFT_UNEXPECTED from VERSION vs origin/<base>:VERSION vs package.json.version (pure reader) - write: validated dual-write to VERSION + package.json (FRESH bump) - repair: DRIFT_STALE_PKG sync, no re-bump Bump-LEVEL choice + queue collision stay agent judgment; slot pick stays bin/gstack-next-version. This removes the re-bump-a-shipped-branch footgun from skippable prose into code that can't be skipped or misread. 15 tests (exhaustive state matrix + write/repair fs + real-git classify). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(parity): sectioned-skill parity capability — guards the carve (T9) Carved skills (skeleton + sections/*.md) need parity checks that see relocated content, or moving a phrase into a section reads as 'lost': - readSkillForParity(): union skeleton + all sections/*.md - checkSkillParity sectioned mode: content checks against the union; minBytes/ maxSizeRatio against union bytes (total behavior preserved); maxSkeletonBytes asserts the always-loaded skeleton actually shrank. Lowering minBytes to fit a small skeleton would otherwise make the size floor toothless [Codex #12]. Built + tested BEFORE the carve so ship's invariant can flip to sectioned in the same commit it lands. Monolith path byte-identical (verified: pre-existing investigate 1.053 ratio drift fails the same with this change stashed). 7 sectioned-parity tests + existing parity tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ship): carve into skeleton + on-demand sections (Claude) (T9) ship/SKILL.md drops 167KB → 68.7KB (~59% of the always-loaded skill) by moving 8 prose-heavy steps into ship/sections/*.md, read on demand: tests, test-coverage, plan-completion, review-army, greptile, adversarial, changelog, pr-body. Step 12's version logic now calls the tested gstack-version-bump CLI instead of inline bash. Claude-first (S2): {{SECTION:id}} emits a STOP-Read pointer on Claude (skeleton + generated section files) and INLINES the content on every other host, so external hosts keep the full monolith — verified factory at 162KB with no sections dir. {{SECTION_INDEX:ship}} renders the situation→section table from the PASSIVE manifest (CM2 / v2_PLAN.md:663); required-reads live only in test fixtures. Multi-pass resolve expands inlined sections' own resolvers. Parity: ship invariant flipped to sectioned (union content checks + maxSkeletonBytes asserts the shrink). Carve-fallout fixed across gen-skill-docs/skill-validation/ golden/plan-completion/#1539/size-budget tests via skeleton+sections union reads. Free suite green except the pre-existing investigate parity drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ship): manifest-consistency + context-parity + requiredReads helper (T9) Free deterministic guards for the carve: - required-reads.ts + unit test: assertRequiredReads(run, requiredFiles) — the mechanical layer-5 check that the agent Read the sections its situation needs (required set comes from the fixture, not the passive manifest) - section-manifest-consistency: 3-tier orphan classification (generated orphan + hand-edited generated file → FAIL; manifest orphan → WARN per v2_PLAN.md) and pins the PASSIVE-manifest contract (no applies_when/required_for) - template-context-parity: generated sections have zero unresolved placeholders and gated resolvers (ADVERSARIAL_STEP/CONFIDENCE_CALIBRATION/CHANGELOG_WORKFLOW) rendered — proving sections resolve with the parent skillName, not 'sections' 16 tests, all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ship): section-loading E2E + idempotency CLI detection (T9) - skill-e2e-ship-section-loading.test.ts (new, periodic): runs real /ship in plan mode against a fresh version-changing fixture and asserts the agent Read the required sections (review-army + changelog). Runs against the INSTALLED skill (~/.claude/skills/gstack/ship), not repo paths, so install-layout 404s surface [Codex outside-voice #5]. Layer-5 mechanical guard against silent section-skip. - skill-e2e-ship-idempotency.test.ts: detection updated for the carve — Step 12 now runs gstack-version-bump classify (JSON "state":"ALREADY_BUMPED") instead of the inline bash echo (STATE: ALREADY_BUMPED). Accept both; add a gstack-version-bump-write re-bump regression signal. - touchfiles: register ship-section-loading (periodic) + extend idempotency deps with bin/gstack-version-bump + scripts/resolvers/sections.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ship): union-read redaction wiring test for the carve (T9) main's PR-body redaction-at-sink lives in sections/pr-body.md.tmpl after the carve, not the skeleton template. Read skeleton + section templates union so the redaction-wiring assertions follow the relocated content. 9/9 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * v1.54.0.0 feat: carve /ship into skeleton + on-demand sections (-59% always-loaded) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1455 lines
60 KiB
Bash
Executable File
1455 lines
60 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack setup — build browser binary + register skills with Claude Code / Codex
|
|
set -e
|
|
umask 077 # Restrict new files to owner-only (0o600 files, 0o700 dirs)
|
|
|
|
if ! command -v bun >/dev/null 2>&1; then
|
|
echo "Error: bun is required but not installed." >&2
|
|
echo "Install with checksum verification:" >&2
|
|
echo ' BUN_VERSION="1.3.10"' >&2
|
|
echo ' tmpfile=$(mktemp)' >&2
|
|
echo ' curl -fsSL "https://bun.sh/install" -o "$tmpfile"' >&2
|
|
echo ' echo "Verify checksum before running: shasum -a 256 $tmpfile"' >&2
|
|
echo ' BUN_VERSION="$BUN_VERSION" bash "$tmpfile" && rm "$tmpfile"' >&2
|
|
exit 1
|
|
fi
|
|
|
|
INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
|
|
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
|
|
CODEX_SKILLS="$HOME/.codex/skills"
|
|
CODEX_GSTACK="$CODEX_SKILLS/gstack"
|
|
FACTORY_SKILLS="$HOME/.factory/skills"
|
|
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
|
|
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
|
|
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
|
|
|
|
IS_WINDOWS=0
|
|
case "$(uname -s)" in
|
|
MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
|
|
esac
|
|
|
|
# ─── Symlink-or-copy helper ───────────────────────────────────
|
|
# On macOS/Linux: create a symlink (existing behavior).
|
|
# On Windows without Developer Mode (MSYS2/Git Bash): plain ln -snf silently
|
|
# creates a frozen file copy that doesn't refresh after `git pull`. We use
|
|
# explicit `cp -R` / `cp -f` so the user gets a real copy and the staleness
|
|
# is reportable (re-run ./setup after pull). Auto-detects file vs dir.
|
|
#
|
|
# INVARIANT: every symlink in this script MUST route through this helper.
|
|
# A raw ln call here will be caught by test/setup-windows-fallback.test.ts
|
|
# (the static-invariant assertion D7).
|
|
_link_or_copy() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
rm -rf "$dst"
|
|
# Unix `ln -snf` accepts a name-only or relative-path source even when the
|
|
# target doesn't resolve from CWD (e.g. the connect-chrome alias points at
|
|
# the sibling-relative "gstack/open-gstack-browser"). On Windows the
|
|
# equivalent semantics don't exist — we'd need a real source on disk to
|
|
# copy. Skip the alias quietly rather than aborting setup under `set -e`.
|
|
if [ ! -e "$src" ]; then
|
|
return 0
|
|
fi
|
|
if [ -d "$src" ]; then
|
|
cp -R "$src" "$dst"
|
|
else
|
|
cp -f "$src" "$dst"
|
|
fi
|
|
else
|
|
ln -snf "$src" "$dst"
|
|
fi
|
|
}
|
|
|
|
_WINDOWS_COPY_NOTE_PRINTED=0
|
|
_print_windows_copy_note_once() {
|
|
if [ "$IS_WINDOWS" -eq 1 ] && [ "$_WINDOWS_COPY_NOTE_PRINTED" -eq 0 ]; then
|
|
echo " note: Windows install uses file copies (no Developer Mode required). Re-run ./setup after every 'git pull' to refresh skill files."
|
|
_WINDOWS_COPY_NOTE_PRINTED=1
|
|
fi
|
|
}
|
|
|
|
# ─── Quiet mode helper ────────────────────────────────────────
|
|
QUIET=0
|
|
log() { [ "$QUIET" -eq 0 ] && echo "$@" || true; }
|
|
|
|
# ─── Parse flags ──────────────────────────────────────────────
|
|
HOST="claude"
|
|
LOCAL_INSTALL=0
|
|
SKILL_PREFIX=1
|
|
SKILL_PREFIX_FLAG=0
|
|
TEAM_MODE=0
|
|
NO_TEAM_MODE=0
|
|
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
|
|
--host=*) HOST="${1#--host=}"; shift ;;
|
|
--local) LOCAL_INSTALL=1; shift ;;
|
|
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
|
|
--no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;;
|
|
--team) TEAM_MODE=1; shift ;;
|
|
--no-team) NO_TEAM_MODE=1; shift ;;
|
|
--plan-tune-hooks) PLAN_TUNE_HOOKS_MODE="yes"; shift ;;
|
|
--no-plan-tune-hooks) PLAN_TUNE_HOOKS_MODE="no"; shift ;;
|
|
--plan-tune-hooks=*) PLAN_TUNE_HOOKS_MODE="${1#--plan-tune-hooks=}"; shift ;;
|
|
-q|--quiet) QUIET=1; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
case "$HOST" in
|
|
claude|codex|kiro|factory|opencode|auto) ;;
|
|
openclaw)
|
|
echo ""
|
|
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
|
|
echo "sessions natively via ACP. gstack provides methodology artifacts, not a"
|
|
echo "full skill installation."
|
|
echo ""
|
|
echo "To integrate gstack with OpenClaw:"
|
|
echo " 1. Tell your OpenClaw agent: 'install gstack for openclaw'"
|
|
echo " 2. Or generate artifacts: bun run gen:skill-docs --host openclaw"
|
|
echo " 3. See docs/OPENCLAW.md for the full architecture"
|
|
echo ""
|
|
exit 0 ;;
|
|
hermes)
|
|
echo ""
|
|
echo "Hermes integration uses the same model as OpenClaw — Hermes spawns"
|
|
echo "Claude Code sessions, and gstack provides methodology artifacts."
|
|
echo ""
|
|
echo "To integrate gstack with Hermes:"
|
|
echo " 1. Tell your Hermes agent: 'install gstack for hermes'"
|
|
echo " 2. Or generate artifacts: bun run gen:skill-docs --host hermes"
|
|
echo ""
|
|
exit 0 ;;
|
|
gbrain)
|
|
echo ""
|
|
echo "GBrain is a mod for gstack — it makes coding skills brain-aware."
|
|
echo "GBrain generates brain-enhanced skill variants that search your brain"
|
|
echo "for context before starting and save results after finishing."
|
|
echo ""
|
|
echo "To generate brain-aware skills:"
|
|
echo " bun run gen:skill-docs --host gbrain"
|
|
echo ""
|
|
echo "GBrain setup and brain skills ship from the GBrain repo."
|
|
echo ""
|
|
exit 0 ;;
|
|
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
|
|
esac
|
|
|
|
# ─── Resolve skill prefix preference ─────────────────────────
|
|
# Priority: CLI flag > saved config > interactive prompt (or flat default for non-TTY)
|
|
GSTACK_CONFIG="$SOURCE_GSTACK_DIR/bin/gstack-config"
|
|
export GSTACK_SETUP_RUNNING=1 # Prevent gstack-config post-set hook from triggering relink mid-setup
|
|
if [ "$SKILL_PREFIX_FLAG" -eq 0 ]; then
|
|
_saved_prefix="$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || true)"
|
|
if [ "$_saved_prefix" = "true" ]; then
|
|
SKILL_PREFIX=1
|
|
elif [ "$_saved_prefix" = "false" ]; then
|
|
SKILL_PREFIX=0
|
|
else
|
|
# No saved preference — prompt interactively (or default flat for non-TTY/quiet)
|
|
if [ "$QUIET" -eq 1 ]; then
|
|
SKILL_PREFIX=0
|
|
elif [ -t 0 ]; then
|
|
echo ""
|
|
echo "Skill naming: how should gstack skills appear?"
|
|
echo ""
|
|
echo " 1) Short names: /qa, /ship, /review"
|
|
echo " Recommended. Clean and fast to type."
|
|
echo ""
|
|
echo " 2) Namespaced: /gstack-qa, /gstack-ship, /gstack-review"
|
|
echo " Use this if you run other skill packs alongside gstack to avoid conflicts."
|
|
echo ""
|
|
printf "Choice [1/2] (default: 1, auto-selects in 10s): "
|
|
read -t 10 -r _prefix_choice </dev/tty 2>/dev/null || _prefix_choice=""
|
|
case "$_prefix_choice" in
|
|
2) SKILL_PREFIX=1 ;;
|
|
*) SKILL_PREFIX=0 ;;
|
|
esac
|
|
else
|
|
SKILL_PREFIX=0
|
|
fi
|
|
# Save the choice for future runs
|
|
"$GSTACK_CONFIG" set skill_prefix "$([ "$SKILL_PREFIX" -eq 1 ] && echo true || echo false)" 2>/dev/null || true
|
|
fi
|
|
else
|
|
# Flag was passed explicitly — persist the choice
|
|
"$GSTACK_CONFIG" set skill_prefix "$([ "$SKILL_PREFIX" -eq 1 ] && echo true || echo false)" 2>/dev/null || true
|
|
fi
|
|
|
|
# --local: install to .claude/skills/ in the current working directory (deprecated)
|
|
if [ "$LOCAL_INSTALL" -eq 1 ]; then
|
|
echo "Warning: --local is deprecated. Use global install + --team instead." >&2
|
|
echo " See: https://github.com/garrytan/gstack#team-mode" >&2
|
|
if [ "$HOST" = "codex" ]; then
|
|
echo "Error: --local is only supported for Claude Code (not Codex)." >&2
|
|
exit 1
|
|
fi
|
|
INSTALL_SKILLS_DIR="$(pwd)/.claude/skills"
|
|
mkdir -p "$INSTALL_SKILLS_DIR"
|
|
HOST="claude"
|
|
INSTALL_CODEX=0
|
|
fi
|
|
|
|
# For auto: detect which agents are installed
|
|
INSTALL_CLAUDE=0
|
|
INSTALL_CODEX=0
|
|
INSTALL_KIRO=0
|
|
INSTALL_FACTORY=0
|
|
INSTALL_OPENCODE=0
|
|
if [ "$HOST" = "auto" ]; then
|
|
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
|
|
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
|
|
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
|
|
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
|
|
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
|
|
# If none found, default to claude
|
|
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
|
|
INSTALL_CLAUDE=1
|
|
fi
|
|
elif [ "$HOST" = "claude" ]; then
|
|
INSTALL_CLAUDE=1
|
|
elif [ "$HOST" = "codex" ]; then
|
|
INSTALL_CODEX=1
|
|
elif [ "$HOST" = "kiro" ]; then
|
|
INSTALL_KIRO=1
|
|
elif [ "$HOST" = "factory" ]; then
|
|
INSTALL_FACTORY=1
|
|
elif [ "$HOST" = "opencode" ]; then
|
|
INSTALL_OPENCODE=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
|
|
|
|
log "Migrating direct Codex install to $migrated_dir to avoid duplicate skill discovery..."
|
|
mv "$gstack_dir" "$migrated_dir"
|
|
SOURCE_GSTACK_DIR="$migrated_dir"
|
|
INSTALL_GSTACK_DIR="$migrated_dir"
|
|
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
|
|
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
|
|
}
|
|
|
|
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
migrate_direct_codex_install "$SOURCE_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
|
|
# (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
|
|
)
|
|
else
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
|
|
) >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
# Ensure a color-emoji font is installed (Linux only).
|
|
#
|
|
# Chromium renders emoji code points as .notdef "tofu" (▯) when no color-emoji
|
|
# font is installed. macOS ships "Apple Color Emoji" and Windows ships "Segoe UI
|
|
# Emoji", so they're fine out of the box. Most Linux distros and containers ship
|
|
# NO color-emoji font, which is why make-pdf output shows tofu in headers/tables
|
|
# that contain emoji. Install Noto Color Emoji to fix it.
|
|
#
|
|
# Best-effort: warn (don't fail) if we can't install — PDFs still generate, they
|
|
# just fall back to tofu for emoji as before. Skip entirely with
|
|
# GSTACK_SKIP_FONTS=1 (CI without sudo, managed machines, offline envs).
|
|
#
|
|
# Returns 0 and sets EMOJI_FONT_INSTALLED=1 when it actually installs a font.
|
|
EMOJI_FONT_INSTALLED=0
|
|
ensure_emoji_font() {
|
|
# macOS/Windows ship a color-emoji font; nothing to do.
|
|
[ "$(uname -s)" = "Linux" ] || return 0
|
|
[ "${GSTACK_SKIP_FONTS:-0}" = "1" ] && return 0
|
|
|
|
# Idempotency: a real COLOR emoji font that resolves for an actual emoji code
|
|
# point (U+1F600). `fc-list :lang=und-zsye` is too broad — it matches symbol
|
|
# and last-resort fallback fonts — so we use fc-match and require color=True.
|
|
if command -v fc-match >/dev/null 2>&1; then
|
|
if fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' 2>/dev/null | grep -qi 'True'; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local sudo=""
|
|
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
|
# -n: never prompt. If a password is required we fail fast into the
|
|
# warn-not-fail path below instead of hanging a non-interactive setup.
|
|
sudo="sudo -n"
|
|
fi
|
|
|
|
# Every package-manager call is wrapped in `timeout` so a stuck dpkg/rpm lock
|
|
# or a wedged mirror fails fast into the warn path instead of hanging setup.
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
echo "Installing color-emoji font (fonts-noto-color-emoji) so make-pdf emoji render (set GSTACK_SKIP_FONTS=1 to skip)..."
|
|
DEBIAN_FRONTEND=noninteractive timeout 30 $sudo apt-get update -qq >/dev/null 2>&1 || true
|
|
DEBIAN_FRONTEND=noninteractive timeout 120 $sudo apt-get install -y -qq fonts-noto-color-emoji >/dev/null 2>&1 || return 1
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
echo "Installing color-emoji font (google-noto-color-emoji-fonts)..."
|
|
timeout 120 $sudo dnf install -y google-noto-color-emoji-fonts >/dev/null 2>&1 || return 1
|
|
elif command -v pacman >/dev/null 2>&1; then
|
|
echo "Installing color-emoji font (noto-fonts-emoji)..."
|
|
timeout 120 $sudo pacman -Sy --noconfirm noto-fonts-emoji >/dev/null 2>&1 || return 1
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
echo "Installing color-emoji font (font-noto-emoji)..."
|
|
timeout 120 $sudo apk add --no-cache font-noto-emoji >/dev/null 2>&1 || return 1
|
|
else
|
|
return 1
|
|
fi
|
|
|
|
# Refresh fontconfig cache so Chromium picks up the new font. Run under sudo
|
|
# for the system cache dirs (unprivileged fc-cache fails on unwritable dirs).
|
|
if command -v fc-cache >/dev/null 2>&1; then
|
|
$sudo fc-cache -f >/dev/null 2>&1 || fc-cache -f >/dev/null 2>&1 || true
|
|
fi
|
|
EMOJI_FONT_INSTALLED=1
|
|
return 0
|
|
}
|
|
|
|
# After a fresh font install, stop any running browse render daemon so the next
|
|
# make-pdf render spawns a fresh Chromium that sees the new font. Chromium
|
|
# caches its font list at process start, so a daemon that was alive before the
|
|
# install would keep emitting tofu. `browse stop` is the graceful API; the
|
|
# daemon auto-respawns on the next render. Best-effort and per-project-root, so
|
|
# we also print a note for daemons in other roots.
|
|
refresh_browse_daemon_for_fonts() {
|
|
[ "$EMOJI_FONT_INSTALLED" -eq 1 ] || return 0
|
|
if [ -x "$BROWSE_BIN" ]; then
|
|
"$BROWSE_BIN" stop >/dev/null 2>&1 || true
|
|
fi
|
|
echo " Installed a color-emoji font. The next make-pdf render will show emoji."
|
|
echo " If a gstack browser is running in another project, restart it to pick up the font."
|
|
}
|
|
|
|
prepare_bun_for_windows_compile() {
|
|
BUN_CMD="bun"
|
|
BUN_CMD_WAS_COPIED=0
|
|
[ "$IS_WINDOWS" -eq 1 ] || return 0
|
|
|
|
local bun_path
|
|
bun_path="$(command -v bun 2>/dev/null || true)"
|
|
case "$bun_path" in
|
|
*[![:ascii:]]*)
|
|
local bun_copy_dir="$SOURCE_GSTACK_DIR/.tmp-bun-bin"
|
|
mkdir -p "$bun_copy_dir"
|
|
cp -f "$bun_path" "$bun_copy_dir/bun.exe"
|
|
BUN_CMD="$bun_copy_dir/bun.exe"
|
|
BUN_CMD_WAS_COPIED=1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
bun_cmd() {
|
|
"$BUN_CMD" "$@"
|
|
}
|
|
|
|
cleanup_copied_bun() {
|
|
if [ "${BUN_CMD_WAS_COPIED:-0}" -eq 1 ]; then
|
|
rm -rf "$SOURCE_GSTACK_DIR/.tmp-bun-bin"
|
|
fi
|
|
}
|
|
|
|
prepare_bun_for_windows_compile
|
|
trap cleanup_copied_bun EXIT
|
|
|
|
# 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock)
|
|
NEEDS_BUILD=0
|
|
if [ ! -x "$BROWSE_BIN" ]; then
|
|
NEEDS_BUILD=1
|
|
elif [ -n "$(find "$SOURCE_GSTACK_DIR/browse/src" -type f -newer "$BROWSE_BIN" -print -quit 2>/dev/null)" ]; then
|
|
NEEDS_BUILD=1
|
|
elif [ "$SOURCE_GSTACK_DIR/package.json" -nt "$BROWSE_BIN" ]; then
|
|
NEEDS_BUILD=1
|
|
elif [ -f "$SOURCE_GSTACK_DIR/bun.lock" ] && [ "$SOURCE_GSTACK_DIR/bun.lock" -nt "$BROWSE_BIN" ]; then
|
|
NEEDS_BUILD=1
|
|
fi
|
|
|
|
if [ "$NEEDS_BUILD" -eq 1 ]; then
|
|
log "Building browse binary..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
|
|
bun_cmd run build
|
|
)
|
|
# Safety net: write .version if build script didn't (e.g., git not available during build)
|
|
if [ ! -f "$SOURCE_GSTACK_DIR/browse/dist/.version" ]; then
|
|
git -C "$SOURCE_GSTACK_DIR" rev-parse HEAD > "$SOURCE_GSTACK_DIR/browse/dist/.version" 2>/dev/null || true
|
|
fi
|
|
|
|
# macOS Apple Silicon: ad-hoc codesign compiled binaries.
|
|
# Bun's --compile can produce a corrupt or linker-only code signature that
|
|
# macOS kills with SIGKILL (exit 137). The two-step remove+re-sign is
|
|
# required because a naive `codesign -s - -f` fails when the existing
|
|
# signature block is corrupt. This is idempotent and costs <1s.
|
|
# See: https://github.com/garrytan/gstack/issues/997
|
|
if [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "arm64" ]; then
|
|
for _bin in browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover; do
|
|
_bin_path="$SOURCE_GSTACK_DIR/$_bin"
|
|
[ -f "$_bin_path" ] && [ -x "$_bin_path" ] || continue
|
|
codesign --remove-signature "$_bin_path" 2>/dev/null || true
|
|
if ! codesign -s - -f "$_bin_path" 2>/dev/null; then
|
|
log "warning: codesign failed for $_bin (binary may not run on Apple Silicon)"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# macOS: install coreutils for `gtimeout` (Codex hang protection in /codex + /autoplan).
|
|
# macOS ships BSD `timeout`-less; Homebrew's coreutils installs GNU timeout as
|
|
# `gtimeout` to avoid shadowing BSD utilities. The /codex and /autoplan skills
|
|
# fall back to unwrapped codex invocations when neither is available — this
|
|
# auto-install upgrades them to hang-protected where possible.
|
|
# Skip entirely with GSTACK_SKIP_COREUTILS=1 (CI, managed machines, offline envs).
|
|
if [ "$(uname -s)" = "Darwin" ] && [ "${GSTACK_SKIP_COREUTILS:-0}" != "1" ]; then
|
|
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
|
if command -v brew >/dev/null 2>&1; then
|
|
log "Installing coreutils for Codex hang protection (set GSTACK_SKIP_COREUTILS=1 to skip)..."
|
|
brew install coreutils >/dev/null 2>&1 || log "warning: brew install coreutils failed; /codex will run without hang protection"
|
|
else
|
|
log "warning: Homebrew not found. /codex will run without hang protection. Install coreutils manually or set GSTACK_SKIP_COREUTILS=1."
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ ! -x "$BROWSE_BIN" ]; then
|
|
echo "gstack setup failed: browse binary missing at $BROWSE_BIN" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 1b. Generate .agents/ Codex skill docs — always regenerate to prevent stale descriptions.
|
|
# .agents/ is no longer committed — generated at setup time from .tmpl templates.
|
|
# bun run build already does this, but we need it when NEEDS_BUILD=0 (binary is fresh).
|
|
# Always regenerate: generation is fast (<2s) and mtime-based staleness checks are fragile
|
|
# (miss stale files when timestamps match after clone/checkout/upgrade).
|
|
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
|
|
NEEDS_AGENTS_GEN=1
|
|
|
|
if [ "$NEEDS_AGENTS_GEN" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
|
log "Generating .agents/ skill docs..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
|
|
bun_cmd run gen:skill-docs --host codex
|
|
)
|
|
fi
|
|
|
|
# 1c. Generate .factory/ Factory Droid skill docs
|
|
if [ "$INSTALL_FACTORY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
|
log "Generating .factory/ skill docs..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
|
|
bun_cmd run gen:skill-docs --host factory
|
|
)
|
|
fi
|
|
|
|
# 1d. Generate .opencode/ OpenCode skill docs
|
|
if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
|
log "Generating .opencode/ skill docs..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
|
|
bun_cmd run gen:skill-docs --host opencode
|
|
)
|
|
fi
|
|
|
|
# 2. Ensure Playwright's Chromium is available
|
|
if ! ensure_playwright_browser; then
|
|
echo "Installing Playwright Chromium..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bunx playwright install chromium
|
|
)
|
|
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
|
|
# Ensure playwright is importable by Node from the gstack directory.
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
echo "gstack setup failed: Node.js is required on Windows (Bun cannot launch Chromium due to a pipe bug)" >&2
|
|
echo " Install Node.js: https://nodejs.org/" >&2
|
|
exit 1
|
|
fi
|
|
echo "Windows detected — verifying Node.js can load Playwright..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
# Bun's node_modules already has playwright; verify Node can require it
|
|
node -e "require('playwright')" 2>/dev/null || npm install --no-save playwright
|
|
# @ngrok/ngrok is externalized in server-node.mjs and resolved at runtime.
|
|
# Verify the platform-specific native binary is installed so /pair-agent
|
|
# tunnels don't fail later with a cryptic module-not-found error.
|
|
node -e "require('@ngrok/ngrok')" 2>/dev/null || npm install --no-save @ngrok/ngrok
|
|
)
|
|
fi
|
|
fi
|
|
|
|
if ! ensure_playwright_browser; then
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
echo "gstack setup failed: Playwright Chromium could not be launched via Node.js" >&2
|
|
echo " This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2
|
|
echo " Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2
|
|
else
|
|
echo "gstack setup failed: Playwright Chromium could not be launched" >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
# 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux).
|
|
# Best-effort: warn instead of failing if it can't install.
|
|
if ! ensure_emoji_font; then
|
|
echo " Note: could not auto-install a color-emoji font. Emoji in make-pdf" >&2
|
|
echo " output may render as boxes (▯). Install one manually, e.g.:" >&2
|
|
echo " Debian/Ubuntu: sudo apt-get install fonts-noto-color-emoji" >&2
|
|
echo " Fedora: sudo dnf install google-noto-color-emoji-fonts" >&2
|
|
echo " Arch: sudo pacman -S noto-fonts-emoji" >&2
|
|
echo " Alpine: sudo apk add font-noto-emoji" >&2
|
|
else
|
|
refresh_browse_daemon_for_fonts
|
|
fi
|
|
|
|
# 3. Ensure ~/.gstack global state directory exists
|
|
mkdir -p "$HOME/.gstack/projects"
|
|
|
|
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
|
# Creates real directories (not symlinks) at the top level with a SKILL.md symlink
|
|
# inside. This ensures Claude discovers them as top-level skills, not nested under
|
|
# gstack/ (which would auto-prefix them as gstack-*).
|
|
# When SKILL_PREFIX=1, directories are prefixed with "gstack-".
|
|
# Use --no-prefix to restore flat names.
|
|
link_claude_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local linked=()
|
|
for skill_dir in "$gstack_dir"/*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
dir_name="$(basename "$skill_dir")"
|
|
# Skip node_modules
|
|
[ "$dir_name" = "node_modules" ] && continue
|
|
# Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test")
|
|
skill_name=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]')
|
|
[ -z "$skill_name" ] && skill_name="$dir_name"
|
|
# Apply gstack- prefix unless --no-prefix or already prefixed
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
case "$skill_name" in
|
|
gstack-*) link_name="$skill_name" ;;
|
|
*) link_name="gstack-$skill_name" ;;
|
|
esac
|
|
else
|
|
link_name="$skill_name"
|
|
fi
|
|
target="$skills_dir/$link_name"
|
|
# Upgrade old directory symlinks to real directories
|
|
if [ -L "$target" ]; then
|
|
rm -f "$target"
|
|
fi
|
|
# Create real directory with symlinked SKILL.md (absolute path)
|
|
# Use mkdir -p unconditionally (idempotent) to avoid TOCTOU race
|
|
mkdir -p "$target"
|
|
# Validate target isn't a symlink before creating the link
|
|
if [ -L "$target/SKILL.md" ]; then rm "$target/SKILL.md"; fi
|
|
_link_or_copy "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"
|
|
# Link the sections/ subdir for carved skills (v2 plan T9). The prefixed
|
|
# Claude skill dir otherwise holds only SKILL.md, so a runtime
|
|
# "Read sections/<name>.md" 404s. Route through _link_or_copy so Windows
|
|
# gets a fresh copy (and re-copies on every ./setup, refreshing staleness).
|
|
if [ -d "$gstack_dir/$dir_name/sections" ]; then
|
|
if [ -e "$target/sections" ] || [ -L "$target/sections" ]; then rm -rf "$target/sections"; fi
|
|
_link_or_copy "$gstack_dir/$dir_name/sections" "$target/sections"
|
|
fi
|
|
linked+=("$link_name")
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
_print_windows_copy_note_once
|
|
fi
|
|
}
|
|
|
|
# Claude Code skips the repo-shaped ~/.claude/skills/gstack directory when
|
|
# building the user-facing slash-command list. Keep the repo path for runtime
|
|
# assets, and add a separate thin wrapper whose frontmatter name remains
|
|
# `gstack` so `/gstack` can autocomplete.
|
|
link_claude_root_skill_alias() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local target="$skills_dir/_gstack-command"
|
|
|
|
[ -f "$gstack_dir/SKILL.md" ] || return 0
|
|
if [ -L "$target" ]; then
|
|
rm -f "$target"
|
|
fi
|
|
mkdir -p "$target"
|
|
if [ -L "$target/SKILL.md" ]; then rm "$target/SKILL.md"; fi
|
|
_link_or_copy "$gstack_dir/SKILL.md" "$target/SKILL.md"
|
|
echo " linked root skill alias: gstack"
|
|
_print_windows_copy_note_once
|
|
}
|
|
|
|
# ─── Helper: remove old unprefixed Claude skill entries ───────────────────────
|
|
# Migration: when switching from flat names to gstack- prefixed names,
|
|
# clean up stale symlinks or directories that point into the gstack directory.
|
|
cleanup_old_claude_symlinks() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local removed=()
|
|
for skill_dir in "$gstack_dir"/*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "node_modules" ] && continue
|
|
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
|
case "$skill_name" in gstack-*) continue ;; esac
|
|
old_target="$skills_dir/$skill_name"
|
|
# Remove directory symlinks pointing into gstack/
|
|
if [ -L "$old_target" ]; then
|
|
link_dest="$(readlink "$old_target" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
gstack/*|*/gstack/*)
|
|
rm -f "$old_target"
|
|
removed+=("$skill_name")
|
|
;;
|
|
esac
|
|
# Remove real directories with symlinked SKILL.md pointing into gstack/
|
|
elif [ -d "$old_target" ] && [ -L "$old_target/SKILL.md" ]; then
|
|
link_dest="$(readlink "$old_target/SKILL.md" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
*gstack*)
|
|
rm -rf "$old_target"
|
|
removed+=("$skill_name")
|
|
;;
|
|
esac
|
|
# Windows install pattern: real dir with real-file SKILL.md (no symlink
|
|
# available, so we can't readlink to verify provenance). The outer loop
|
|
# iterates known gstack skill names from "$gstack_dir"/*, so a name match
|
|
# plus IS_WINDOWS is safe to treat as gstack-managed during a mode flip.
|
|
elif [ "$IS_WINDOWS" -eq 1 ] && [ -d "$old_target" ] && [ -f "$old_target/SKILL.md" ]; then
|
|
rm -rf "$old_target"
|
|
removed+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#removed[@]} -gt 0 ]; then
|
|
echo " cleaned up old entries: ${removed[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── Helper: remove old prefixed Claude skill entries ─────────────────────────
|
|
# Reverse migration: when switching from gstack- prefixed names to flat names,
|
|
# clean up stale gstack-* symlinks or directories that point into the gstack directory.
|
|
cleanup_prefixed_claude_symlinks() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local removed=()
|
|
for skill_dir in "$gstack_dir"/*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "node_modules" ] && continue
|
|
# Only clean up prefixed entries for dirs that AREN'T already prefixed
|
|
# (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
|
|
case "$skill_name" in gstack-*) continue ;; esac
|
|
prefixed_target="$skills_dir/gstack-$skill_name"
|
|
# Remove directory symlinks pointing into gstack/
|
|
if [ -L "$prefixed_target" ]; then
|
|
link_dest="$(readlink "$prefixed_target" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
gstack/*|*/gstack/*)
|
|
rm -f "$prefixed_target"
|
|
removed+=("gstack-$skill_name")
|
|
;;
|
|
esac
|
|
# Remove real directories with symlinked SKILL.md pointing into gstack/
|
|
elif [ -d "$prefixed_target" ] && [ -L "$prefixed_target/SKILL.md" ]; then
|
|
link_dest="$(readlink "$prefixed_target/SKILL.md" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
*gstack*)
|
|
rm -rf "$prefixed_target"
|
|
removed+=("gstack-$skill_name")
|
|
;;
|
|
esac
|
|
# Windows install pattern: real dir with real-file SKILL.md. Same
|
|
# reasoning as cleanup_old_claude_symlinks — directory name match plus
|
|
# IS_WINDOWS is safe during a mode flip.
|
|
elif [ "$IS_WINDOWS" -eq 1 ] && [ -d "$prefixed_target" ] && [ -f "$prefixed_target/SKILL.md" ]; then
|
|
rm -rf "$prefixed_target"
|
|
removed+=("gstack-$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#removed[@]} -gt 0 ]; then
|
|
echo " cleaned up prefixed entries: ${removed[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── Helper: link generated Codex skills into a skills parent directory ──
|
|
# Installs from .agents/skills/gstack-* (the generated Codex-format skills)
|
|
# instead of source dirs (which have Claude paths).
|
|
link_codex_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local agents_dir="$gstack_dir/.agents/skills"
|
|
local linked=()
|
|
|
|
if [ ! -d "$agents_dir" ]; then
|
|
echo " Generating .agents/ skill docs..."
|
|
( cd "$gstack_dir" && bun run gen:skill-docs --host codex )
|
|
fi
|
|
|
|
if [ ! -d "$agents_dir" ]; then
|
|
echo " warning: .agents/skills/ generation failed — run 'bun run gen:skill-docs --host codex' manually" >&2
|
|
return 1
|
|
fi
|
|
|
|
for skill_dir in "$agents_dir"/gstack*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
# Skip the sidecar directory — it contains runtime asset symlinks (bin/,
|
|
# browse/), not a skill. Linking it would overwrite the root gstack
|
|
# symlink that Step 5 already pointed at the repo root.
|
|
[ "$skill_name" = "gstack" ] && continue
|
|
target="$skills_dir/$skill_name"
|
|
# Create or update symlink
|
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
|
_link_or_copy "$skill_dir" "$target"
|
|
linked+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── Helper: create .agents/skills/gstack/ sidecar symlinks ──────────
|
|
# Codex/Gemini/Cursor read skills from .agents/skills/. We link runtime
|
|
# assets (bin/, browse/dist/, review/, qa/, etc.) so skill templates can
|
|
# resolve paths like $SKILL_ROOT/review/design-checklist.md.
|
|
create_agents_sidecar() {
|
|
local repo_root="$1"
|
|
local agents_gstack="$repo_root/.agents/skills/gstack"
|
|
mkdir -p "$agents_gstack"
|
|
|
|
# Sidecar directories that skills reference at runtime
|
|
for asset in bin browse review qa; do
|
|
local src="$SOURCE_GSTACK_DIR/$asset"
|
|
local dst="$agents_gstack/$asset"
|
|
if [ -d "$src" ] || [ -f "$src" ]; then
|
|
if [ -L "$dst" ] || [ ! -e "$dst" ]; then
|
|
_link_or_copy "$src" "$dst"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Sidecar files that skills reference at runtime
|
|
for file in ETHOS.md; do
|
|
local src="$SOURCE_GSTACK_DIR/$file"
|
|
local dst="$agents_gstack/$file"
|
|
if [ -f "$src" ]; then
|
|
if [ -L "$dst" ] || [ ! -e "$dst" ]; then
|
|
_link_or_copy "$src" "$dst"
|
|
fi
|
|
fi
|
|
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"
|
|
elif [ -d "$codex_gstack" ] && [ "$codex_gstack" != "$gstack_dir" ]; then
|
|
# Old direct installs left a real directory here with stale source skills.
|
|
# Remove it so we start fresh with only the minimal runtime assets.
|
|
rm -rf "$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
|
|
_link_or_copy "$agents_dir/gstack/SKILL.md" "$codex_gstack/SKILL.md"
|
|
fi
|
|
if [ -d "$gstack_dir/bin" ]; then
|
|
_link_or_copy "$gstack_dir/bin" "$codex_gstack/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/dist" ]; then
|
|
_link_or_copy "$gstack_dir/browse/dist" "$codex_gstack/browse/dist"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/bin" ]; then
|
|
_link_or_copy "$gstack_dir/browse/bin" "$codex_gstack/browse/bin"
|
|
fi
|
|
if [ -f "$agents_dir/gstack-upgrade/SKILL.md" ]; then
|
|
_link_or_copy "$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
|
|
_link_or_copy "$gstack_dir/review/$f" "$codex_gstack/review/$f"
|
|
fi
|
|
done
|
|
# ETHOS.md — referenced by "Search Before Building" in all skill preambles
|
|
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
|
_link_or_copy "$gstack_dir/ETHOS.md" "$codex_gstack/ETHOS.md"
|
|
fi
|
|
}
|
|
|
|
create_factory_runtime_root() {
|
|
local gstack_dir="$1"
|
|
local factory_gstack="$2"
|
|
local factory_dir="$gstack_dir/.factory/skills"
|
|
|
|
if [ -L "$factory_gstack" ]; then
|
|
rm -f "$factory_gstack"
|
|
elif [ -d "$factory_gstack" ] && [ "$factory_gstack" != "$gstack_dir" ]; then
|
|
rm -rf "$factory_gstack"
|
|
fi
|
|
|
|
mkdir -p "$factory_gstack" "$factory_gstack/browse" "$factory_gstack/gstack-upgrade" "$factory_gstack/review"
|
|
|
|
if [ -f "$factory_dir/gstack/SKILL.md" ]; then
|
|
_link_or_copy "$factory_dir/gstack/SKILL.md" "$factory_gstack/SKILL.md"
|
|
fi
|
|
if [ -d "$gstack_dir/bin" ]; then
|
|
_link_or_copy "$gstack_dir/bin" "$factory_gstack/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/dist" ]; then
|
|
_link_or_copy "$gstack_dir/browse/dist" "$factory_gstack/browse/dist"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/bin" ]; then
|
|
_link_or_copy "$gstack_dir/browse/bin" "$factory_gstack/browse/bin"
|
|
fi
|
|
if [ -f "$factory_dir/gstack-upgrade/SKILL.md" ]; then
|
|
_link_or_copy "$factory_dir/gstack-upgrade/SKILL.md" "$factory_gstack/gstack-upgrade/SKILL.md"
|
|
fi
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$gstack_dir/review/$f" ]; then
|
|
_link_or_copy "$gstack_dir/review/$f" "$factory_gstack/review/$f"
|
|
fi
|
|
done
|
|
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
|
_link_or_copy "$gstack_dir/ETHOS.md" "$factory_gstack/ETHOS.md"
|
|
fi
|
|
}
|
|
|
|
create_opencode_runtime_root() {
|
|
local gstack_dir="$1"
|
|
local opencode_gstack="$2"
|
|
local opencode_dir="$gstack_dir/.opencode/skills"
|
|
|
|
if [ -L "$opencode_gstack" ]; then
|
|
rm -f "$opencode_gstack"
|
|
elif [ -d "$opencode_gstack" ] && [ "$opencode_gstack" != "$gstack_dir" ]; then
|
|
rm -rf "$opencode_gstack"
|
|
fi
|
|
|
|
mkdir -p "$opencode_gstack" "$opencode_gstack/browse" "$opencode_gstack/design" "$opencode_gstack/gstack-upgrade" "$opencode_gstack/review" "$opencode_gstack/qa" "$opencode_gstack/plan-devex-review"
|
|
|
|
if [ -f "$opencode_dir/gstack/SKILL.md" ]; then
|
|
_link_or_copy "$opencode_dir/gstack/SKILL.md" "$opencode_gstack/SKILL.md"
|
|
fi
|
|
if [ -d "$gstack_dir/bin" ]; then
|
|
_link_or_copy "$gstack_dir/bin" "$opencode_gstack/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/dist" ]; then
|
|
_link_or_copy "$gstack_dir/browse/dist" "$opencode_gstack/browse/dist"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/bin" ]; then
|
|
_link_or_copy "$gstack_dir/browse/bin" "$opencode_gstack/browse/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/design/dist" ]; then
|
|
_link_or_copy "$gstack_dir/design/dist" "$opencode_gstack/design/dist"
|
|
fi
|
|
if [ -f "$opencode_dir/gstack-upgrade/SKILL.md" ]; then
|
|
_link_or_copy "$opencode_dir/gstack-upgrade/SKILL.md" "$opencode_gstack/gstack-upgrade/SKILL.md"
|
|
fi
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$gstack_dir/review/$f" ]; then
|
|
_link_or_copy "$gstack_dir/review/$f" "$opencode_gstack/review/$f"
|
|
fi
|
|
done
|
|
if [ -d "$gstack_dir/review/specialists" ]; then
|
|
_link_or_copy "$gstack_dir/review/specialists" "$opencode_gstack/review/specialists"
|
|
fi
|
|
if [ -d "$gstack_dir/qa/templates" ]; then
|
|
_link_or_copy "$gstack_dir/qa/templates" "$opencode_gstack/qa/templates"
|
|
fi
|
|
if [ -d "$gstack_dir/qa/references" ]; then
|
|
_link_or_copy "$gstack_dir/qa/references" "$opencode_gstack/qa/references"
|
|
fi
|
|
if [ -f "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" ]; then
|
|
_link_or_copy "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" "$opencode_gstack/plan-devex-review/dx-hall-of-fame.md"
|
|
fi
|
|
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
|
_link_or_copy "$gstack_dir/ETHOS.md" "$opencode_gstack/ETHOS.md"
|
|
fi
|
|
}
|
|
|
|
link_factory_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local factory_dir="$gstack_dir/.factory/skills"
|
|
local linked=()
|
|
|
|
if [ ! -d "$factory_dir" ]; then
|
|
echo " Generating .factory/ skill docs..."
|
|
( cd "$gstack_dir" && bun run gen:skill-docs --host factory )
|
|
fi
|
|
|
|
if [ ! -d "$factory_dir" ]; then
|
|
echo " warning: .factory/skills/ generation failed — run 'bun run gen:skill-docs --host factory' manually" >&2
|
|
return 1
|
|
fi
|
|
|
|
for skill_dir in "$factory_dir"/gstack*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "gstack" ] && continue
|
|
target="$skills_dir/$skill_name"
|
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
|
_link_or_copy "$skill_dir" "$target"
|
|
linked+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
link_opencode_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local opencode_dir="$gstack_dir/.opencode/skills"
|
|
local linked=()
|
|
|
|
if [ ! -d "$opencode_dir" ]; then
|
|
echo " Generating .opencode/ skill docs..."
|
|
( cd "$gstack_dir" && bun run gen:skill-docs --host opencode )
|
|
fi
|
|
|
|
if [ ! -d "$opencode_dir" ]; then
|
|
echo " warning: .opencode/skills/ generation failed — run 'bun run gen:skill-docs --host opencode' manually" >&2
|
|
return 1
|
|
fi
|
|
|
|
for skill_dir in "$opencode_dir"/gstack*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "gstack" ] && continue
|
|
target="$skills_dir/$skill_name"
|
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
|
_link_or_copy "$skill_dir" "$target"
|
|
linked+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
# 4. Install for Claude (default)
|
|
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
|
|
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
|
|
CODEX_REPO_LOCAL=0
|
|
if [ "$SKILLS_BASENAME" = "skills" ] && [ "$SKILLS_PARENT_BASENAME" = ".agents" ]; then
|
|
CODEX_REPO_LOCAL=1
|
|
fi
|
|
|
|
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
|
|
if [ "$SKILLS_BASENAME" = "skills" ]; then
|
|
# Clean up stale symlinks from the opposite prefix mode
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
cleanup_old_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
else
|
|
cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
fi
|
|
# Patch name: fields BEFORE creating symlinks so link_claude_skill_dirs
|
|
# reads the correct (patched) name: values for symlink naming
|
|
"$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX"
|
|
link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
# Self-healing: re-run gstack-relink to ensure name: fields and directory
|
|
# names are consistent with the config. This catches cases where an interrupted
|
|
# setup, stale git state, or gen:skill-docs left name: fields out of sync.
|
|
GSTACK_RELINK="$SOURCE_GSTACK_DIR/bin/gstack-relink"
|
|
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
|
|
# Backwards-compat alias: /connect-chrome → /open-gstack-browser
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome"
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome"
|
|
fi
|
|
if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then
|
|
_link_or_copy "gstack/open-gstack-browser" "$_OGB_LINK"
|
|
fi
|
|
if [ "$LOCAL_INSTALL" -eq 1 ]; then
|
|
log "gstack ready (project-local)."
|
|
log " skills: $INSTALL_SKILLS_DIR"
|
|
else
|
|
log "gstack ready (claude)."
|
|
fi
|
|
log " browse: $BROWSE_BIN"
|
|
else
|
|
# Not inside a skills/ directory — would symlink the source into
|
|
# ~/.claude/skills/gstack/ and register from there.
|
|
CLAUDE_SKILLS_DIR="$HOME/.claude/skills"
|
|
CLAUDE_GSTACK_LINK="$CLAUDE_SKILLS_DIR/gstack"
|
|
|
|
# Conductor worktree guard: if ~/.claude/skills/gstack is already a real
|
|
# (non-symlink) directory pointing to a *different* install, refuse to plant
|
|
# a symlink there. On macOS/BSD, `ln -snf SRC DST` won't replace a real DST;
|
|
# it creates DST/$(basename SRC) → SRC inside it. The result is per-worktree
|
|
# symlinks leaking into the global install that Claude Code picks up as
|
|
# separate top-level skills (dublin-v1, lincoln-v2, ...). Typical trigger:
|
|
# running ./setup from a Conductor worktree of the gstack repo itself.
|
|
_SKIP_CLAUDE_REGISTER=0
|
|
if [ -d "$CLAUDE_GSTACK_LINK" ] && [ ! -L "$CLAUDE_GSTACK_LINK" ]; then
|
|
_EXISTING_REAL=$(cd "$CLAUDE_GSTACK_LINK" 2>/dev/null && pwd -P || echo "")
|
|
if [ -n "$_EXISTING_REAL" ] && [ "$_EXISTING_REAL" != "$SOURCE_GSTACK_DIR" ]; then
|
|
_SKIP_CLAUDE_REGISTER=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$_SKIP_CLAUDE_REGISTER" -eq 1 ]; then
|
|
log ""
|
|
log " $CLAUDE_GSTACK_LINK already exists as a separate global install."
|
|
log " Skipping Claude skill registration to avoid polluting it with"
|
|
log " per-worktree symlinks. (Binaries still built locally for dev.)"
|
|
log ""
|
|
log " Global install: $CLAUDE_GSTACK_LINK"
|
|
log " This worktree: $SOURCE_GSTACK_DIR"
|
|
log ""
|
|
log " To register this worktree as the active gstack, remove the global"
|
|
log " install first: rm -rf $CLAUDE_GSTACK_LINK"
|
|
log ""
|
|
log "gstack built (claude registration skipped)."
|
|
log " browse: $BROWSE_BIN"
|
|
else
|
|
mkdir -p "$CLAUDE_SKILLS_DIR"
|
|
_link_or_copy "$SOURCE_GSTACK_DIR" "$CLAUDE_GSTACK_LINK"
|
|
log " symlinked $CLAUDE_GSTACK_LINK -> $SOURCE_GSTACK_DIR"
|
|
INSTALL_SKILLS_DIR="$CLAUDE_SKILLS_DIR"
|
|
INSTALL_GSTACK_DIR="$CLAUDE_GSTACK_LINK"
|
|
# Clean up stale symlinks from the opposite prefix mode
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
cleanup_old_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
else
|
|
cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
fi
|
|
"$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX"
|
|
link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
GSTACK_RELINK="$SOURCE_GSTACK_DIR/bin/gstack-relink"
|
|
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
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome"
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome"
|
|
fi
|
|
if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then
|
|
_link_or_copy "gstack/open-gstack-browser" "$_OGB_LINK"
|
|
fi
|
|
log "gstack ready (claude)."
|
|
log " browse: $BROWSE_BIN"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 5. Install for Codex
|
|
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
if [ "$CODEX_REPO_LOCAL" -eq 1 ]; then
|
|
CODEX_SKILLS="$INSTALL_SKILLS_DIR"
|
|
CODEX_GSTACK="$INSTALL_GSTACK_DIR"
|
|
fi
|
|
mkdir -p "$CODEX_SKILLS"
|
|
|
|
# Skip runtime root creation for repo-local installs — the checkout IS the runtime root.
|
|
# create_codex_runtime_root would create self-referential symlinks (bin → bin, etc.).
|
|
if [ "$CODEX_REPO_LOCAL" -eq 0 ]; then
|
|
create_codex_runtime_root "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
|
|
fi
|
|
# Install generated Codex-format skills (not Claude source dirs)
|
|
link_codex_skill_dirs "$SOURCE_GSTACK_DIR" "$CODEX_SKILLS"
|
|
|
|
log "gstack ready (codex)."
|
|
log " browse: $BROWSE_BIN"
|
|
log " codex skills: $CODEX_SKILLS"
|
|
fi
|
|
|
|
# 6. Install for Kiro CLI (copy from .agents/skills, rewrite paths)
|
|
if [ "$INSTALL_KIRO" -eq 1 ]; then
|
|
KIRO_SKILLS="$HOME/.kiro/skills"
|
|
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
|
|
mkdir -p "$KIRO_SKILLS"
|
|
|
|
# Create gstack dir with symlinks for runtime assets, copy+sed for SKILL.md
|
|
KIRO_GSTACK="$KIRO_SKILLS/gstack"
|
|
# Remove old whole-dir symlink from previous installs
|
|
[ -L "$KIRO_GSTACK" ] && rm -f "$KIRO_GSTACK"
|
|
mkdir -p "$KIRO_GSTACK" "$KIRO_GSTACK/browse" "$KIRO_GSTACK/gstack-upgrade" "$KIRO_GSTACK/review"
|
|
_link_or_copy "$SOURCE_GSTACK_DIR/bin" "$KIRO_GSTACK/bin"
|
|
_link_or_copy "$SOURCE_GSTACK_DIR/browse/dist" "$KIRO_GSTACK/browse/dist"
|
|
_link_or_copy "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin"
|
|
# ETHOS.md — referenced by "Search Before Building" in all skill preambles
|
|
if [ -f "$SOURCE_GSTACK_DIR/ETHOS.md" ]; then
|
|
_link_or_copy "$SOURCE_GSTACK_DIR/ETHOS.md" "$KIRO_GSTACK/ETHOS.md"
|
|
fi
|
|
# gstack-upgrade skill
|
|
if [ -f "$AGENTS_DIR/gstack-upgrade/SKILL.md" ]; then
|
|
_link_or_copy "$AGENTS_DIR/gstack-upgrade/SKILL.md" "$KIRO_GSTACK/gstack-upgrade/SKILL.md"
|
|
fi
|
|
# Review runtime assets (individual files, not whole dir)
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$SOURCE_GSTACK_DIR/review/$f" ]; then
|
|
_link_or_copy "$SOURCE_GSTACK_DIR/review/$f" "$KIRO_GSTACK/review/$f"
|
|
fi
|
|
done
|
|
|
|
# Rewrite root SKILL.md paths for Kiro
|
|
sed -e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
-e "s|\.claude/skills/gstack|.kiro/skills/gstack|g" \
|
|
-e "s|\.claude/skills|.kiro/skills|g" \
|
|
"$SOURCE_GSTACK_DIR/SKILL.md" > "$KIRO_GSTACK/SKILL.md"
|
|
|
|
if [ ! -d "$AGENTS_DIR" ]; then
|
|
echo " warning: no .agents/skills/ directory found — run 'bun run build' first" >&2
|
|
else
|
|
for skill_dir in "$AGENTS_DIR"/gstack*/; do
|
|
[ -f "$skill_dir/SKILL.md" ] || continue
|
|
skill_name="$(basename "$skill_dir")"
|
|
target_dir="$KIRO_SKILLS/$skill_name"
|
|
mkdir -p "$target_dir"
|
|
# Generated Codex skills use $HOME/.codex (not ~/), plus $GSTACK_ROOT variables.
|
|
# Rewrite the default GSTACK_ROOT value and any remaining literal paths.
|
|
sed -e 's|\$HOME/.codex/skills/gstack|$HOME/.kiro/skills/gstack|g' \
|
|
-e "s|~/.codex/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
-e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
"$skill_dir/SKILL.md" > "$target_dir/SKILL.md"
|
|
# Carved skills (v2 plan T9): rewrite + copy each sections/*.md the same way,
|
|
# so a runtime "Read sections/<name>.md" resolves under ~/.kiro and doesn't
|
|
# leak a ~/.codex or ~/.claude path. Kiro builds from the codex output, so
|
|
# these section files only exist for skills that have been carved.
|
|
if [ -d "$skill_dir/sections" ]; then
|
|
mkdir -p "$target_dir/sections"
|
|
for section_file in "$skill_dir/sections"/*; do
|
|
[ -f "$section_file" ] || continue
|
|
sed -e 's|\$HOME/.codex/skills/gstack|$HOME/.kiro/skills/gstack|g' \
|
|
-e "s|~/.codex/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
-e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
"$section_file" > "$target_dir/sections/$(basename "$section_file")"
|
|
done
|
|
fi
|
|
done
|
|
echo "gstack ready (kiro)."
|
|
echo " browse: $BROWSE_BIN"
|
|
echo " kiro skills: $KIRO_SKILLS"
|
|
fi
|
|
fi
|
|
|
|
# 6b. Install for Factory Droid
|
|
if [ "$INSTALL_FACTORY" -eq 1 ]; then
|
|
mkdir -p "$FACTORY_SKILLS"
|
|
create_factory_runtime_root "$SOURCE_GSTACK_DIR" "$FACTORY_GSTACK"
|
|
link_factory_skill_dirs "$SOURCE_GSTACK_DIR" "$FACTORY_SKILLS"
|
|
echo "gstack ready (factory)."
|
|
echo " browse: $BROWSE_BIN"
|
|
echo " factory skills: $FACTORY_SKILLS"
|
|
fi
|
|
|
|
# 6c. Install for OpenCode
|
|
if [ "$INSTALL_OPENCODE" -eq 1 ]; then
|
|
mkdir -p "$OPENCODE_SKILLS"
|
|
create_opencode_runtime_root "$SOURCE_GSTACK_DIR" "$OPENCODE_GSTACK"
|
|
link_opencode_skill_dirs "$SOURCE_GSTACK_DIR" "$OPENCODE_SKILLS"
|
|
echo "gstack ready (opencode)."
|
|
echo " browse: $BROWSE_BIN"
|
|
echo " opencode skills: $OPENCODE_SKILLS"
|
|
fi
|
|
|
|
# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
|
|
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
|
|
# so the runtime assets must live there for both global and repo-local installs.
|
|
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
create_agents_sidecar "$SOURCE_GSTACK_DIR"
|
|
fi
|
|
|
|
# 8. Run pending version migrations
|
|
# Migrations handle state fixes that ./setup alone can't cover (stale config,
|
|
# orphaned files, directory structure changes). Each migration is idempotent.
|
|
MIGRATIONS_DIR="$SOURCE_GSTACK_DIR/gstack-upgrade/migrations"
|
|
CURRENT_VERSION=$(cat "$SOURCE_GSTACK_DIR/VERSION" 2>/dev/null || echo "unknown")
|
|
LAST_SETUP_VERSION=$(cat "$HOME/.gstack/.last-setup-version" 2>/dev/null || echo "0.0.0.0")
|
|
if [ -d "$MIGRATIONS_DIR" ] && [ "$CURRENT_VERSION" != "unknown" ] && [ "$LAST_SETUP_VERSION" != "$CURRENT_VERSION" ]; then
|
|
# Fresh install (no marker file) — skip migrations, just write marker
|
|
if [ ! -f "$HOME/.gstack/.last-setup-version" ]; then
|
|
: # fall through to marker write below
|
|
else
|
|
find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V | while IFS= read -r migration; do
|
|
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
|
|
# Run if migration is newer than last setup version AND not newer than current version
|
|
if [ "$(printf '%s\n%s' "$LAST_SETUP_VERSION" "$m_ver" | sort -V | head -1)" = "$LAST_SETUP_VERSION" ] && [ "$LAST_SETUP_VERSION" != "$m_ver" ] \
|
|
&& [ "$(printf '%s\n%s' "$m_ver" "$CURRENT_VERSION" | sort -V | tail -1)" = "$CURRENT_VERSION" ]; then
|
|
echo " running migration $m_ver..."
|
|
bash "$migration" || echo " warning: migration $m_ver had errors (non-fatal)"
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
mkdir -p "$HOME/.gstack"
|
|
if [ "$CURRENT_VERSION" != "unknown" ]; then
|
|
echo "$CURRENT_VERSION" > "$HOME/.gstack/.last-setup-version"
|
|
fi
|
|
|
|
# 9. First-time welcome + legacy cleanup
|
|
if [ ! -f "$HOME/.gstack/.welcome-seen" ]; then
|
|
log " Welcome! Run /gstack-upgrade anytime to stay current."
|
|
touch "$HOME/.gstack/.welcome-seen"
|
|
fi
|
|
rm -f /tmp/gstack-latest-version
|
|
|
|
# 10. Team mode: register/unregister SessionStart hook
|
|
SETTINGS_HOOK="$SOURCE_GSTACK_DIR/bin/gstack-settings-hook"
|
|
HOOK_CMD="$SOURCE_GSTACK_DIR/bin/gstack-session-update"
|
|
|
|
if [ "$TEAM_MODE" -eq 1 ]; then
|
|
"$GSTACK_CONFIG" set auto_upgrade true 2>/dev/null || true
|
|
"$GSTACK_CONFIG" set team_mode true 2>/dev/null || true
|
|
|
|
# Register SessionStart hook in Claude Code settings
|
|
if [ -x "$SETTINGS_HOOK" ]; then
|
|
"$SETTINGS_HOOK" add "$HOOK_CMD" 2>/dev/null || true
|
|
fi
|
|
|
|
log ""
|
|
log "Team mode enabled: gstack will auto-update at the start of each Claude Code session."
|
|
log " Hook: $HOOK_CMD"
|
|
log " To disable: ./setup --no-team"
|
|
log ""
|
|
log "Bootstrap your repo:"
|
|
log " cd <your-repo> && $SOURCE_GSTACK_DIR/bin/gstack-team-init required"
|
|
fi
|
|
|
|
if [ "$NO_TEAM_MODE" -eq 1 ]; then
|
|
"$GSTACK_CONFIG" set auto_upgrade false 2>/dev/null || true
|
|
"$GSTACK_CONFIG" set team_mode false 2>/dev/null || true
|
|
|
|
# Remove SessionStart hook from Claude Code settings
|
|
if [ -x "$SETTINGS_HOOK" ]; then
|
|
"$SETTINGS_HOOK" remove "$HOOK_CMD" 2>/dev/null || true
|
|
fi
|
|
|
|
log "Team mode disabled: auto-update hook removed."
|
|
fi
|
|
|
|
# ─── GBrain detection + conditional SKILL.md regen ──────────────────────
|
|
#
|
|
# Detect whether gbrain is installed and persist the result to
|
|
# ~/.gstack/gbrain-detection.json so gen-skill-docs can decide whether to
|
|
# render GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS blocks. If detected,
|
|
# regenerate the Claude-host SKILL.md files with the un-suppressed
|
|
# (compressed) brain-aware blocks via `bun run gen:skill-docs:user`.
|
|
#
|
|
# If gbrain is not detected, the canonical no-gbrain SKILL.md files
|
|
# (which were just generated above by `gen:skill-docs --host claude` if
|
|
# applicable, or which are checked in) stay as-is. Zero token overhead
|
|
# for non-gbrain users.
|
|
#
|
|
# Users who install gbrain after running ./setup should re-run setup OR
|
|
# call `gstack-config gbrain-refresh` + `bun run gen:skill-docs:user`.
|
|
DETECT_BIN="$SOURCE_GSTACK_DIR/bin/gstack-gbrain-detect"
|
|
GBRAIN_STATE_DIR="${GSTACK_HOME:-$HOME/.gstack}"
|
|
DETECTION_FILE="$GBRAIN_STATE_DIR/gbrain-detection.json"
|
|
mkdir -p "$GBRAIN_STATE_DIR"
|
|
if [ -x "$DETECT_BIN" ]; then
|
|
if "$DETECT_BIN" > "$DETECTION_FILE.tmp" 2>/dev/null; then
|
|
mv "$DETECTION_FILE.tmp" "$DETECTION_FILE"
|
|
if grep -q '"gbrain_local_status": "ok"' "$DETECTION_FILE" 2>/dev/null; then
|
|
log "gbrain detected — regenerating Claude SKILL.md with brain-aware blocks (~250 token overhead per planning skill)..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun_cmd run gen:skill-docs:user --host claude 2>&1 | tail -3
|
|
) || log " warning: gen:skill-docs:user failed — run 'bun run gen:skill-docs:user' manually if you want brain-aware blocks"
|
|
else
|
|
log "gbrain not detected — brain-aware blocks suppressed in planning-skill SKILL.md files (zero token overhead)."
|
|
log " To enable: install gbrain via /setup-gbrain, then re-run ./setup or 'gstack-config gbrain-refresh'."
|
|
fi
|
|
else
|
|
rm -f "$DETECTION_FILE.tmp"
|
|
log " warning: gstack-gbrain-detect failed — brain-aware blocks will stay suppressed"
|
|
fi
|
|
fi
|
|
|
|
# 11. Plan-tune cathedral hook install (T8).
|
|
#
|
|
# Registers PostToolUse (deterministic AUQ capture) + PreToolUse (preference
|
|
# enforcement) hooks in ~/.claude/settings.json so /plan-tune actually does
|
|
# something at runtime instead of being agent-convention. Explicit consent UX
|
|
# per D4 + Codex: never mutate settings.json silently.
|
|
#
|
|
# Idempotent via _gstack_source tag = 'plan-tune-cathedral'. If both hooks
|
|
# already registered under that tag, the install is a no-op (no prompt).
|
|
PLAN_TUNE_LOG_HOOK="$SOURCE_GSTACK_DIR/hosts/claude/hooks/question-log-hook"
|
|
PLAN_TUNE_PREF_HOOK="$SOURCE_GSTACK_DIR/hosts/claude/hooks/question-preference-hook"
|
|
PLAN_TUNE_INSTALL_MARKER="$HOME/.gstack/.plan-tune-hooks-prompted"
|
|
|
|
if [ "$NO_TEAM_MODE" -ne 1 ] \
|
|
&& [ -x "$SETTINGS_HOOK" ] \
|
|
&& [ -x "$PLAN_TUNE_LOG_HOOK" ] \
|
|
&& [ -x "$PLAN_TUNE_PREF_HOOK" ]; then
|
|
|
|
# Already installed? Check the settings.json for our source tag.
|
|
ALREADY_INSTALLED=0
|
|
if "$SETTINGS_HOOK" list-sources 2>/dev/null | grep -q "plan-tune-cathedral"; then
|
|
ALREADY_INSTALLED=1
|
|
fi
|
|
|
|
# Resolve the desired action without ever blocking.
|
|
# Priority: CLI flag (--plan-tune-hooks / --no-plan-tune-hooks)
|
|
# > env (GSTACK_PLAN_TUNE_HOOKS=yes|no)
|
|
# > saved config (plan_tune_hooks)
|
|
# > smart default ("prompt" → timed prompt on a real TTY, else skip).
|
|
# This guarantees scripted/workspace setups (conductor, CI) are never
|
|
# interactive: pass --no-plan-tune-hooks (or --plan-tune-hooks) and the
|
|
# block runs to completion with no `read`.
|
|
PT_DECISION="$PLAN_TUNE_HOOKS_MODE"
|
|
[ -z "$PT_DECISION" ] && PT_DECISION="${GSTACK_PLAN_TUNE_HOOKS:-}"
|
|
[ -z "$PT_DECISION" ] && PT_DECISION="$("$GSTACK_CONFIG" get plan_tune_hooks 2>/dev/null || true)"
|
|
# Normalize: strip whitespace + lowercase so "YES", "Yes", " yes" from a flag
|
|
# or env var all resolve correctly (an unrecognized opt-in must NOT silently
|
|
# downgrade to skip). Unknown values fall through to "prompt".
|
|
PT_DECISION=$(printf '%s' "$PT_DECISION" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
|
|
case "$PT_DECISION" in
|
|
y|yes|true|install|on|1) PT_DECISION="yes" ;;
|
|
n|no|false|skip|off|0) PT_DECISION="no" ;;
|
|
*) PT_DECISION="prompt" ;;
|
|
esac
|
|
|
|
_install_plan_tune_hooks() {
|
|
"$SETTINGS_HOOK" add-event \
|
|
--event PostToolUse \
|
|
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
|
--command "$PLAN_TUNE_LOG_HOOK" \
|
|
--source plan-tune-cathedral \
|
|
--timeout 5
|
|
"$SETTINGS_HOOK" add-event \
|
|
--event PreToolUse \
|
|
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
|
--command "$PLAN_TUNE_PREF_HOOK" \
|
|
--source plan-tune-cathedral \
|
|
--timeout 5
|
|
}
|
|
|
|
if [ "$ALREADY_INSTALLED" -eq 1 ]; then
|
|
log ""
|
|
log "Plan-tune hooks already installed. Run \`$SETTINGS_HOOK list-sources\` to inspect."
|
|
elif [ "$PT_DECISION" = "yes" ]; then
|
|
# Explicit opt-in (flag / env / config). Non-interactive.
|
|
_install_plan_tune_hooks
|
|
log ""
|
|
log "Plan-tune hooks installed. Run /plan-tune anytime to inspect."
|
|
touch "$PLAN_TUNE_INSTALL_MARKER"
|
|
elif [ "$PT_DECISION" = "no" ]; then
|
|
# Explicit opt-out (flag / env / config). Non-interactive.
|
|
log ""
|
|
log "Plan-tune cathedral hooks not installed (opted out)."
|
|
log "Install later with: ./setup --plan-tune-hooks (or /update-config)."
|
|
touch "$PLAN_TUNE_INSTALL_MARKER"
|
|
elif [ -f "$PLAN_TUNE_INSTALL_MARKER" ]; then
|
|
# Previously declined. Don't re-ask. User can re-enable via /update-config.
|
|
:
|
|
elif [ "$QUIET" -ne 1 ] && [ -t 0 ] && [ -t 1 ]; then
|
|
# Real interactive terminal with no recorded preference: ask, with explicit
|
|
# consent + diff preview. The read is time-bounded and defaults to "skip" so
|
|
# it can never hang an automated/forwarded TTY (the conductor failure mode).
|
|
_PT_PROMPT_TIMEOUT=10 # single source of truth for the read + the countdown text
|
|
log ""
|
|
log "──────────────────────────────────────────────────────────"
|
|
log "Plan-tune cathedral: install Claude Code hooks?"
|
|
log "──────────────────────────────────────────────────────────"
|
|
log ""
|
|
log "These hooks make /plan-tune settings actually bind at runtime:"
|
|
log " • PostToolUse hook captures every AskUserQuestion fire (no agent"
|
|
log " compliance required). Today it's agent-convention and the log"
|
|
log " is empty in dogfood."
|
|
log " • PreToolUse hook enforces 'never-ask' preferences via Claude Code's"
|
|
log " permissionDecision protocol. Today preferences are agent-honored"
|
|
log " convention; this makes them binding."
|
|
log ""
|
|
log "Diff preview (PostToolUse capture hook):"
|
|
"$SETTINGS_HOOK" diff-event \
|
|
--event PostToolUse \
|
|
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
|
--command "$PLAN_TUNE_LOG_HOOK" \
|
|
--source plan-tune-cathedral \
|
|
--timeout 5 2>/dev/null || true
|
|
log ""
|
|
log "Backup: settings.json.bak.<ts> written before any mutation."
|
|
log "Rollback: $SETTINGS_HOOK rollback"
|
|
log ""
|
|
printf "Install both hooks now? [y/N] (default: N, auto-skips in %ss): " "$_PT_PROMPT_TIMEOUT"
|
|
read -t "$_PT_PROMPT_TIMEOUT" -r PLAN_TUNE_INSTALL_REPLY </dev/tty 2>/dev/null || PLAN_TUNE_INSTALL_REPLY=""
|
|
case "$PLAN_TUNE_INSTALL_REPLY" in
|
|
y|Y)
|
|
_install_plan_tune_hooks
|
|
log ""
|
|
log "Plan-tune hooks installed. Run /plan-tune anytime to inspect."
|
|
touch "$PLAN_TUNE_INSTALL_MARKER"
|
|
;;
|
|
n|N)
|
|
log ""
|
|
log "Skipped. Re-run ./setup --plan-tune-hooks or use /update-config to install later."
|
|
touch "$PLAN_TUNE_INSTALL_MARKER"
|
|
;;
|
|
*)
|
|
# Empty / timed out — treat as "ask me again" (don't persist a decline).
|
|
log ""
|
|
log "No response — skipped for now. Re-run ./setup --plan-tune-hooks to install."
|
|
;;
|
|
esac
|
|
else
|
|
# Non-interactive (CI, scripted/workspace setup, quiet). Never prompt.
|
|
log ""
|
|
log "Plan-tune cathedral hooks not installed (non-interactive setup)."
|
|
log "Install with: ./setup --plan-tune-hooks"
|
|
log " (or set GSTACK_PLAN_TUNE_HOOKS=yes, or run the commands below)"
|
|
log " $SETTINGS_HOOK add-event --event PostToolUse \\"
|
|
log " --matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \\"
|
|
log " --command $PLAN_TUNE_LOG_HOOK --source plan-tune-cathedral --timeout 5"
|
|
log " $SETTINGS_HOOK add-event --event PreToolUse \\"
|
|
log " --matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \\"
|
|
log " --command $PLAN_TUNE_PREF_HOOK --source plan-tune-cathedral --timeout 5"
|
|
fi
|
|
fi
|
|
|
|
# Also tear down plan-tune hooks on --no-team (matches the existing pattern).
|
|
if [ "$NO_TEAM_MODE" -eq 1 ] && [ -x "$SETTINGS_HOOK" ]; then
|
|
"$SETTINGS_HOOK" remove-source --source plan-tune-cathedral 2>/dev/null || true
|
|
fi
|