mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(restamp): gate-ahead keeps the lane CHANGELOG entry when both PRs took the same tentative stamp (GBRA-102 diagnosis fixture) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit43c75a6bb6) * test(tree-receipt): a real gbrain stamp pair is same-modulo-stamps under gbrain committed policy (W1 acceptance) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit6cf53f9fed) * feat(ship-policy): gate_command, full_e2e and min_gstack_version; unknown keys stay fail-closed; validate prints introducing versions Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit878a7123d7) * feat(ship-queue): gstack-ship-queue ready, stamp, verify and gate-ahead (WIP, tests next) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit409a5957ad) * test(ship-queue): stamp journal and resume, verify from a separate clone, gate-ahead attempts, ready Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitade554087b) * docs(ship-policy): Run a shared merge queue; new keys and compatibility rule; troubleshooting anchors; /ship pointer Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7fc73e8892) * fix(ship): shared-queue pointer rides the show-effective ship: line (ship skeleton has 2 bytes of parity headroom) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit5b33d4a015) * test(windows): curate the shared-queue tests out of the Windows subset (shebang spawns) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit8deee14728) * test(fixtures): register the gbrain stamp-pair fixture as free-only Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit563a07aa03) * test(ship-queue): receipt digest contract names lib/ship-queue.ts for its pregate lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit501637c258) * feat(pregate): timing check flags elapsed-time bounds and sleep-then-assert in added test lines (W4) lib/pregate/checks/timing.ts prints TIMING_ASSERTION: <file>:<line> <pattern> and a TIMING_ASSERTIONS summary; gstack-timing-ok: <reason> in any comment syntax on the line or the line above escapes one. It warns (PREGATE_TIMING_ASSERTION): measured on gstack (9 of 47 test-touching commits, 263 hits in the tree) and gbrain (37 of 95 commits; 31 of 38 sites of the five timing-flake fixes), blocking would stop a fifth to two fifths of test changes. Adds the /review checklist rule and the /investigate timing-flake playbook. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita3b86b10b2) * feat(update-check): an equal VERSION with a moved main is an upgrade, via the compare API (W12) When VERSION matches but ls-remote's main SHA differs from this checkout's HEAD, bin/gstack-update-check asks the GitHub compare API (status, ahead_by) through the existing receipted curl and prints UPGRADE_AVAILABLE <ver> <ver>+<n> <sha>; a diverged or unreadable compare is CHECK_FAILED (unknown), and behind/identical is up to date. Never fetches. The cache gains an IDENTITY line (head, compared remote SHA, upstream): an outcome replays only for the same HEAD and upstream, a same-remote refresh reuses it without another compare, and a git checkout ignores a legacy one-line cache (non-git installs keep the one-line format). The snooze is keyed by the SHA. gstack-upgrade's question, snooze note and What's New step handle the four-field line; gstack-skill-end reports it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit421e66d2a4) * fix(pregate): treat planning and evidence markdown as an empty lane A docs-only diff with no .gstack/pregate.json failed lanes as PREGATE_NO_LANE. Planning and evidence Markdown is now a docs/no-tests empty lane. Unmapped source files still fail closed. Co-authored-by: Nick <surlymo@apache.org> (cherry picked from commite8793e59e8) * fix(pregate): keep test-read markdown on its test lane Unmarked Markdown that no test depends on is a docs/no-tests empty lane. A file a test reads, including a fixture, still selects that test through the import graph. Unmapped source and unreferenced test-tree files still fail closed. Co-authored-by: Nick <surlymo@apache.org> (cherry picked from commit7aabbddf3d) * test(pregate): reviewer edges for the docs/no-tests empty lane (#3215) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2fb3fa8613) * test(pregate): requires-remote on a gbrain-shaped repo (ci:ubicloud, UBI_OWNER) (W10) A base pregate.json with gbrain's dependencies and remote_gate "bun run ci:ubicloud" (a package script whose stub needs UBICLOUD_API_KEY, names VMs from UBI_OWNER and prints PREGATE_REMOTE). Under 8 cores the test tier is one obligation naming the command; --remote passes the caller's UBI_OWNER and key through and clears it on the report; without the key the gate's own error shows and the obligation stays uncleared. #3190's path needed no change. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit4d69ff02df) * test(pregate): the ship receipt counts the timing check (7 checks) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitcb03ef67e7) * test(context-budget): gstack-upgrade eager ceiling for the W12 equal-version upgrade question (ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(approval-classes): gstack-approval-classes and ship-receipt citation enforcement (W2, WIP) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit847c7dae3d) * feat(approval-classes): tests, docs, /review and /ship wiring (W2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit269bd5b8b5) * feat(review): state-machines specialist and gstack-state-matrix round-trip check with a captured #6792 regression (W3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit1a634979c6) * fix(review-army): keep the --simplification force flag pinned; approval test timeouts Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit789d392bce) * test(free-fixtures): register the state-matrix-6792 fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit1bacef296d) * test(state-matrix): cover state-machines NEVER_GATE in gstack-specialist-stats Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit859063d56f) * test(fixtures): register the pregate-timing fixtures as free-only (W4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-queue): full_e2e needs bound gate evidence in ready and verify, CI reads every check-run page and commit status, next: quotes its paths (review #3235 P1-1, P1-5, P2-1) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): timing reads a native diff and never passes an unread one The timing check's git diff honored diff.external, textconv and diff.noprefix, so a configured driver hid every added line and the check passed. Run it with --no-ext-diff --no-textconv and explicit a/ b/ prefixes, and report a git diff that fails or does not finish as incomplete (PREGATE_INCOMPLETE), which a warn downgrade does not soften. Review of #3235, sibling of P1-4 (lib/pregate/checks/timing.ts:189). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): only affirmative APPROVED evidence counts, explicit tokens are authoritative, native diffs fail closed (review #3235 P1-2, P1-3, P1-4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(state-matrix): detect reads the native diff, ignoring diff.external and textconv (review #3235 P1-4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(e2e): review-army-state-matrix behavior case on the #6792 fixture (field list 2, W3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(review-army): tighten the State Machines dispatch line so /ship stays under its parity size ratio with main's #3231 review-army text Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): only top-level approval lines count; quoted, fenced, indented-code and commented text never does (review #3235 r2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval-classes): approval evidence is the first non-blank line only, at column 0 (review #3235 r3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: keep gstack-upgrade under its size budget, exempt state-matrix's fetch hint, fit the periodic plan's 37 slices Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: the scope-signal clarity check follows helper-printed signals; the hermetic update-check fixture writes the identity cache Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor: move the release-path result codes to lib/result-codes-release.ts (module-size ratchet); fit the periodic plan's 38 slices Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave B Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.116.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: copy lib/result-codes-release.ts wherever a fixture copies lib/result-codes.ts; the scope-signal check requires the helper's computed emission Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: state-matrix stats test resolves its slug under its own state root (home containment) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate pregate-docs-lane-review out of the Windows subset (runBin shebang, like the other pregate fixtures) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate pregate-remote-gbrain out of the Windows subset (runBin shebang, like pregate-remote) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Nick <surlymo@apache.org>
421 lines
18 KiB
Bash
Executable File
421 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-update-check — periodic version check for all skills.
|
|
#
|
|
# Output (one line, or nothing):
|
|
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
|
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
|
# UPGRADE_AVAILABLE <ver> <ver>+<n> <sha>
|
|
# — VERSION is equal but main moved <n>
|
|
# commits past this checkout's HEAD (the
|
|
# GitHub compare API says so); the snooze
|
|
# is keyed by <sha>
|
|
# CHECK_FAILED <reason> — the check crashed, or (--force only) the
|
|
# remote version could not be read
|
|
# (nothing) — up to date, snoozed, disabled, or check skipped
|
|
#
|
|
# A remote version that cannot be read is cached as CHECK_FAILED (short TTL),
|
|
# never as UP_TO_DATE (#2786). Skill preambles replay it silently; only
|
|
# --force (/gstack-upgrade) prints it with the failed URL. A diverged or
|
|
# unreadable compare (equal VERSION, different main) is CHECK_FAILED too.
|
|
#
|
|
# Cache: line 1 is the outcome; line 2 is
|
|
# IDENTITY head=<local HEAD|none> remote=<compared main SHA|none> upstream=<url>
|
|
# A cached outcome is replayed only while this checkout's HEAD and upstream
|
|
# match line 2. A git checkout ignores a cache without line 2 (written before
|
|
# the equal-version compare existed); a non-git install has no HEAD, so its
|
|
# cache keeps the version-only meaning. Never fetches: ls-remote and one
|
|
# compare request are the only network reads.
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
# GSTACK_REMOTE_URL — override remote VERSION URL (branch-pinned fallback)
|
|
# GSTACK_REMOTE_REPO — override remote git URL for ls-remote SHA resolution
|
|
# GSTACK_REMOTE_RAW_BASE — override the SHA-pinned raw VERSION base URL
|
|
# GSTACK_COMPARE_API — override the GitHub compare API base URL
|
|
# GSTACK_HOME — relocate the state root (chain: bin/gstack-state-root.sh)
|
|
set -euo pipefail
|
|
|
|
# A crash must not read as "up to date" (#1974). With set -e, any unguarded
|
|
# failure used to exit silently — and silence IS the up-to-date signal, so a
|
|
# broken check was indistinguishable from a current install (observed live as
|
|
# a 45-release silent-staleness incident). -E propagates the trap into
|
|
# functions/subshells; exit 0 keeps callers' `|| true` from eating the line.
|
|
set -E
|
|
trap 'rc=$?; echo "CHECK_FAILED gstack-update-check crashed (line $LINENO, rc=$rc) — update status UNKNOWN, not up-to-date"; exit 0' ERR
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
|
gstack_state_root_select
|
|
STATE_DIR="$_gstack_sr_root"
|
|
|
|
# Egress receipt helpers (_receipted_curl / _receipted_git). Update checks
|
|
# are fail-OPEN: a receipt hiccup warns but never blocks the version check.
|
|
. "$GSTACK_DIR/bin/gstack-egress-lib.sh"
|
|
|
|
# _receipted_version_fetch <url>
|
|
# Receipted bodyless GET for a VERSION file (fail-open). Local reads
|
|
# (file://, or a URL with no network host) never leave the machine — they
|
|
# are not egress, so they get no receipt.
|
|
_receipted_version_fetch() {
|
|
local url="$1" host scheme
|
|
scheme="${url%%://*}"
|
|
host="${url#*://}"; host="${host%%/*}"
|
|
case "$scheme" in
|
|
http|https|git|ssh|ftp|ftps)
|
|
GSTACK_HOME="$STATE_DIR" _receipted_curl open update-check "$host" version-fetch "update_check!=false" --no-payload \
|
|
curl -sf --max-time 5 "$url" || true
|
|
;;
|
|
*)
|
|
curl -sf --max-time 5 "$url" 2>/dev/null || true
|
|
;;
|
|
esac
|
|
}
|
|
CACHE_FILE="$STATE_DIR/last-update-check"
|
|
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
|
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
|
VERSION_FILE="$GSTACK_DIR/VERSION"
|
|
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
|
REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/garrytan/gstack.git}"
|
|
REMOTE_RAW_BASE="${GSTACK_REMOTE_RAW_BASE:-https://raw.githubusercontent.com/garrytan/gstack}"
|
|
COMPARE_API="${GSTACK_COMPARE_API:-https://api.github.com/repos/garrytan/gstack/compare}"
|
|
|
|
# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
|
|
FORCE=0
|
|
if [ "${1:-}" = "--force" ]; then
|
|
FORCE=1
|
|
rm -f "$CACHE_FILE"
|
|
rm -f "$SNOOZE_FILE"
|
|
fi
|
|
|
|
# _display_url <url> — strip userinfo (user:token@) before a URL is printed or cached.
|
|
_display_url() {
|
|
printf '%s' "$1" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/@]*@#\1#'
|
|
}
|
|
|
|
# ─── Cache identity: this checkout's HEAD and the upstream it compares with ──
|
|
LOCAL_HEAD="none"
|
|
if [ -e "$GSTACK_DIR/.git" ]; then
|
|
_H="$(git -C "$GSTACK_DIR" rev-parse --verify --quiet HEAD 2>/dev/null || true)"
|
|
if echo "$_H" | grep -qE '^[0-9a-f]{40}$'; then LOCAL_HEAD="$_H"; fi
|
|
fi
|
|
if [ -n "${GSTACK_REMOTE_URL:-}" ]; then UPSTREAM="$(_display_url "$REMOTE_URL")"; else UPSTREAM="$(_display_url "$REMOTE_REPO")"; fi
|
|
|
|
# _identity_field <name> — a field of the cache's IDENTITY line (empty when absent).
|
|
_identity_field() {
|
|
printf '%s\n' "${CACHED_ID:-}" | tr ' ' '\n' | sed -n "s/^$1=//p" | head -1
|
|
}
|
|
|
|
# _identity_ok — the cached outcome belongs to this HEAD and upstream.
|
|
_identity_ok() {
|
|
if [ -z "${CACHED_ID:-}" ]; then
|
|
[ "$LOCAL_HEAD" = "none" ]
|
|
return
|
|
fi
|
|
[ "$(_identity_field head)" = "$LOCAL_HEAD" ] && [ "$(_identity_field upstream)" = "$UPSTREAM" ]
|
|
}
|
|
|
|
# _write_cache <outcome line> — the outcome plus the identity it was computed for
|
|
# (a non-git install keeps the one-line, version-only cache).
|
|
_write_cache() {
|
|
if [ "$LOCAL_HEAD" = "none" ]; then
|
|
printf '%s\n' "$1" > "$CACHE_FILE"
|
|
return
|
|
fi
|
|
printf '%s\nIDENTITY head=%s remote=%s upstream=%s\n' "$1" "$LOCAL_HEAD" "${_REMOTE_SHA:-none}" "$UPSTREAM" > "$CACHE_FILE"
|
|
}
|
|
|
|
# _snooze_key <UPGRADE_AVAILABLE line> — the SHA for the equal-version form, else the new version.
|
|
_snooze_key() {
|
|
printf '%s\n' "$1" | awk '{ print (NF >= 4 ? $4 : $3) }'
|
|
}
|
|
|
|
# ─── Step 0: Check if updates are disabled ────────────────────
|
|
_UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true)
|
|
if [ "$_UC" = "false" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Migration: fix stale Codex descriptions (one-time) ───────
|
|
# Existing installs may have .agents/skills/gstack/SKILL.md with oversized
|
|
# descriptions (>1024 chars) that Codex rejects. We can't regenerate from
|
|
# the runtime root (no bun/scripts), so delete oversized files — the next
|
|
# ./setup or /gstack-upgrade will regenerate them properly.
|
|
# Marker file ensures this runs at most once per install.
|
|
if [ ! -f "$STATE_DIR/.codex-desc-healed" ]; then
|
|
for _AGENTS_SKILL in "$GSTACK_DIR"/.agents/skills/*/SKILL.md; do
|
|
[ -f "$_AGENTS_SKILL" ] || continue
|
|
_DESC=$(awk '/^---$/{n++;next}n==1&&/^description:/{d=1;sub(/^description:\s*/,"");if(length>0)print;next}d&&/^ /{sub(/^ /,"");print;next}d{d=0}' "$_AGENTS_SKILL" | wc -c | tr -d ' ')
|
|
if [ "${_DESC:-0}" -gt 1024 ]; then
|
|
rm -f "$_AGENTS_SKILL"
|
|
fi
|
|
done
|
|
mkdir -p "$STATE_DIR"
|
|
touch "$STATE_DIR/.codex-desc-healed"
|
|
fi
|
|
|
|
# ─── Snooze helper ──────────────────────────────────────────
|
|
# check_snooze <remote_version>
|
|
# Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output).
|
|
#
|
|
# Snooze file format: <version> <level> <epoch>
|
|
# Level durations: 1=24h, 2=48h, 3+=7d
|
|
# New version (version mismatch) resets snooze.
|
|
check_snooze() {
|
|
local remote_ver="$1"
|
|
if [ ! -f "$SNOOZE_FILE" ]; then
|
|
return 1 # no snooze file → not snoozed
|
|
fi
|
|
local snoozed_ver snoozed_level snoozed_epoch
|
|
snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
|
|
# Validate: all three fields must be non-empty
|
|
if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
|
|
return 1 # corrupt file → not snoozed
|
|
fi
|
|
|
|
# Validate: level and epoch must be integers
|
|
case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
|
|
case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
|
|
|
|
# New version dropped? Ignore snooze.
|
|
if [ "$snoozed_ver" != "$remote_ver" ]; then
|
|
return 1
|
|
fi
|
|
|
|
# Compute snooze duration based on level
|
|
local duration
|
|
case "$snoozed_level" in
|
|
1) duration=86400 ;; # 24 hours
|
|
2) duration=172800 ;; # 48 hours
|
|
*) duration=604800 ;; # 7 days (level 3+)
|
|
esac
|
|
|
|
local now
|
|
now="$(date +%s)"
|
|
local expires=$(( snoozed_epoch + duration ))
|
|
if [ "$now" -lt "$expires" ]; then
|
|
return 0 # still snoozed
|
|
fi
|
|
|
|
return 1 # snooze expired
|
|
}
|
|
|
|
# ─── Step 1: Read local version ──────────────────────────────
|
|
LOCAL=""
|
|
if [ -f "$VERSION_FILE" ]; then
|
|
LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
fi
|
|
if [ -z "$LOCAL" ]; then
|
|
exit 0 # No VERSION file → skip check
|
|
fi
|
|
|
|
# ─── Step 2: Check "just upgraded" marker ─────────────────────
|
|
if [ -f "$MARKER_FILE" ]; then
|
|
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
rm -f "$MARKER_FILE"
|
|
rm -f "$SNOOZE_FILE"
|
|
if [ -n "$OLD" ]; then
|
|
echo "JUST_UPGRADED $OLD $LOCAL"
|
|
fi
|
|
# Don't exit — fall through to remote check in case
|
|
# more updates landed since the upgrade
|
|
fi
|
|
|
|
# ─── Step 3: Check cache freshness ──────────────────────────
|
|
# UP_TO_DATE: 60 min TTL (detect new releases quickly)
|
|
# UPGRADE_AVAILABLE: 720 min TTL (keep nagging)
|
|
# CHECK_FAILED: 10 min TTL (retry soon, without re-fetching on every skill start)
|
|
CACHED=""
|
|
CACHED_ID=""
|
|
if [ -f "$CACHE_FILE" ]; then
|
|
CACHED="$(head -1 "$CACHE_FILE" 2>/dev/null || true)"
|
|
CACHED_ID="$(sed -n '2{/^IDENTITY /p;}' "$CACHE_FILE" 2>/dev/null || true)"
|
|
case "$CACHED" in
|
|
UP_TO_DATE*) CACHE_TTL=60 ;;
|
|
UPGRADE_AVAILABLE*) CACHE_TTL=720 ;;
|
|
CHECK_FAILED*) CACHE_TTL=10 ;;
|
|
*) CACHE_TTL=0 ;; # corrupt → force re-fetch
|
|
esac
|
|
|
|
_identity_ok || CACHE_TTL=0
|
|
STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true)
|
|
if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then
|
|
case "$CACHED" in
|
|
UP_TO_DATE*|CHECK_FAILED*)
|
|
# A cached CHECK_FAILED replays silently: status is unknown, and a
|
|
# preamble line every skill start for a transient outage is noise.
|
|
CACHED_VER="$(echo "$CACHED" | awk '{print $2}')"
|
|
if [ "$CACHED_VER" = "$LOCAL" ]; then
|
|
exit 0
|
|
fi
|
|
;;
|
|
UPGRADE_AVAILABLE*)
|
|
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
|
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
|
if check_snooze "$(_snooze_key "$CACHED")"; then
|
|
exit 0 # snoozed — stay quiet
|
|
fi
|
|
echo "$CACHED"
|
|
exit 0
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
# ─── Step 4: Slow path — fetch remote version ────────────────
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Fire Supabase install ping in background (parallel, non-blocking)
|
|
# This logs an update check event for community health metrics via edge function.
|
|
# If Supabase is not configured or telemetry is off, this is a no-op.
|
|
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
|
. "$GSTACK_DIR/supabase/config.sh"
|
|
fi
|
|
_SUPA_URL="${GSTACK_SUPABASE_URL:-}"
|
|
_SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
|
# Respect telemetry opt-out — don't ping Supabase if user set telemetry: off
|
|
_TEL_TIER="$("$GSTACK_DIR/bin/gstack-config" get telemetry 2>/dev/null || true)"
|
|
if [ -n "$_SUPA_URL" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off" ]; then
|
|
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
# Receipted (fail-open) and fully backgrounded: the receipt write happens
|
|
# inside the background subshell, so it can never block this script's exit.
|
|
_PING_FILE="$(mktemp "${TMPDIR:-/tmp}/gstack-update-ping-XXXXXX" 2>/dev/null)" || _PING_FILE=""
|
|
if [ -n "$_PING_FILE" ]; then
|
|
printf '{"version":"%s","os":"%s"}' "$LOCAL" "$_OS" > "$_PING_FILE"
|
|
_SUPA_HOST="${_SUPA_URL#*://}"; _SUPA_HOST="${_SUPA_HOST%%/*}"
|
|
GSTACK_HOME="$STATE_DIR" _receipted_curl open update-check "$_SUPA_HOST" update-check-ping "telemetry=$_TEL_TIER" "$_PING_FILE" \
|
|
curl -sf --max-time 5 \
|
|
-X POST "${_SUPA_URL}/functions/v1/update-check" \
|
|
-H "Content-Type: application/json" \
|
|
-H "apikey: ${_SUPA_KEY}" \
|
|
>/dev/null 2>&1 &
|
|
fi
|
|
fi
|
|
|
|
# Resolve VERSION via a SHA-pinned raw URL. GitHub's branch-raw CDN
|
|
# (raw.githubusercontent.com/<owner>/<repo>/<branch>/...) can serve stale
|
|
# content for several minutes after a push, which previously caused
|
|
# /gstack-upgrade to silently report "up to date" right after a release
|
|
# landed. git ls-remote always returns the live HEAD; SHA-pinned raw URLs
|
|
# are immediately consistent.
|
|
#
|
|
# An explicit GSTACK_REMOTE_URL override (tests, mirrors) skips this path
|
|
# so the override is honored verbatim.
|
|
REMOTE=""
|
|
if [ -z "${GSTACK_REMOTE_URL:-}" ]; then
|
|
# Disable credential prompts and apply a 5-second low-speed timeout so a
|
|
# flaky network or captive portal can't hang every skill preamble.
|
|
_REPO_HOST="${REMOTE_REPO#*://}"; _REPO_HOST="${_REPO_HOST#*@}"; _REPO_HOST="${_REPO_HOST%%[/:]*}"
|
|
_LSR_LINE="$(GSTACK_HOME="$STATE_DIR" _receipted_git open update-check "${_REPO_HOST:-unknown}" version-ls-remote "update_check!=false" \
|
|
bash -c 'GIT_TERMINAL_PROMPT=0 GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 \
|
|
git ls-remote "$1" refs/heads/main 2>/dev/null' _ "$REMOTE_REPO" || true)"
|
|
_REMOTE_SHA="$(echo "$_LSR_LINE" | awk '{print $1}')"
|
|
if echo "$_REMOTE_SHA" | grep -qE '^[0-9a-f]{40}$'; then
|
|
# The same main SHA, HEAD and upstream as the cache: its outcome still holds.
|
|
if [ "$(_identity_field remote)" = "$_REMOTE_SHA" ] && _identity_ok; then
|
|
case "$CACHED" in
|
|
UP_TO_DATE*)
|
|
[ "$(echo "$CACHED" | awk '{print $2}')" = "$LOCAL" ] && { touch "$CACHE_FILE"; exit 0; } ;;
|
|
UPGRADE_AVAILABLE*)
|
|
if [ "$(echo "$CACHED" | awk '{print $2}')" = "$LOCAL" ]; then
|
|
touch "$CACHE_FILE"
|
|
check_snooze "$(_snooze_key "$CACHED")" || echo "$CACHED"
|
|
exit 0
|
|
fi ;;
|
|
esac
|
|
fi
|
|
_SHA_URL="$REMOTE_RAW_BASE/${_REMOTE_SHA}/VERSION"
|
|
REMOTE="$(_receipted_version_fetch "$_SHA_URL")"
|
|
else
|
|
_REMOTE_SHA=""
|
|
fi
|
|
fi
|
|
|
|
# Fallback: branch-pinned URL when ls-remote is unavailable (no git, no
|
|
# network, mirror without refs/heads/main) or when GSTACK_REMOTE_URL was
|
|
# explicitly overridden.
|
|
if [ -z "$REMOTE" ]; then
|
|
REMOTE="$(_receipted_version_fetch "$REMOTE_URL")"
|
|
fi
|
|
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
|
|
|
# Validate: must look like a version number (reject HTML error pages)
|
|
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
|
|
# Invalid or empty response: the remote version is UNKNOWN, not equal (#2786).
|
|
# Both fetches were tried when ls-remote resolved a SHA: name both.
|
|
_FAILED_URL="$(_display_url "$REMOTE_URL")"
|
|
if [ -n "${_SHA_URL:-}" ]; then
|
|
_FAILED_URL="$(_display_url "$_SHA_URL") (then $_FAILED_URL)"
|
|
fi
|
|
_write_cache "CHECK_FAILED $LOCAL $_FAILED_URL"
|
|
if [ "$FORCE" = "1" ]; then
|
|
echo "CHECK_FAILED could not read the remote gstack version from $_FAILED_URL — update status UNKNOWN, not up-to-date"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
NEW="$REMOTE"
|
|
if [ "$LOCAL" = "$REMOTE" ]; then
|
|
# Equal VERSION: main may still have moved past this checkout without a
|
|
# release. Ask GitHub how the two commits relate (status, ahead_by); page 2
|
|
# of one commit per page carries the counts without the file list.
|
|
if [ -z "${_REMOTE_SHA:-}" ] || [ "$LOCAL_HEAD" = "none" ] || [ "$_REMOTE_SHA" = "$LOCAL_HEAD" ]; then
|
|
_write_cache "UP_TO_DATE $LOCAL"
|
|
exit 0
|
|
fi
|
|
_CMP_URL="$COMPARE_API/${LOCAL_HEAD}...${_REMOTE_SHA}?per_page=1&page=2"
|
|
_CMP="$(_receipted_version_fetch "$_CMP_URL" | tr -s '\n\r\t ' ' ')"
|
|
_CMP_STATUS="$(printf '%s' "$_CMP" | sed -nE 's/.*"status" ?: ?"([a-z]+)" ?, ?"ahead_by" ?: ?([0-9]+).*/\1 \2/p')"
|
|
_AHEAD="${_CMP_STATUS#* }"
|
|
case "${_CMP_STATUS% *}" in
|
|
identical|behind)
|
|
_write_cache "UP_TO_DATE $LOCAL"
|
|
exit 0 ;;
|
|
ahead)
|
|
if [ "${_AHEAD:-0}" -gt 0 ] 2>/dev/null; then
|
|
NEW="$LOCAL+$_AHEAD $(printf '%s' "$_REMOTE_SHA" | cut -c1-7)"
|
|
fi ;;
|
|
esac
|
|
if [ "$NEW" = "$REMOTE" ]; then
|
|
_WHY="${_CMP_STATUS% *}"
|
|
_CMP_SHOWN="$(_display_url "$COMPARE_API")/${LOCAL_HEAD:0:12}...${_REMOTE_SHA:0:12}"
|
|
_write_cache "CHECK_FAILED $LOCAL $_CMP_SHOWN ${_WHY:-unreadable}"
|
|
if [ "$FORCE" = "1" ]; then
|
|
if [ "$_WHY" = "diverged" ]; then
|
|
echo "CHECK_FAILED this checkout (${LOCAL_HEAD:0:12}) has diverged from main (${_REMOTE_SHA:0:12}) at the same VERSION $LOCAL — update status UNKNOWN, not up-to-date"
|
|
else
|
|
echo "CHECK_FAILED could not compare this checkout (${LOCAL_HEAD:0:12}) with main (${_REMOTE_SHA:0:12}) at $_CMP_SHOWN — update status UNKNOWN, not up-to-date"
|
|
fi
|
|
fi
|
|
exit 0
|
|
fi
|
|
else
|
|
# Semver-order guard: only flag an upgrade when REMOTE sorts higher than
|
|
# LOCAL. Protects against transient stale-CDN regressions (REMOTE < LOCAL)
|
|
# and dev installs running ahead of main, both of which would otherwise
|
|
# emit a backwards UPGRADE_AVAILABLE line.
|
|
_HIGHER="$(printf '%s\n%s\n' "$LOCAL" "$REMOTE" | sort -V | tail -1)"
|
|
if [ "$_HIGHER" != "$REMOTE" ]; then
|
|
_write_cache "UP_TO_DATE $LOCAL"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# REMOTE is strictly newer, or main moved past HEAD at the same VERSION
|
|
_write_cache "UPGRADE_AVAILABLE $LOCAL $NEW"
|
|
if check_snooze "$(_snooze_key "UPGRADE_AVAILABLE $LOCAL $NEW")"; then
|
|
exit 0 # snoozed — stay quiet
|
|
fi
|
|
|
|
# Log upgrade_prompted event (only on slow-path fetch, not cached replays)
|
|
TEL_CMD="$GSTACK_DIR/bin/gstack-telemetry-log"
|
|
if [ -x "$TEL_CMD" ]; then
|
|
"$TEL_CMD" --event-type upgrade_prompted --skill "" --duration 0 \
|
|
--outcome success --session-id "update-$$-$(date +%s)" 2>/dev/null &
|
|
fi
|
|
|
|
echo "UPGRADE_AVAILABLE $LOCAL $NEW"
|