From f2012f3ab3dfb8c0f30a697045da86efec0c6b34 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 8 Oct 2026 08:08:29 +0000 Subject: [PATCH] fix(build): an install renders skills only for the hosts installed from it (#1694) ./setup ran gen:skill-docs --host all inside the install, so a global Claude install at ~/.claude/skills/gstack carried 632 SKILL.md files (34.7 MB) where 56 are its own; Claude Code and Cursor-agent scan that tree, and Cursor-agent freezes above roughly 11 MB. setup now keeps an additive record of the hosts it installs from a checkout (.gstack-installed-hosts, owned by bin/gstack-host-renders.sh), seeded from installs that predate it: registry rows, install roots that resolve to the checkout, host skills that link into its renders, and legacy skill copies with no runtime root. scripts/build.sh renders claude plus the recorded hosts and prints which hosts and why. A checkout with no record (development) or GSTACK_RENDER_HOSTS=all renders every host. The Codex .agents/ render runs only when Codex is installed or recorded; bin/dev-setup records Codex because it serves the repo's .agents/. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .gitignore | 2 + bin/dev-setup | 5 + bin/gstack-host-renders.sh | 150 ++++++++++++++++++++ scripts/build.sh | 15 +- setup | 26 +++- test/host-renders.test.ts | 172 +++++++++++++++++++++++ test/setup-claude-code-migration.test.ts | 2 +- 7 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 bin/gstack-host-renders.sh create mode 100644 test/host-renders.test.ts diff --git a/.gitignore b/.gitignore index e4a01db37..65a5007b9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ lib/cso/images/gstack-cso-preparation .hermes/ .gbrain/ .gbrain-source +# Hosts installed from this checkout (bin/gstack-host-renders.sh, #1694) +.gstack-installed-hosts .context/ extension/.auth.json # xterm assets are vendored from npm at build time; not source-of-truth. diff --git a/bin/dev-setup b/bin/dev-setup index f661b593b..4079e5cc4 100755 --- a/bin/dev-setup +++ b/bin/dev-setup @@ -56,6 +56,11 @@ if [ ! -e "$AGENTS_LINK" ]; then ln -s "$REPO_ROOT" "$AGENTS_LINK" fi +# 5b. Record Codex as a host this checkout serves (bin/gstack-host-renders.sh), +# so setup and `bun run build` keep rendering .agents/ for it (#1694). +. "$REPO_ROOT/bin/gstack-host-renders.sh" +gstack_render_hosts_add "$REPO_ROOT" codex + # 6. Run setup via the symlink so it detects .claude/skills/ as its parent. # # Workspace/dev setup MUST be non-interactive: Conductor runs this under a diff --git a/bin/gstack-host-renders.sh b/bin/gstack-host-renders.sh new file mode 100644 index 000000000..dd6c20b67 --- /dev/null +++ b/bin/gstack-host-renders.sh @@ -0,0 +1,150 @@ +# shellcheck shell=bash +# gstack-host-renders.sh — the one owner of a checkout's installed-hosts record +# and of the prune of skill renders for hosts it does not serve (#1694). +# +# Sourced, never executed: scripts/build.sh, setup and the gstack-upgrade +# migration source it. Bash 3.2 clean. +# +# A checkout renders one skills tree per host (.agents/ for Codex, .factory/, +# .kiro/, ...). Rendering all of them inside an install put ~576 extra SKILL.md +# files (32 MB) under ~/.claude/skills/gstack, a tree Claude Code and +# Cursor-agent scan (#1694). Instead: +# - setup adds each host it installs to $ROOT/.gstack-installed-hosts. The +# record is additive: ./setup --host codex never drops a recorded factory. +# - scripts/build.sh renders claude plus the recorded hosts. A checkout with +# no record (a development checkout) or GSTACK_RENDER_HOSTS=all renders +# every host. +# - every ./setup prunes renders of unrecorded hosts with the +# bin/gstack-relink proof rules: a symlink into this checkout (STRONG) is +# removed; a file proven generated (WEAK: gen-skill-docs' banner, the +# generator's exact openai.yaml shape, or byte identity with this checkout) +# is moved to $GSTACK_STATE_ROOT/backups/host-renders/-/; anything +# else stays where it is. Only directories left empty are removed. + +# host:render-dir for every host whose render lives in its own checkout dir. +# Claude renders into the tracked tree. test/host-renders.test.ts keeps this +# equal to hostSubdir in hosts/*.ts. +GSTACK_HOST_RENDER_DIRS="codex:.agents kiro:.kiro factory:.factory opencode:.opencode cursor:.cursor copilot:.copilot slate:.slate openclaw:.openclaw hermes:.hermes gbrain:.gbrain" + +gstack_render_hosts_file() { printf '%s/.gstack-installed-hosts\n' "$1"; } + +_gstack_render_host_known() { + local pair + [ "$1" = claude ] && return 0 + for pair in $GSTACK_HOST_RENDER_DIRS; do [ "${pair%%:*}" = "$1" ] && return 0; done + return 1 +} + +# gstack_render_hosts_read ROOT — the recorded hosts, one per line, sorted. +gstack_render_hosts_read() { + local file h + file="$(gstack_render_hosts_file "$1")" + [ -f "$file" ] || return 0 + while IFS= read -r h; do + h="${h%%#*}"; h="$(printf '%s' "$h" | tr -d '[:space:]')" + [ -n "$h" ] && _gstack_render_host_known "$h" && printf '%s\n' "$h" + done < "$file" | sort -u +} + +# gstack_render_hosts_has ROOT HOST +gstack_render_hosts_has() { gstack_render_hosts_read "$1" | grep -qx "$2"; } + +# gstack_render_hosts_add ROOT HOST... — add hosts to the record (never removes +# one). Writes only when the set changed, by atomic rename. +gstack_render_hosts_add() { + local root="$1" file tmp old new h + shift + file="$(gstack_render_hosts_file "$root")" + old="$(gstack_render_hosts_read "$root")" + new="$( { [ -n "$old" ] && printf '%s\n' "$old"; for h in "$@"; do _gstack_render_host_known "$h" && printf '%s\n' "$h"; done; } | sort -u)" + [ -f "$file" ] && [ "$new" = "$old" ] && return 0 + tmp="$file.tmp.$$" + if { + echo "# Hosts installed from this gstack checkout, one per line. Written by ./setup;" + echo "# scripts/build.sh renders skills only for these (#1694) and setup prunes the" + echo "# renders of every other host. Add a host by running ./setup --host ." + if [ -n "$new" ]; then printf '%s\n' "$new"; fi + } > "$tmp" && mv -f "$tmp" "$file"; then + return 0 + fi + rm -f "$tmp" + return 1 +} + +# gstack_render_hosts_select ROOT — sets GSTACK_RENDER_HOST_LIST ("all", or +# claude followed by the recorded hosts) and GSTACK_RENDER_HOST_REASON. +gstack_render_hosts_select() { + local root="$1" file h + file="$(gstack_render_hosts_file "$root")" + GSTACK_RENDER_HOST_LIST=all + case "${GSTACK_RENDER_HOSTS:-}" in + all) GSTACK_RENDER_HOST_REASON="GSTACK_RENDER_HOSTS=all"; return 0 ;; + '') ;; + *) echo "warning: GSTACK_RENDER_HOSTS accepts only 'all' (got '${GSTACK_RENDER_HOSTS}'); ignoring it" >&2 ;; + esac + if [ ! -f "$file" ]; then + GSTACK_RENDER_HOST_REASON="no installed-hosts record, so this is a development checkout" + return 0 + fi + GSTACK_RENDER_HOST_LIST=claude + for h in $(gstack_render_hosts_read "$root"); do + [ "$h" = claude ] || GSTACK_RENDER_HOST_LIST="$GSTACK_RENDER_HOST_LIST $h" + done + GSTACK_RENDER_HOST_REASON="the hosts installed from this checkout ($file); GSTACK_RENDER_HOSTS=all renders every host" +} + +# _gstack_hr_link_abs LINK — absolute target of LINK, its directory part +# canonicalized (same rule as bin/gstack-relink's _link_target_abs). +_gstack_hr_link_abs() { + local dest d b d_real + dest="$(readlink "$1" 2>/dev/null || true)" + [ -n "$dest" ] || return 1 + case "$dest" in /*) ;; *) dest="${1%/*}/$dest" ;; esac + d="${dest%/*}"; b="${dest##*/}" + if d_real="$(cd "$d" 2>/dev/null && pwd -P)"; then printf '%s/%s\n' "$d_real" "$b"; else printf '%s\n' "$dest"; fi +} + +# gstack_render_hosts_seed ROOT — record the hosts this checkout already +# serves, for installs made before the record existed: registry rows whose +# source is ROOT, known install roots that resolve to ROOT (a bin/ link or the +# .source-path a Windows copy root carries), host skills entries that link +# into ROOT's render of that host, and gstack skill copies in a host skills dir +# that has no runtime root at all. Needs bin/gstack-install-registry.sh. +gstack_render_hosts_seed() { + local root="$1" real host dest iroot src pair dir e target found="" + real="$(cd "$root" 2>/dev/null && pwd -P)" || return 0 + command -v gstack_install_registry_rows >/dev/null 2>&1 || return 0 + while IFS=' ' read -r host _ _ _ _ src _; do + [ -n "$host" ] && [ "$src" = "$real" ] && found="$found $host" + done </dev/null) +EOF + while IFS=' ' read -r host _ dest iroot; do + [ -n "$host" ] || continue + if { [ -e "$iroot" ] || [ -L "$iroot" ]; }; then + src="$(_gstack_install_source "$iroot")" + [ -z "$src" ] && [ -f "$iroot/.source-path" ] && src="$(head -1 "$iroot/.source-path" 2>/dev/null)" + if [ -n "$src" ] && { [ "$src" = "$real" ] || [ "$(cd "$src" 2>/dev/null && pwd -P)" = "$real" ]; }; then + found="$found $host"; continue + fi + fi + dir="" + for pair in $GSTACK_HOST_RENDER_DIRS; do [ "${pair%%:*}" = "$host" ] && dir="${pair#*:}"; done + [ -n "$dir" ] && [ -d "$dest" ] || continue + for e in "$dest"/gstack*; do + if [ -L "$e" ]; then + target="$(_gstack_hr_link_abs "$e")" || continue + case "$target" in "$real/$dir"/*|"$root/$dir"/*) found="$found $host"; break ;; esac + elif [ ! -e "$iroot" ] && [ -d "$e" ] && { [ -f "$e/.gstack-owned" ] || grep -q '