diff --git a/bin/gstack-config b/bin/gstack-config index c92befed8..b8adf9c25 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -17,6 +17,21 @@ set -euo pipefail STATE_DIR="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}}" CONFIG_FILE="$STATE_DIR/config.yaml" +# Swap a freshly-rendered tmp dir into the live render location (#2569 +# hardening). Installed skills SYMLINK into the live dir, so it is only ever +# replaced AFTER a successful render — a failed render leaves the previous +# render (and every link into it) fully intact. Keep in sync with setup's +# _swap_in_render (same contract, both pinned by +# test/user-render-out-dir-install.test.ts). +_swap_in_render() { + local render_dir="$1" render_tmp="$2" + local render_old="$render_dir.old.$$" + rm -rf "$render_old" + if [ -e "$render_dir" ] || [ -L "$render_dir" ]; then mv "$render_dir" "$render_old"; fi + mv "$render_tmp" "$render_dir" + rm -rf "$render_old" +} + # Annotated header for new config files. Written once on first `set`. # Default semantics: DEFAULTS table below is the canonical source. Header text # is documentation that must stay in sync with DEFAULTS. @@ -462,15 +477,26 @@ case "${1:-}" in elif ! command -v bun >/dev/null 2>&1; then echo "Skip: bun not on PATH — can't render. Install bun, then re-run 'gstack-config gbrain-refresh'." else - rm -rf "$RENDER_DIR" - if ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_DIR" >/dev/null 2>&1 ); then + # Render into a tmp dir and swap it in only on SUCCESS. Installed + # skills SYMLINK into $RENDER_DIR (gstack-relink prefers it), so + # wiping it before the render meant one transient failure (bun + # error, disk full, broken template) left every brain-aware + # SKILL.md link dangling — the whole skill set vanished from + # Claude Code until a successful re-render. A failed render now + # leaves the previous render fully intact. + RENDER_TMP="$RENDER_DIR.tmp.$$" + rm -rf "$RENDER_TMP" + if ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_TMP" >/dev/null 2>&1 ); then + _swap_in_render "$RENDER_DIR" "$RENDER_TMP" # Repoint installed skills at the render — gstack-relink prefers # the render dir when present. "$INSTALL_DIR/bin/gstack-relink" >/dev/null 2>&1 || true echo "Rendered brain-aware blocks into $RENDER_DIR — now live across all your projects' Claude sessions." echo "The install checkout stays clean: upgrades no longer stash generated render dirt (#2569)." else - echo "Warning: render failed. Run 'cd $INSTALL_DIR && bun run gen:skill-docs:user --host claude --out-dir $RENDER_DIR' manually to see the error." + rm -rf "$RENDER_TMP" + echo "Warning: render failed — previous render (if any) left in place, links stay valid." + echo "Run 'cd $INSTALL_DIR && bun run gen:skill-docs:user --host claude --out-dir $RENDER_DIR' manually to see the error." fi fi ;; diff --git a/setup b/setup index 4301ce80a..a17e5c22c 100755 --- a/setup +++ b/setup @@ -136,6 +136,21 @@ _sidecar_root_user_owned() { ! grep -q '