merge: incorporate origin/main into community-mode branch

Resolved 10 conflicted files:
- VERSION/package.json: kept 0.12.0.0 (feature branch version)
- CHANGELOG.md: preserved both branch entry and main's new entries
- supabase/config.sh: kept GSTACK_WEB_URL, accepted TELEMETRY_ENDPOINT removal
- bin/gstack-{community-dashboard,telemetry-log,telemetry-sync,update-check}:
  took main's improved versions (edge function approach, safe cursor, UUID gen)
- supabase/functions/community-pulse: took main's count-based approach
- test/telemetry.test.ts: took main's structure with fingerprint field name

Post-merge fixes:
- Removed shadowed local RESOLVERS/functions in gen-skill-docs.ts (main's
  resolver imports now take precedence for tier-based preamble, coverage gates)
- Added 3 missing E2E_TIERS entries (ship-plan-*, review-plan-completion)
- Updated telemetry test to match current prompt text
- Regenerated all SKILL.md files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-24 21:01:19 -07:00
68 changed files with 5645 additions and 495 deletions
+35 -91
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# gstack-community-dashboard — community usage stats from Supabase
#
# Queries the Supabase REST API to show community-wide gstack usage:
# Calls the community-pulse edge function for aggregated stats:
# skill popularity, crash clusters, version distribution, retention.
#
# Env overrides (for testing):
@@ -30,46 +30,40 @@ if [ -z "$SUPABASE_URL" ] || [ -z "$ANON_KEY" ]; then
exit 0
fi
# ─── Helper: query Supabase REST API ─────────────────────────
query() {
local table="$1"
local params="${2:-}"
curl -sf --max-time 10 \
"${SUPABASE_URL}/rest/v1/${table}?${params}" \
-H "apikey: ${ANON_KEY}" \
-H "Authorization: Bearer ${ANON_KEY}" \
2>/dev/null || echo "[]"
}
# ─── Fetch aggregated stats from edge function ────────────────
DATA="$(curl -sf --max-time 15 \
"${SUPABASE_URL}/functions/v1/community-pulse" \
-H "apikey: ${ANON_KEY}" \
2>/dev/null || echo "{}")"
echo "gstack community dashboard"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# ─── Weekly active installs ──────────────────────────────────
WEEK_AGO="$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")"
if [ -n "$WEEK_AGO" ]; then
# Direct REST query (replaces unreliable community-pulse edge function)
WEEKLY="$(curl -sf --max-time 10 \
"${SUPABASE_URL}/rest/v1/update_checks?select=install_fingerprint&checked_at=gte.${WEEK_AGO}&source=eq.live" \
-H "apikey: ${ANON_KEY}" \
-H "Authorization: Bearer ${ANON_KEY}" \
2>/dev/null | grep -o '"install_fingerprint":"[^"]*"' | sort -u | wc -l | tr -d ' ')"
WEEKLY="${WEEKLY:-0}"
WEEKLY="$(echo "$DATA" | grep -o '"weekly_active":[0-9]*' | grep -o '[0-9]*' || echo "0")"
CHANGE="$(echo "$DATA" | grep -o '"change_pct":[0-9-]*' | grep -o '[0-9-]*' || echo "0")"
echo "Weekly active installs: ${WEEKLY} unique"
echo ""
echo "Weekly active installs: ${WEEKLY}"
if [ "$CHANGE" -gt 0 ] 2>/dev/null; then
echo " Change: +${CHANGE}%"
elif [ "$CHANGE" -lt 0 ] 2>/dev/null; then
echo " Change: ${CHANGE}%"
fi
echo ""
# ─── Skill popularity (top 10) ───────────────────────────────
echo "Top skills (last 7 days)"
echo "────────────────────────"
# Query telemetry_events, group by skill
EVENTS="$(query "telemetry_events" "select=skill,gstack_version,session_id&event_type=eq.skill_run&event_timestamp=gte.${WEEK_AGO}&source=eq.live&limit=1000" 2>/dev/null || echo "[]")"
if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then
echo "$EVENTS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -10 | while read -r COUNT SKILL; do
printf " /%-20s %d runs\n" "$SKILL" "$COUNT"
# Parse top_skills array from JSON
SKILLS="$(echo "$DATA" | grep -o '"top_skills":\[[^]]*\]' || echo "")"
if [ -n "$SKILLS" ] && [ "$SKILLS" != '"top_skills":[]' ]; then
# Parse each object — handle any key order (JSONB doesn't preserve order)
echo "$SKILLS" | grep -o '{[^}]*}' | while read -r OBJ; do
SKILL="$(echo "$OBJ" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}')"
COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')"
[ -n "$SKILL" ] && [ -n "$COUNT" ] && printf " /%-20s %s runs\n" "$SKILL" "$COUNT"
done
else
echo " No data yet"
@@ -80,31 +74,13 @@ echo ""
echo "Top errors (last 7 days)"
echo "────────────────────────"
ERRORS="$(query "telemetry_events" "select=skill,error_class,error_message,failed_step,duration_s,session_id&outcome=eq.error&event_timestamp=gte.${WEEK_AGO}&source=eq.live&order=event_timestamp.desc&limit=200" 2>/dev/null || echo "[]")"
if [ "$ERRORS" != "[]" ] && [ -n "$ERRORS" ]; then
# Group by skill + error_class, show count and example message
echo "$ERRORS" | grep -o '"skill":"[^"]*"[^}]*"error_class":"[^"]*"' | \
sed 's/.*"skill":"//;s/".*"error_class":"/\t/' | sed 's/"$//' | \
sort | uniq -c | sort -rn | head -8 | while read -r COUNT COMBO; do
SKILL="$(echo "$COMBO" | cut -f1)"
ERR="$(echo "$COMBO" | cut -f2)"
# Find an example error_message for this combo
MSG="$(echo "$ERRORS" | grep -o "\"skill\":\"${SKILL}\"[^}]*\"error_message\":\"[^\"]*\"" | \
grep -o '"error_message":"[^"]*"' | head -1 | sed 's/"error_message":"//;s/"$//' || true)"
# Find an example failed_step
STEP="$(echo "$ERRORS" | grep -o "\"skill\":\"${SKILL}\"[^}]*\"failed_step\":\"[^\"]*\"" | \
grep -o '"failed_step":"[^"]*"' | head -1 | sed 's/"failed_step":"//;s/"$//' || true)"
printf " /%-12s %-18s %3d errors\n" "$SKILL" "${ERR:-unknown}" "$COUNT"
[ -n "$STEP" ] && printf " step: %s\n" "$STEP"
[ -n "$MSG" ] && printf " e.g.: %s\n" "$(echo "$MSG" | head -c 80)"
done
# Show how many unique sessions have errors
ERR_SESSIONS="$(echo "$ERRORS" | grep -o '"session_id":"[^"]*"' | sort -u | wc -l | tr -d ' ')"
echo ""
echo " ${ERR_SESSIONS} unique session(s) with errors"
CRASHES="$(echo "$DATA" | grep -o '"crashes":\[[^]]*\]' || echo "")"
if [ -n "$CRASHES" ] && [ "$CRASHES" != '"crashes":[]' ]; then
echo "$CRASHES" | grep -o '{[^}]*}' | head -5 | while read -r OBJ; do
ERR="$(echo "$OBJ" | grep -o '"error_class":"[^"]*"' | awk -F'"' '{print $4}')"
C="$(echo "$OBJ" | grep -o '"total_occurrences":[0-9]*' | grep -o '[0-9]*')"
[ -n "$ERR" ] && printf " %-30s %s occurrences\n" "$ERR" "${C:-?}"
done
else
echo " No errors reported"
fi
@@ -114,49 +90,17 @@ echo ""
echo "Version distribution (last 7 days)"
echo "───────────────────────────────────"
if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then
echo "$EVENTS" | grep -o '"gstack_version":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -5 | while read -r COUNT VER; do
printf " v%-15s %d events\n" "$VER" "$COUNT"
VERSIONS="$(echo "$DATA" | grep -o '"versions":\[[^]]*\]' || echo "")"
if [ -n "$VERSIONS" ] && [ "$VERSIONS" != '"versions":[]' ]; then
echo "$VERSIONS" | grep -o '{[^}]*}' | head -5 | while read -r OBJ; do
VER="$(echo "$OBJ" | grep -o '"version":"[^"]*"' | awk -F'"' '{print $4}')"
COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')"
[ -n "$VER" ] && [ -n "$COUNT" ] && printf " v%-15s %s events\n" "$VER" "$COUNT"
done
else
echo " No data yet"
fi
# ─── Sessions (distinct session_id, works for all tiers) ────
echo "Sessions (last 7 days)"
echo "──────────────────────"
if [ "$EVENTS" != "[]" ] && [ -n "$EVENTS" ]; then
SESSION_COUNT="$(echo "$EVENTS" | grep -o '"session_id":"[^"]*"' | sort -u | wc -l | tr -d ' ')"
echo " ${SESSION_COUNT} unique sessions"
else
echo " No session data"
fi
echo ""
# ─── Skill recommendations ─────────────────────────────────
# Fetch top skills for recommendations
TOP_SKILLS="$(echo "$EVENTS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -3 | awk '{print $2}' | tr '\n' ',' | sed 's/,$//')"
if [ -n "$TOP_SKILLS" ]; then
RECS="$(curl -sf --max-time 10 \
"${SUPABASE_URL}/functions/v1/community-recommendations?skills=${TOP_SKILLS}" \
-H "Authorization: Bearer ${ANON_KEY}" \
2>/dev/null || echo '{"recommendations":[]}')"
REC_LIST="$(echo "$RECS" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}')"
REC_REASONS="$(echo "$RECS" | grep -o '"reason":"[^"]*"' | awk -F'"' '{print $4}')"
if [ -n "$REC_LIST" ]; then
echo "Skills you might like"
echo "─────────────────────"
paste <(echo "$REC_LIST") <(echo "$REC_REASONS") 2>/dev/null | while IFS=$'\t' read -r SKILL REASON; do
[ -z "$SKILL" ] && continue
printf " /%-20s %s\n" "$SKILL" "${REASON:-}"
done
echo ""
fi
fi
echo "For local analytics: gstack-analytics"
echo "For benchmarks: gstack-community-benchmarks"
+13 -4
View File
@@ -115,17 +115,26 @@ if [ -d "$STATE_DIR/sessions" ]; then
fi
# Generate/read persistent UUID fingerprint (all tiers, not just community)
# Uses a random UUID stored locally — not derived from hostname/user so it
# can't be guessed or correlated by someone who knows your machine identity.
INSTALL_FP=""
FP_FILE="$STATE_DIR/.install-id"
if [ -f "$FP_FILE" ]; then
INSTALL_FP="$(cat "$FP_FILE" 2>/dev/null | tr -d '[:space:]')"
fi
if [ -z "$INSTALL_FP" ]; then
INSTALL_FP="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null || echo "")"
INSTALL_FP="$(echo "$INSTALL_FP" | tr '[:upper:]' '[:lower:]')" # normalize case
# Generate a random UUID v4
if command -v uuidgen >/dev/null 2>&1; then
INSTALL_FP="$(uuidgen | tr '[:upper:]' '[:lower:]')"
elif [ -r /proc/sys/kernel/random/uuid ]; then
INSTALL_FP="$(cat /proc/sys/kernel/random/uuid)"
else
# Fallback: random hex from /dev/urandom
INSTALL_FP="$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n')"
fi
if [ -n "$INSTALL_FP" ]; then
mkdir -p "$STATE_DIR"
echo "$INSTALL_FP" > "$FP_FILE"
mkdir -p "$STATE_DIR" 2>/dev/null
printf '%s' "$INSTALL_FP" > "$FP_FILE" 2>/dev/null
fi
fi
+25 -36
View File
@@ -3,11 +3,12 @@
#
# Fire-and-forget, backgrounded, rate-limited to once per 5 minutes.
# Strips local-only fields before sending. Respects privacy tiers.
# Posts to the telemetry-ingest edge function (not PostgREST directly).
#
# Env overrides (for testing):
# GSTACK_STATE_DIR — override ~/.gstack state directory
# GSTACK_DIR — override auto-detected gstack root
# GSTACK_TELEMETRY_ENDPOINT — override Supabase endpoint URL
# GSTACK_SUPABASE_URL — override Supabase project URL
set -uo pipefail
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
@@ -19,15 +20,15 @@ RATE_FILE="$ANALYTICS_DIR/.last-sync-time"
CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
# Source Supabase config if not overridden by env
if [ -z "${GSTACK_TELEMETRY_ENDPOINT:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
. "$GSTACK_DIR/supabase/config.sh"
fi
ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
SUPABASE_URL="${GSTACK_SUPABASE_URL:-}"
ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
# ─── Pre-checks ──────────────────────────────────────────────
# No endpoint configured yet → exit silently
[ -z "$ENDPOINT" ] && exit 0
# No Supabase URL configured yet → exit silently
[ -z "$SUPABASE_URL" ] && exit 0
# No JSONL file → nothing to sync
[ -f "$JSONL_FILE" ] || exit 0
@@ -66,6 +67,8 @@ UNSENT="$(tail -n "+$SKIP" "$JSONL_FILE" 2>/dev/null || true)"
[ -z "$UNSENT" ] && exit 0
# ─── Strip local-only fields and build batch ─────────────────
# Edge function expects raw JSONL field names (v, ts, sessions) —
# no column renaming needed (the function maps them internally).
BATCH="["
FIRST=true
COUNT=0
@@ -75,15 +78,10 @@ while IFS= read -r LINE; do
[ -z "$LINE" ] && continue
echo "$LINE" | grep -q '^{' || continue
# Strip local-only fields + map JSONL field names to Postgres column names
# Backward compat: map old installation_id → install_fingerprint for unsent entries
# Strip local-only fields (keep v, ts, sessions as-is for edge function)
CLEAN="$(echo "$LINE" | sed \
-e 's/,"_repo_slug":"[^"]*"//g' \
-e 's/,"_branch":"[^"]*"//g' \
-e 's/"v":/"schema_version":/g' \
-e 's/"ts":/"event_timestamp":/g' \
-e 's/"sessions":/"concurrent_sessions":/g' \
-e 's/"installation_id":/"install_fingerprint":/g' \
-e 's/,"repo":"[^"]*"//g')"
if [ "$FIRST" = "true" ]; then
@@ -103,40 +101,31 @@ BATCH="$BATCH]"
# Nothing to send after filtering
[ "$COUNT" -eq 0 ] && exit 0
# ─── POST to Supabase ────────────────────────────────────────
HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
-X POST "${ENDPOINT}/telemetry_events" \
# ─── POST to edge function ───────────────────────────────────
RESP_FILE="$(mktemp /tmp/gstack-sync-XXXXXX 2>/dev/null || echo "/tmp/gstack-sync-$$")"
HTTP_CODE="$(curl -s -w '%{http_code}' --max-time 10 \
-X POST "${SUPABASE_URL}/functions/v1/telemetry-ingest" \
-H "Content-Type: application/json" \
-H "apikey: ${ANON_KEY}" \
-H "Authorization: Bearer ${ANON_KEY}" \
-H "Prefer: return=minimal" \
-o "$RESP_FILE" \
-d "$BATCH" 2>/dev/null || echo "000")"
# ─── Update cursor on success (2xx) ─────────────────────────
case "$HTTP_CODE" in
2*) NEW_CURSOR=$(( CURSOR + COUNT ))
2*)
# Parse inserted count from response — only advance if events were actually inserted.
# Advance by SENT count (not inserted count) because we can't map inserted back to
# source lines. If inserted==0, something is systemically wrong — don't advance.
INSERTED="$(grep -o '"inserted":[0-9]*' "$RESP_FILE" 2>/dev/null | grep -o '[0-9]*' || echo "0")"
if [ "${INSERTED:-0}" -gt 0 ] 2>/dev/null; then
NEW_CURSOR=$(( CURSOR + COUNT ))
echo "$NEW_CURSOR" > "$CURSOR_FILE" 2>/dev/null || true
# Ping update_checks (install base proxy)
GSTACK_VERSION="$(cat "$GSTACK_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "unknown")"
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
curl -sf --max-time 5 \
-X POST "${ENDPOINT}/update_checks" \
-H "Content-Type: application/json" \
-H "apikey: ${ANON_KEY}" \
-H "Authorization: Bearer ${ANON_KEY}" \
-H "Prefer: return=minimal" \
-d "{\"gstack_version\":\"$GSTACK_VERSION\",\"os\":\"$_OS\"}" \
>/dev/null 2>&1 || true
# Trigger community backup if community tier
BACKUP_CMD="$GSTACK_DIR/bin/gstack-community-backup"
if [ "$TIER" = "community" ] && [ -x "$BACKUP_CMD" ]; then
"$BACKUP_CMD" 2>/dev/null &
fi
;;
fi
;;
esac
rm -f "$RESP_FILE" 2>/dev/null || true
# Update rate limit marker
touch "$RATE_FILE" 2>/dev/null || true
+9 -27
View File
@@ -160,40 +160,22 @@ fi
mkdir -p "$STATE_DIR"
# Fire Supabase install ping in background (parallel, non-blocking)
# This logs an update check event for community health metrics.
# If the endpoint isn't configured or Supabase is down, this is a no-op.
# Source Supabase config for install ping
if [ -z "${GSTACK_TELEMETRY_ENDPOINT:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
# 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_ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
_SUPA_URL="${GSTACK_SUPABASE_URL:-}"
_SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
# Generate/read install fingerprint (runs for ALL tiers including off)
_FP=""
_FP_FILE="$STATE_DIR/.install-id"
if [ -f "$_FP_FILE" ]; then
_FP="$(cat "$_FP_FILE" 2>/dev/null | tr -d '[:space:]')"
fi
if [ -z "$_FP" ]; then
_FP="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null || echo "")"
_FP="$(echo "$_FP" | tr '[:upper:]' '[:lower:]')"
if [ -n "$_FP" ]; then
mkdir -p "$STATE_DIR"
echo "$_FP" > "$_FP_FILE"
fi
fi
# Update-check pings always fire (ungated from telemetry tier).
# This sends only: version, OS, and a random UUID. No usage data.
# Equivalent to what GitHub sees in HTTP access logs for VERSION.
if [ -n "$_SUPA_ENDPOINT" ] && [ -n "$_SUPA_KEY" ]; then
# 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:]')"
curl -sf --max-time 5 \
-X POST "${_SUPA_ENDPOINT}/update_checks" \
-X POST "${_SUPA_URL}/functions/v1/update-check" \
-H "Content-Type: application/json" \
-H "apikey: ${_SUPA_KEY}" \
-H "Authorization: Bearer ${_SUPA_KEY}" \
-H "Prefer: return=minimal" \
-d "{\"gstack_version\":\"$LOCAL\",\"os\":\"$_OS\",\"install_fingerprint\":\"${_FP}\"}" \
-d "{\"version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
>/dev/null 2>&1 &
fi