vibe-upgrade — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited vibe-upgrade (Agent Skill) and scored it 83/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 4 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 4 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use when asked to "upgrade", "update vibestack", "get the latest skills", or after vibe-update-check reports a newer version is available (a preamble that sees UPDATE: in the update-check output runs the Inline upgrade flow below; a direct /vibe-upgrade runs Standalone usage).
Claude Code ships a built-in /upgrade, so this skill stays namespaced as /vibe-upgrade. Binaries live in ~/.vibestack/bin/ (vibe-config, vibe-update-check) and mutable state lives under ~/.vibestack/.
This section is referenced by skill preambles when they detect an available update (UPDATE: line from vibe-update-check).
First, check whether auto-upgrade is enabled:
_AUTO=""
[ "${VIBESTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
[ -z "$_AUTO" ] && _AUTO=$("${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" get auto_upgrade 2>/dev/null || true)
echo "AUTO_UPGRADE=$_AUTO"If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`: Skip the question. Log "Auto-upgrading vibestack v{old} → v{new}..." and proceed directly to Step 2. The upgrade blocks in Step 4 roll back automatically if ./install fails during an auto-upgrade. Report what the block actually printed: on RESTORED warn "Auto-upgrade failed — restored the previous version. Run /vibe-upgrade manually to retry."; on RESTORE_FAILED say the previous version could not be restored and relay the recovery line it printed. Never claim a restore the block did not report.
Otherwise, use AskUserQuestion:
If "Yes, upgrade now": Proceed to Step 2.
If "Always keep me up to date":
"${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" set auto_upgrade trueTell the user: "Auto-upgrade enabled. Future updates install automatically." Then proceed to Step 2.
If "Not now": Write a snooze marker with escalating backoff (first snooze = 24h, second = 48h, third+ = 1 week), then continue with the skill the user originally invoked. Do not mention the upgrade again this session.
_SNOOZE_FILE="${VIBESTACK_HOME:-$HOME/.vibestack}/update-snoozed"
_REMOTE_VER="{new}"
_CUR_LEVEL=0
if [ -f "$_SNOOZE_FILE" ]; then
_SNOOZED_VER=$(awk '{print $1}' "$_SNOOZE_FILE")
if [ "$_SNOOZED_VER" = "$_REMOTE_VER" ]; then
_CUR_LEVEL=$(awk '{print $2}' "$_SNOOZE_FILE")
case "$_CUR_LEVEL" in *[!0-9]*) _CUR_LEVEL=0 ;; esac
fi
fi
_NEW_LEVEL=$((_CUR_LEVEL + 1))
[ "$_NEW_LEVEL" -gt 3 ] && _NEW_LEVEL=3
mkdir -p "${VIBESTACK_HOME:-$HOME/.vibestack}"
echo "$_REMOTE_VER $_NEW_LEVEL $(date +%s)" > "$_SNOOZE_FILE"Note: substitute {new} (the remote version from the update-check result) for _REMOTE_VER. Tell the user the snooze duration ("Next reminder in 24h", or 48h, or 1 week, matching the level). Tip: "Enable automatic upgrades with "${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" set auto_upgrade true."
If "Never ask again":
"${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" set update_check falseTell the user: "Update checks disabled. Re-enable with "${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" set update_check true." Continue with the current skill.
Each installed skill's bin is a symlink back into the cloned repo (<checkout>/skills/<name>/bin); resolve one to find the primary checkout. Fall back to common clone paths. Then note whether it's a git checkout (normal) or a plain directory.
REPO=""
for _l in "$HOME"/.claude/skills/*/bin "$HOME"/.cursor/skills/*/bin "$HOME"/.kiro/skills/*/bin "$HOME"/.agents/skills/*/bin "${COPILOT_HOME:-$HOME/.copilot}"/skills/*/bin; do
[ -L "$_l" ] || continue
_T="$(readlink "$_l")"; _C="${_T%/skills/*/bin}"
[ "$_C" != "$_T" ] && [ -f "$_C/install" ] && [ -f "$_C/VERSION" ] && REPO="$_C" && break
done
if [ -z "$REPO" ] || [ ! -f "$REPO/install" ]; then
for d in "$HOME/.claude/skills/vibestack" "$HOME/data/vibestack" "$HOME/vibestack" "$HOME/code/vibestack"; do
[ -f "$d/install" ] && [ -f "$d/VERSION" ] && REPO="$d" && break
done
fi
if [ -z "$REPO" ] || [ ! -f "$REPO/install" ]; then
echo "REPO_NOT_FOUND"
elif [ -d "$REPO/.git" ]; then
echo "REPO=$REPO INSTALL_TYPE=global-git"
else
echo "REPO=$REPO INSTALL_TYPE=vendored"
fiIf REPO_NOT_FOUND: check for a project-local vendored copy (Step 2.5). If there is none either, ask the user where they cloned vibestack, then continue with that path. The REPO and INSTALL_TYPE printed above are used in all later steps.
A team can commit a vibestack checkout into a project so teammates get it without a global install. Detect one distinct from the primary REPO, and read team mode. Replace <REPO> with the REPO Step 2 printed (an empty string on REPO_NOT_FOUND):
REPO='<REPO>'
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
LOCAL_VIBESTACK=""
if [ -n "$_ROOT" ]; then
for cand in "$_ROOT/.vibestack" "$_ROOT/vendor/vibestack" "$_ROOT/tools/vibestack"; do
if [ -f "$cand/install" ] && [ -f "$cand/VERSION" ]; then
_RESOLVED_LOCAL=$(cd "$cand" && pwd -P)
_RESOLVED_PRIMARY=$([ -n "$REPO" ] && cd "$REPO" 2>/dev/null && pwd -P || echo "")
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_VIBESTACK="$cand"; break
fi
fi
done
fi
_TEAM_MODE=$("${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" get team_mode 2>/dev/null || echo "false")
echo "_ROOT=$_ROOT"
echo "LOCAL_VIBESTACK=$LOCAL_VIBESTACK"
echo "TEAM_MODE=$_TEAM_MODE"If REPO_NOT_FOUND but LOCAL_VIBESTACK is non-empty, treat LOCAL_VIBESTACK as REPO for the upgrade — the vendored copy is the only install here.
REPO='<REPO>'
OLD_VERSION=$(cat "$REPO/VERSION" 2>/dev/null || echo "unknown")
echo "OLD_VERSION=$OLD_VERSION"Carry OLD_VERSION into the later steps (substitute the printed value).
Every bash block runs in a fresh shell, so nothing set in one block survives into the next. Each block from here on opens with placeholder assignments such as REPO='<REPO>': replace every <REPO>, <REPLAY>, <OLD_VERSION>, <LOCAL_VIBESTACK> and <_ROOT> with the value the earlier step printed on its NAME= line. The mutating blocks check them first and print NOT_A_VIBESTACK_INSTALL (or REPLAY_UNKNOWN) instead of running git, mv or rm against an empty or wrong path.
The upgrade re-runs ./install exactly as it was originally run — the same runtimes, the same scope, config only where config was deployed. It never uses --yes, which means "all five runtimes" and would widen a Claude-only install into Cursor, Kiro, Codex and Copilot CLI. The choices are read from what the install left behind: the .vibestack-manifest in each skills root, and the config manifests under ~/.local/state/vibekit/. Substitute REPO (Step 2) and _ROOT (Step 2.5). The result is a small replay script, printed so the user can see it. The optional plugin flags (--deliberation, --caveman, --ponytail) leave no manifest, so they are not replayed; plugins already installed stay in place. Tell the user to re-run ./install --only=config with those flags if they want them refreshed too.
REPO='<REPO>'
_ROOT='<_ROOT>'
_VH="${VIBESTACK_HOME:-$HOME/.vibestack}"
REPLAY="$_VH/upgrade-replay.sh"
_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/vibekit"
_USER_T=""; _CFG_T=""; _PROJ_T=""
for t in claude cursor kiro codex copilot; do
case "$t" in
claude) r="$HOME/.claude/skills"; rel=".claude/skills" ;;
cursor) r="$HOME/.cursor/skills"; rel=".cursor/skills" ;;
kiro) r="$HOME/.kiro/skills"; rel=".kiro/skills" ;;
codex) r="$HOME/.agents/skills"; rel=".agents/skills" ;;
copilot) r="${COPILOT_HOME:-$HOME/.copilot}/skills"; rel=".github/skills" ;;
esac
if [ -f "$r/.vibestack-manifest" ] || [ -f "$r/vibe-upgrade/SKILL.md" ] \
|| { [ "$t" = codex ] && [ -f "${CODEX_HOME:-$HOME/.codex}/skills/vibe-upgrade/SKILL.md" ]; }; then
_USER_T="$_USER_T,$t"
fi
[ -f "$_STATE/manifest_$t" ] && _CFG_T="$_CFG_T,$t"
# A project-scope install in the current repo that links back into this checkout.
if [ -n "${_ROOT:-}" ] && [ -f "$_ROOT/$rel/.vibestack-manifest" ] \
&& [ "$(cd "$_ROOT/$rel" && pwd -P)" != "$(cd "$r" 2>/dev/null && pwd -P)" ]; then
for l in "$_ROOT/$rel"/*/bin; do
[ -L "$l" ] || continue
case "$(readlink "$l")" in "$REPO"/skills/*) _PROJ_T="$_PROJ_T,$t"; break ;; esac
done
fi
done
_USER_T="${_USER_T#,}"; _CFG_T="${_CFG_T#,}"; _PROJ_T="${_PROJ_T#,}"
if [ -z "$_USER_T$_PROJ_T$_CFG_T" ]; then
echo "REPLAY_UNKNOWN — no install manifest found; cannot tell which runtimes to refresh"
exit 1
fi
# RTK follows what the first install did, not what is on PATH: the config phase
# leaves its hook in Claude's settings, and --no-rtk leaves none.
_RTK=" --no-rtk"; grep -q 'rtk' "$HOME/.claude/settings.json" 2>/dev/null && _RTK=""
mkdir -p "$_VH" || { echo "REPLAY_UNWRITABLE $_VH"; exit 1; }
{
echo '#!/usr/bin/env bash'
echo '# Re-runs the original ./install choices from the checkout given as $1.'
echo 'cd "$1" || exit 1'
if [ -n "$_USER_T" ]; then echo "./install --only=skills --target=$_USER_T || exit 1"; fi
if [ -n "$_PROJ_T" ]; then
printf './install --only=skills --scope=project --project-root=%q --target=%s || exit 1\n' "$_ROOT" "$_PROJ_T"
fi
if [ -n "$_CFG_T" ]; then echo "./install --only=config --target=$_CFG_T$_RTK || exit 1"; fi
} > "$REPLAY" || { echo "REPLAY_UNWRITABLE $REPLAY"; exit 1; }
echo "REPLAY=$REPLAY"
sed -n '4,$p' "$REPLAY"On REPLAY_UNKNOWN: nothing has changed yet. In an auto-upgrade, stop (see the stop rule below). Interactively, ask the user which runtimes vibestack is installed into and whether they used --with-config, then write $REPLAY with those ./install --only=... --target=... lines and continue. Project-scope installs in other repos are not visible from here — mention that they can be refreshed by running ./install --scope=project ... in each one.
Every block in Steps 3.5 through 4.5 prints exactly one outcome token. Only these mean success: REPLAY= (Step 3.5), INSTALL_OK, VENDORED_UPGRADE_OK, SYNC_OK, VENDORED_REMOVED (Step 4.5). Any other token — including REPLAY_UNKNOWN, REPLAY_UNWRITABLE, FETCH_FAILED, PULL_BLOCKED, INSTALL_FAILED, RESTORED, RESTORE_FAILED, NOT_A_VIBESTACK_INSTALL, STALE_BACKUP, MKTEMP_FAILED, CLONE_FAILED, SWAP_FAILED, SYNC_FAILED, SYNC_SKIPPED — or a block that printed no token, means stop:
and never print the "upgraded!" banner.
recovery command) to the user verbatim.
on the version that is actually installed.
Step 4.5 is the partial case: the primary already upgraded, so a SYNC_FAILED, SYNC_SKIPPED or SKIP there does not stop Steps 4.75–6 — say the vendored copy was not refreshed or removed.
Use REPO and INSTALL_TYPE from Step 2 and REPLAY from Step 3.5.
For a git checkout (`global-git`): fast-forward pull, then replay the install. Never force-push and never hard-reset — a blocked pull is reported, not forced, and the checkout is often a working clone with edits in it. If the install fails during an auto-upgrade, move back to the pre-pull commit with git reset --keep (which refuses rather than discard uncommitted changes) and replay the install of the known-good version. Interactively, stop with the recorded commit instead and let the user decide.
REPO='<REPO>'
REPLAY='<REPLAY>'
case "${REPO:-}" in /?*) : ;; *) echo "NOT_A_VIBESTACK_INSTALL — REPO is empty or relative; set it to the path from Step 2"; exit 1 ;; esac
[ -f "$REPO/install" ] && [ -f "$REPO/VERSION" ] && [ -d "$REPO/skills" ] \
|| { echo "NOT_A_VIBESTACK_INSTALL $REPO"; exit 1; }
[ -n "${REPLAY:-}" ] && [ -f "$REPLAY" ] || { echo "REPLAY_UNKNOWN — REPLAY is unset or missing; re-run Step 3.5"; exit 1; }
_TOP="$(git -C "$REPO" rev-parse --show-toplevel 2>/dev/null || true)"
if [ -z "$_TOP" ] || [ "$(cd "$_TOP" && pwd -P)" != "$(cd "$REPO" && pwd -P)" ]; then
echo "NOT_A_VIBESTACK_INSTALL $REPO is not the top of its own git checkout"; exit 1
fi
cd "$REPO" || exit 1
_AUTO=""
[ "${VIBESTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
[ -z "$_AUTO" ] && _AUTO=$("${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-config" get auto_upgrade 2>/dev/null || true)
PREV_SHA="$(git rev-parse HEAD 2>/dev/null || true)"
BR="$(git symbolic-ref --short HEAD 2>/dev/null || echo main)"
git fetch --quiet origin || { echo "FETCH_FAILED — could not fetch origin in $REPO; nothing changed"; exit 1; }
if ! git pull --quiet --ff-only origin "$BR"; then
echo "PULL_BLOCKED — local changes or non-fast-forward. Stash/commit first, then re-run /vibe-upgrade. Not forcing."
exit 1
fi
if bash "$REPLAY" "$REPO"; then
echo "INSTALL_OK $(cat VERSION 2>/dev/null || echo unknown)"
exit 0
fi
if { [ "$_AUTO" = "true" ] || [ "$_AUTO" = "1" ]; } && [ -n "$PREV_SHA" ]; then
if ! git reset --quiet --keep "$PREV_SHA"; then
echo "RESTORE_FAILED — could not move $REPO back to $PREV_SHA without touching local changes."
echo " Recover by hand: run git -C \"$REPO\" status, commit or stash the files it lists,"
echo " then: git -C \"$REPO\" reset --keep $PREV_SHA && bash \"$REPLAY\" \"$REPO\""
exit 1
fi
if bash "$REPLAY" "$REPO"; then
echo "RESTORED $PREV_SHA — the new version failed to install; the previous version is installed again"
else
echo "RESTORE_FAILED — checkout is back at $PREV_SHA, but re-installing it also failed."
echo " Recover by hand: bash \"$REPLAY\" \"$REPO\""
fi
exit 1
fi
echo "INSTALL_FAILED — the checkout is now at $(git rev-parse --short HEAD 2>/dev/null); the previous commit was $PREV_SHA."
echo " To go back: git -C \"$REPO\" reset --keep $PREV_SHA && bash \"$REPLAY\" \"$REPO\""
exit 1For a plain vendored primary (`vendored`, no `.git`): clone a fresh copy next to it, swap it in with a backup, replay the install, and put the backup back on failure. Each move is checked, and the live install is never deleted — on a failure it is either still in place or still at $REPO.bak.
REPO='<REPO>'
REPLAY='<REPLAY>'
case "${REPO:-}" in /?*) : ;; *) echo "NOT_A_VIBESTACK_INSTALL — REPO is empty or relative; set it to the path from Step 2"; exit 1 ;; esac
[ -f "$REPO/install" ] && [ -f "$REPO/VERSION" ] && [ -d "$REPO/skills" ] || { echo "NOT_A_VIBESTACK_INSTALL $REPO"; exit 1; }
[ -n "${REPLAY:-}" ] && [ -f "$REPLAY" ] || { echo "REPLAY_UNKNOWN — REPLAY is unset or missing; re-run Step 3.5"; exit 1; }
if [ -e "$REPO.bak" ] || [ -L "$REPO.bak" ]; then
echo "STALE_BACKUP — $REPO.bak is left over from an interrupted upgrade. Check which copy you want, move or delete $REPO.bak, then re-run /vibe-upgrade."
exit 1
fi
TMP_DIR="$(mktemp -d "$(dirname "$REPO")/.vibestack-upgrade.XXXXXX")" && [ -d "$TMP_DIR" ] \
|| { echo "MKTEMP_FAILED — could not create a staging directory next to $REPO; nothing changed"; exit 1; }
if ! git clone --quiet --depth 1 https://github.com/timurgaleev/vibestack.git "$TMP_DIR/vibestack" \
|| [ ! -f "$TMP_DIR/vibestack/install" ]; then
rm -rf "$TMP_DIR"; echo "CLONE_FAILED — nothing changed"; exit 1
fi
if ! mv "$REPO" "$REPO.bak"; then
rm -rf "$TMP_DIR"; echo "SWAP_FAILED — could not move $REPO aside; nothing changed"; exit 1
fi
if [ -e "$REPO" ] || ! mv "$TMP_DIR/vibestack" "$REPO"; then
if [ ! -e "$REPO" ] && mv "$REPO.bak" "$REPO"; then
rm -rf "$TMP_DIR"; echo "SWAP_FAILED — could not move the new copy in; previous install is back at $REPO"
else
echo "SWAP_FAILED — RESTORE_FAILED: the previous install is kept at $REPO.bak and the new copy at $TMP_DIR. Move $REPO.bak back to $REPO by hand."
fi
exit 1
fi
if bash "$REPLAY" "$REPO"; then
rm -rf "$REPO.bak" "$TMP_DIR"
echo "VENDORED_UPGRADE_OK $(cat "$REPO/VERSION" 2>/dev/null || echo unknown)"
exit 0
fi
# The new copy failed to install: park it, put the backup back, re-install it.
if mv "$REPO" "$TMP_DIR/failed" && mv "$REPO.bak" "$REPO"; then
rm -rf "$TMP_DIR"
if bash "$REPLAY" "$REPO"; then
echo "RESTORED — the new version failed to install; the previous version is installed again"
else
echo "RESTORE_FAILED — the previous copy is back at $REPO, but re-installing it failed. Recover: bash \"$REPLAY\" \"$REPO\""
fi
else
echo "RESTORE_FAILED — the previous install is kept at $REPO.bak (the failed new copy is at $REPO or $TMP_DIR/failed). Move $REPO.bak back to $REPO by hand."
fi
exit 1Only when Step 2.5 found LOCAL_VIBESTACK separate from the freshly-upgraded REPO. Behavior depends on TEAM_MODE.
If `LOCAL_VIBESTACK` is non-empty AND `TEAM_MODE` is `true`: remove the vendored copy — team mode uses the global install as the single source of truth.
_ROOT='<_ROOT>'
LOCAL_VIBESTACK='<LOCAL_VIBESTACK>'
# Guard: refuse to run without a concrete vendored-copy path (never rm an empty
# or root path). Both must be set and LOCAL_VIBESTACK must live under _ROOT.
case "${_ROOT:-}" in /?*) : ;; *) echo "SKIP — _ROOT is empty or relative; set it to the repo root from Step 2.5"; exit 0 ;; esac
case "${LOCAL_VIBESTACK:-}" in "$_ROOT"/?*) : ;; *) echo "SKIP — no valid vendored copy under repo root"; exit 0 ;; esac
[ -f "$LOCAL_VIBESTACK/install" ] && [ -f "$LOCAL_VIBESTACK/VERSION" ] \
|| { echo "SKIP — $LOCAL_VIBESTACK is not a vibestack copy"; exit 0; }
cd "$_ROOT" || { echo "SYNC_FAILED — cannot enter $_ROOT"; exit 1; }
_REL="${LOCAL_VIBESTACK#$_ROOT/}"
git rm -r --cached "$_REL" 2>/dev/null || true
if ! grep -qF "$_REL/" .gitignore 2>/dev/null; then
echo "$_REL/" >> .gitignore
fi
rm -rf "$LOCAL_VIBESTACK" || { echo "SYNC_FAILED — could not remove $LOCAL_VIBESTACK"; exit 1; }
echo "VENDORED_REMOVED $LOCAL_VIBESTACK"On VENDORED_REMOVED tell the user: "Removed vendored copy at $LOCAL_VIBESTACK (team mode active — the global install is the source of truth). Commit the .gitignore change when ready."
If `LOCAL_VIBESTACK` is non-empty AND `TEAM_MODE` is NOT `true`: refresh the vendored copy's files from the freshly-upgraded primary (with a backup restore on failure). The global install is already up to date, so this only re-stages the committed copy — teammates run ./install from it themselves. The new copy is staged completely before the live one is moved, and every move is checked.
REPO='<REPO>'
LOCAL_VIBESTACK='<LOCAL_VIBESTACK>'
case "${LOCAL_VIBESTACK:-}" in /?*) : ;; *) echo "SYNC_SKIPPED — no valid vendored copy path"; exit 1 ;; esac
case "${REPO:-}" in /?*) : ;; *) echo "SYNC_SKIPPED — REPO is empty or relative; set it to the path from Step 2"; exit 1 ;; esac
[ -f "$REPO/install" ] && [ -f "$REPO/VERSION" ] && [ -d "$REPO/skills" ] \
|| { echo "SYNC_SKIPPED — $REPO is not a vibestack install"; exit 1; }
[ -f "$LOCAL_VIBESTACK/install" ] && [ -f "$LOCAL_VIBESTACK/VERSION" ] \
|| { echo "SYNC_SKIPPED — $LOCAL_VIBESTACK is not a vibestack copy"; exit 1; }
if [ -e "$LOCAL_VIBESTACK.bak" ] || [ -L "$LOCAL_VIBESTACK.bak" ]; then
echo "SYNC_SKIPPED — $LOCAL_VIBESTACK.bak is left over from an interrupted sync; move or delete it, then re-run /vibe-upgrade"
exit 1
fi
_STAGE="$(mktemp -d "$(dirname "$LOCAL_VIBESTACK")/.vibestack-sync.XXXXXX")" && [ -d "$_STAGE" ] \
|| { echo "SYNC_FAILED — could not create a staging directory; vendored copy unchanged"; exit 1; }
if ! { mkdir "$_STAGE/copy" && cp -R "$REPO/." "$_STAGE/copy/" && rm -rf "$_STAGE/copy/.git"; }; then
rm -rf "$_STAGE"; echo "SYNC_FAILED — could not stage the new copy; vendored copy unchanged"; exit 1
fi
if ! mv "$LOCAL_VIBESTACK" "$LOCAL_VIBESTACK.bak"; then
rm -rf "$_STAGE"; echo "SYNC_FAILED — could not move the vendored copy aside; unchanged"; exit 1
fi
if [ ! -e "$LOCAL_VIBESTACK" ] && mv "$_STAGE/copy" "$LOCAL_VIBESTACK"; then
rm -rf "$LOCAL_VIBESTACK.bak" "$_STAGE"
echo "SYNC_OK"
elif [ ! -e "$LOCAL_VIBESTACK" ] && mv "$LOCAL_VIBESTACK.bak" "$LOCAL_VIBESTACK"; then
rm -rf "$_STAGE"; echo "SYNC_FAILED — restored the previous version at $LOCAL_VIBESTACK"; exit 1
else
echo "SYNC_FAILED — the previous vendored copy is kept at $LOCAL_VIBESTACK.bak; move it back by hand"; exit 1
fiOn SYNC_OK tell the user: "Also refreshed the vendored copy at $LOCAL_VIBESTACK — commit it when you're ready." On SYNC_FAILED or SYNC_SKIPPED relay the printed line and say the vendored copy was not refreshed; run /vibe-upgrade manually to retry once it is resolved.
Only after Step 4 printed INSTALL_OK or VENDORED_UPGRADE_OK (see the stop rule), run any migration scripts for versions between the old and new version. Migrations handle state fixes ./install alone can't cover (stale config, orphaned files, directory-structure changes). Substitute OLD_VERSION and REPO from earlier steps. Each migration gets the install directory as VIBESTACK_INSTALL_DIR, so it works for a project-local copy as well as the global checkout.
REPO='<REPO>'
OLD_VERSION='<OLD_VERSION>'
case "${REPO:-}" in /?*) : ;; *) echo "MIGRATIONS_SKIPPED — REPO is empty or relative; set it to the path from Step 2"; exit 1 ;; esac
case "${OLD_VERSION:-}" in ''|'<'*) echo "MIGRATIONS_SKIPPED — OLD_VERSION is unset; set it to the value printed by Step 3"; exit 1 ;; esac
MIGRATIONS_DIR="$REPO/skills/vibe-upgrade/migrations"
if [ -d "$MIGRATIONS_DIR" ]; then
for migration in $(find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V); do
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
if [ "$OLD_VERSION" != "unknown" ] && [ "$(printf '%s\n%s' "$OLD_VERSION" "$m_ver" | sort -V | head -1)" = "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$m_ver" ]; then
echo "Running migration $m_ver..."
VIBESTACK_INSTALL_DIR="$REPO" bash "$migration" || echo " Warning: migration $m_ver had errors (non-fatal)"
fi
done
fiMigrations are idempotent bash scripts in skills/vibe-upgrade/migrations/, each named v{VERSION}.sh, run only when upgrading from an older version. See skills/vibe-upgrade/migrations/README.md for how to add one.
Only after a successful Step 4. The marker drives the next session's what's-new message, so writing it after a failed or rolled-back upgrade would announce a version that is not installed.
OLD_VERSION='<OLD_VERSION>'
mkdir -p "${VIBESTACK_HOME:-$HOME/.vibestack}"
echo "$OLD_VERSION" > "${VIBESTACK_HOME:-$HOME/.vibestack}/just-upgraded-from"
rm -f "${VIBESTACK_HOME:-$HOME/.vibestack}/.update-check-stamp" "${VIBESTACK_HOME:-$HOME/.vibestack}/.update-check-failed"
rm -f "${VIBESTACK_HOME:-$HOME/.vibestack}/update-snoozed"Read $REPO/CHANGELOG.md. Find every version entry between the old and new version and summarize as 5–7 bullets grouped by theme. Focus on user-facing changes; skip internal refactors unless significant.
vibestack v{new} — upgraded from v{old}!
What's new:
- [bullet 1]
- [bullet 2]
- ...
Happy shipping!Note that a new agent session may be needed if the host doesn't hot-reload skills.
After showing what's new, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
When invoked directly as /vibe-upgrade (not from a preamble), the user has already opted in — skip the Step 1 question and go straight to the upgrade.
update_check gate):
"${VIBESTACK_HOME:-$HOME/.vibestack}/bin/vibe-update-check" --force 2>/dev/null || trueAn UPDATE: vibestack <new> is available (you have <old>) line means an upgrade is available; no output means the primary is already current. A CHECK_FAILED <reason> line means the update status is unknown — the remote could not be read. Never report "already on the latest version" in that case. Tell the user the check failed and why (the line names the remote and the checkout), then use AskUserQuestion: "Try the upgrade anyway?" with options ["Yes, run the upgrade", "No, stop here"]. On yes, continue as in point 2 — pulling an already-current checkout is harmless. On no, stop.
then follow Steps 3–6 (skip Step 1 — the invocation is the consent), obeying the stop rule.
stale project-local vendored copy.
on the latest version (v{version})."
team-mode removal block in Step 4.5. Tell the user: "Global v{version} is up to date. Removed the stale vendored copy (team mode active). Commit the .gitignore change when ready."
versions:
REPO='<REPO>'
LOCAL_VIBESTACK='<LOCAL_VIBESTACK>'
PRIMARY_VER=$(cat "$REPO/VERSION" 2>/dev/null || echo "unknown")
LOCAL_VER=$(cat "$LOCAL_VIBESTACK/VERSION" 2>/dev/null || echo "unknown")
echo "PRIMARY=$PRIMARY_VER LOCAL=$LOCAL_VER""Global v{PRIMARY_VER} is up to date. Refreshed the local vendored copy from v{LOCAL_VER} → v{PRIMARY_VER}. Commit it when you're ready."
(v{PRIMARY_VER}). Global and vendored copy are both up to date."
Never force-push, and never hard-reset. A blocked pull is reported, not overridden. The only reset is the scoped auto-upgrade rollback in Step 4 — a git reset --keep to the commit you were just on, which refuses rather than discard local changes. Never re-install with --yes; always replay the original install from Step 3.5.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.