Files
getsentry__sentry-for-ai/scripts/build-skill-tree.sh
Evan Purkhiser 81e88c7c5c docs: add /sentry-get-started design brainstorm
feat(skills): add /sentry-get-started and restructure into five routers

Reorganize the skill tree around a single guided entry point. Previously the
three routers (sdk-setup / workflow / feature-setup) assumed the user already
knew their intent, there was no orientation layer, and SDK setup never
confirmed an event actually reached Sentry.

Add a `/sentry-get-started` command that orients the user from a cheap probe
(MCP auth, repo already using Sentry, has a project) and routes into the right
skill -- sending brand-new users straight into first-error setup and showing an
existing user the relevant menu.

Move to five visible routers so every menu item maps 1:1 to a router the user
can invoke directly later (get-started is just a first-run concierge, never the
sole path to anything): sdk-setup, add-signal, improve-setup, workflow,
monitors. Retire feature-setup and redistribute its skills.

Separate WHAT/WHY from HOW: new platform-agnostic concept skills (tracing,
logging, metrics, crons, profiling, session-replay, user-feedback) cover
strategy and best practices, while the SDK skill remains the source of truth
for platform mechanics and is entered scoped to a capability.

Add sentry-verify-instrumentation, a shared hidden utility that closes the loop
-- trigger a real event and confirm it landed via the MCP -- so no SDK skill
reimplements verification.

Several new improve-setup (source-maps, releases, data-scrubbing,
reduce-volume, enable-ai-review) and monitors (uptime, metric-alerts,
dashboards) leaves are intentionally stubs to flesh out in follow-up passes.

Extend build-skill-tree.sh for the new categories/sections, regenerate
SKILL_TREE.md, and update AGENTS.md. All four plugin builds validate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:07:08 -04:00

550 lines
19 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================
# build-skill-tree.sh — Generate and validate the Sentry skill tree
# ============================================================
# Scans all skills/*/SKILL.md files, regenerates SKILL_TREE.md,
# validates the skill hierarchy, and checks breadcrumb links.
#
# Usage:
# scripts/build-skill-tree.sh # regenerate + validate
# scripts/build-skill-tree.sh --check # validate only (no write)
#
# Exit codes: 0 = pass, 1 = errors found
# Requirements: bash 3.2+, grep, sed, awk, diff, find
set -euo pipefail
# ── Setup ────────────────────────────────────────────────────
CHECK_ONLY=false
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=true
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
SKILL_TREE_FILE="SKILL_TREE.md"
SKILLS_DIR="skills"
# Temp directory for per-skill data (bash 3 compatible, no assoc arrays)
TMPDIR_SKILLS="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_SKILLS"' EXIT
ERRORS=()
error() { ERRORS+=("ERROR: $*"); }
warn() { echo "WARN: $*" >&2; }
# ============================================================
# SECTION 1: Parse frontmatter from a SKILL.md file
# Outputs: key=value lines for known fields
# ============================================================
parse_frontmatter() {
local file="$1"
awk '
BEGIN { in_fm=0; fm_count=0 }
/^---$/ {
fm_count++
if (fm_count == 1) { in_fm=1; next }
if (fm_count == 2) { exit }
}
in_fm && /^[a-zA-Z-]+:/ {
colon = index($0, ":")
key = substr($0, 1, colon - 1)
val = substr($0, colon + 2)
# Remove leading/trailing whitespace from val
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
# Normalize key: replace hyphens with underscores
gsub(/-/, "_", key)
print key "=" val
}
' "$file"
}
# Write a field value to a temp file for skill $name
skill_set() {
local name="$1" field="$2" value="$3"
# Sanitize name for filesystem use (replace / and spaces)
local safe_name="${name//[^a-zA-Z0-9_-]/_}"
printf '%s' "$value" > "${TMPDIR_SKILLS}/${safe_name}.${field}"
}
# Read a field value for skill $name (empty string if missing)
skill_get() {
local name="$1" field="$2"
local safe_name="${name//[^a-zA-Z0-9_-]/_}"
local f="${TMPDIR_SKILLS}/${safe_name}.${field}"
[[ -f "$f" ]] && cat "$f" || echo ""
}
# ============================================================
# SECTION 2: Scan all skills
# ============================================================
ALL_SKILLS=()
while IFS= read -r skill_file; do
s_name="" s_desc="" s_cat="" s_parent="" s_role="" s_disable=""
while IFS='=' read -r key val; do
case "$key" in
name) s_name="$val" ;;
description) s_desc="$val" ;;
category) s_cat="$val" ;;
parent) s_parent="$val" ;;
role) s_role="$val" ;;
disable_model_invocation) s_disable="$val" ;;
esac
done < <(parse_frontmatter "$skill_file")
# Fall back to directory name if name field is missing
[[ -z "$s_name" ]] && s_name="$(basename "$(dirname "$skill_file")")"
ALL_SKILLS+=("$s_name")
skill_set "$s_name" "desc" "$s_desc"
skill_set "$s_name" "category" "$s_cat"
skill_set "$s_name" "parent" "$s_parent"
skill_set "$s_name" "role" "$s_role"
skill_set "$s_name" "disable" "$s_disable"
skill_set "$s_name" "file" "$skill_file"
done < <(find "$SKILLS_DIR" -name "SKILL.md" | sort)
TOTAL_SKILLS=${#ALL_SKILLS[@]}
# ============================================================
# SECTION 3: Categorize
# ============================================================
ROUTERS=()
SKILLS_SDK_SETUP=()
SKILLS_ADD_SIGNAL=()
SKILLS_IMPROVE_SETUP=()
SKILLS_WORKFLOW=()
SKILLS_MONITORS=()
for name in "${ALL_SKILLS[@]}"; do
role="$(skill_get "$name" role)"
cat="$(skill_get "$name" category)"
if [[ "$role" == "router" ]]; then
ROUTERS+=("$name")
else
case "$cat" in
sdk-setup) SKILLS_SDK_SETUP+=("$name") ;;
add-signal) SKILLS_ADD_SIGNAL+=("$name") ;;
improve-setup) SKILLS_IMPROVE_SETUP+=("$name") ;;
workflow) SKILLS_WORKFLOW+=("$name") ;;
monitors) SKILLS_MONITORS+=("$name") ;;
internal) ;; # validated but not shown in public skill tree
esac
fi
done
TOTAL_ROUTERS=${#ROUTERS[@]}
# ============================================================
# SECTION 4: Generate SKILL_TREE.md content
# ============================================================
# Extract a short column value from a description.
# sdk-setup: "Full Sentry SDK setup for X." -> "X"
# others: first sentence
get_column_value() {
local desc="$1"
local category="$2"
case "$category" in
sdk-setup)
echo "$desc" \
| sed 's/Full Sentry SDK setup for //' \
| sed 's/\. .*//' \
| sed 's/\.$//'
;;
*)
echo "$desc" \
| sed 's/\. .*//' \
| sed 's/\.$//'
;;
esac
}
column_header() {
case "$1" in
sdk-setup) echo "Platform" ;;
add-signal) echo "Signal" ;;
improve-setup) echo "Improvement" ;;
workflow) echo "Use when" ;;
monitors) echo "Monitor / Alert" ;;
internal) echo "Purpose" ;;
*) echo "Notes" ;;
esac
}
# Build markdown table rows for a list of skills in a category
build_table_rows() {
local category="$1"
shift
local skills=("$@")
for name in ${skills[@]+"${skills[@]}"}; do
local file desc col_val skill_path
file="$(skill_get "$name" file)"
desc="$(skill_get "$name" desc)"
col_val="$(get_column_value "$desc" "$category")"
# Strip leading "skills/" from file path for the fetchable URL path
skill_path="${file#skills/}"
printf "| %s | [\`%s\`](%s) | \`%s\` |\n" "$col_val" "$name" "$file" "$skill_path"
done
}
# Build keyword lookup rows for SDK skills
# Maps common platform keywords to fetchable paths
build_keyword_lookup() {
for name in ${SKILLS_SDK_SETUP[@]+"${SKILLS_SDK_SETUP[@]}"}; do
local file keywords skill_path
file="$(skill_get "$name" file)"
skill_path="${file#skills/}"
case "$name" in
sentry-android-sdk)
keywords="android, kotlin, java, jetpack compose" ;;
sentry-browser-sdk)
keywords="browser, vanilla js, javascript, jquery, cdn, wordpress, static site" ;;
sentry-cloudflare-sdk)
keywords="cloudflare, cloudflare workers, cloudflare pages, wrangler, durable objects, d1, hono" ;;
sentry-cocoa-sdk)
keywords="ios, macos, swift, cocoa, tvos, watchos, visionos, swiftui, uikit" ;;
sentry-dotnet-sdk)
keywords=".net, csharp, c#, asp.net, maui, wpf, winforms, blazor, azure functions" ;;
sentry-flutter-sdk)
keywords="flutter, dart, sentry_flutter, pubspec, dio" ;;
sentry-go-sdk)
keywords="go, golang, gin, echo, fiber" ;;
sentry-elixir-sdk)
keywords="elixir, phoenix, plug, liveview, oban, quantum, mix" ;;
sentry-nestjs-sdk)
keywords="nestjs, nest" ;;
sentry-nextjs-sdk)
keywords="nextjs, next.js, next" ;;
sentry-node-sdk)
keywords="node, nodejs, node.js, bun, deno, express, fastify, koa, hapi" ;;
sentry-php-sdk)
keywords="php, laravel, symfony" ;;
sentry-python-sdk)
keywords="python, django, flask, fastapi, celery, starlette" ;;
sentry-react-native-sdk)
keywords="react native, expo" ;;
sentry-react-sdk)
keywords="react, react router, tanstack, redux, vite" ;;
sentry-react-router-framework-sdk)
keywords="react-router framework, @sentry/react-router, @react-router/dev, react-router reveal" ;;
sentry-tanstack-start-sdk)
keywords="tanstack start, tanstack react start, @tanstack/react-start, tanstackstart-react" ;;
sentry-ruby-sdk)
keywords="ruby, rails, sinatra, sidekiq, rack" ;;
sentry-svelte-sdk)
keywords="svelte, sveltekit" ;;
*)
keywords="$name" ;;
esac
printf "| %s | \`%s\` |\n" "$keywords" "$skill_path"
done
}
generate_skill_tree() {
cat <<'HEADER'
# Sentry Skills
You are **Sentry's AI assistant**. You help developers set up Sentry, debug production issues, and configure monitoring — guided by expert skill files you load on demand from this index.
## How to Use These Skills
Tell your AI coding assistant:
Use curl to download, read and follow: https://skills.sentry.dev/sdks
Or for a specific SDK:
Use curl to download, read and follow: https://skills.sentry.dev/sentry-nextjs-sdk/SKILL.md
**Why curl?** Skills are detailed 10–20 KB markdown files. Fetch tools (like WebFetch) often summarize them, losing critical configuration details. `curl -sL` guarantees the full content.
### Entry Points
| URL | What it loads |
|---|---|
| `https://skills.sentry.dev/` | This page — full skill index |
| `https://skills.sentry.dev/sdks` | SDK setup — detect platform and install Sentry |
| `https://skills.sentry.dev/workflows` | Workflows — debug issues, review code, upgrade SDKs |
| `https://skills.sentry.dev/features` | Features — AI monitoring, alerts, OpenTelemetry |
### Fetching Individual Skills
Append the skill path from the `Path` column in the tables below:
https://skills.sentry.dev/<skill-name>/SKILL.md
Do not guess or shorten URLs. Use exact paths from the tables.
## Start Here — Read This Before Doing Anything
**Do not skip this section.** Do not assume what the user needs based on their project files. Do not start installing packages, creating files, or running commands until you have confirmed the user's intent.
1. **Ask first.** Greet the user and ask what they'd like help with. Present these options:
- **Start monitoring** — First-time setup: install the SDK, capture an error, and confirm it lands (`sentry-sdk-setup`)
- **Add a signal** — Tracing, logging, metrics, crons, profiling, session replay, user feedback, AI/LLM monitoring (`sentry-add-signal`)
- **Fix something** — Investigate and fix production issues, or resolve Sentry bot / Seer PR comments (`sentry-workflow`)
- **Improve my setup** — Source maps, releases, data scrubbing, quota, AI code review, upgrades, migrations (`sentry-improve-setup`)
- **Monitors & alerts** — Alert rules, uptime, metric alerts, dashboards (`sentry-monitors`)
2. **Wait for their answer.** Do not proceed until the user tells you what they want.
3. **Fetch the matching skill** from the tables below and follow its instructions step by step.
Each skill file contains its own detection logic, prerequisites, and configuration steps. Trust the skill — read it carefully and follow it. Do not improvise or take shortcuts.
---
HEADER
# SDK Setup
local col_sdk col_wf col_fs
col_sdk="$(column_header sdk-setup)"
cat <<'SDK_HEADER'
## SDK Setup
Install and configure Sentry for any platform. If unsure which SDK fits, detect the platform from the user's project files (`package.json`, `go.mod`, `requirements.txt`, `Gemfile`, `*.csproj`, `build.gradle`, etc.).
SDK_HEADER
printf "| %s | Skill | Path |\n" "$col_sdk"
printf "|---|---|---|\n"
build_table_rows "sdk-setup" ${SKILLS_SDK_SETUP[@]+"${SKILLS_SDK_SETUP[@]}"}
cat <<'SDK_ROUTING'
### Platform Detection Priority
When multiple SDKs could match, prefer the more specific one:
- **Android** (`build.gradle` with android plugin) → `sentry-android-sdk`
- **NestJS** (`@nestjs/core`) → `sentry-nestjs-sdk` over `sentry-node-sdk`
- **Next.js** → `sentry-nextjs-sdk` over `sentry-react-sdk` or `sentry-node-sdk`
- **React Router Framework** (`@sentry/react-router` or `@react-router/*`) → `sentry-react-router-framework-sdk` over `sentry-react-sdk`
- **TanStack Start React** (`@tanstack/react-start`) → `sentry-tanstack-start-sdk` over `sentry-react-sdk`
- **React Native** → `sentry-react-native-sdk` over `sentry-react-sdk`
- **PHP** with Laravel or Symfony → `sentry-php-sdk`
- **Node.js / Bun / Deno** without a specific framework → `sentry-node-sdk`
- **Browser JS** (vanilla, jQuery, static sites) → `sentry-browser-sdk`
- **No match** → direct user to [Sentry Docs](https://docs.sentry.io/platforms/)
SDK_ROUTING
# Add a Signal
local col_as col_is col_mon
col_as="$(column_header add-signal)"
cat <<'AS_HEADER'
## Add a Signal
Add telemetry to a project that already reports errors: tracing, logging, metrics, crons,
profiling, session replay, user feedback, and AI/LLM monitoring. The concept skills cover the
WHAT/WHY; the platform SDK skill handles the HOW.
AS_HEADER
printf "| %s | Skill | Path |\n" "$col_as"
printf "|---|---|---|\n"
build_table_rows "add-signal" ${SKILLS_ADD_SIGNAL[@]+"${SKILLS_ADD_SIGNAL[@]}"}
# Improve My Setup
col_is="$(column_header improve-setup)"
cat <<'IS_HEADER'
## Improve My Setup
Make an existing Sentry setup better, cleaner, cheaper, safer, or more current: source maps,
releases, data scrubbing, quota, AI code review, upgrades, and migrations.
IS_HEADER
printf "| %s | Skill | Path |\n" "$col_is"
printf "|---|---|---|\n"
build_table_rows "improve-setup" ${SKILLS_IMPROVE_SETUP[@]+"${SKILLS_IMPROVE_SETUP[@]}"}
# Workflows
col_wf="$(column_header workflow)"
cat <<'WF_HEADER'
## Workflows
Debug production issues and maintain code quality with Sentry context.
WF_HEADER
printf "| %s | Skill | Path |\n" "$col_wf"
printf "|---|---|---|\n"
build_table_rows "workflow" ${SKILLS_WORKFLOW[@]+"${SKILLS_WORKFLOW[@]}"}
# Monitors & Alerts
col_mon="$(column_header monitors)"
cat <<'MON_HEADER'
## Monitors & Alerts
Get notified and watch availability: alert rules and notifications, uptime, metric alerts, and
dashboards.
MON_HEADER
printf "| %s | Skill | Path |\n" "$col_mon"
printf "|---|---|---|\n"
build_table_rows "monitors" ${SKILLS_MONITORS[@]+"${SKILLS_MONITORS[@]}"}
# Quick Lookup section
cat <<'LOOKUP_HEADER'
## Quick Lookup
Match your project to a skill by keywords. Append the path to `https://skills.sentry.dev/` to fetch.
| Keywords | Path |
|---|---|
LOOKUP_HEADER
build_keyword_lookup
printf "\n"
}
# ============================================================
# SECTION 5: Validate
# ============================================================
KNOWN_CATEGORIES=("sdk-setup" "add-signal" "improve-setup" "workflow" "monitors" "internal")
validate() {
for name in "${ALL_SKILLS[@]}"; do
local role cat parent disable skill_file
role="$(skill_get "$name" role)"
cat="$(skill_get "$name" category)"
parent="$(skill_get "$name" parent)"
disable="$(skill_get "$name" disable)"
skill_file="$(skill_get "$name" file)"
# ── (a/b/c) Required fields per skill type ───────────────
if [[ "$role" == "router" ]]; then
: # role: router is sufficient
elif [[ "$cat" == "internal" ]]; then
# (b) Internal skills
[[ "$disable" == "true" ]] || \
error "$name: internal skill missing 'disable-model-invocation: true'"
else
# (a) Regular hidden skills
[[ -n "$cat" ]] || \
error "$name: non-router skill missing 'category' field"
[[ -n "$parent" ]] || \
error "$name: non-router skill missing 'parent' field"
[[ "$disable" == "true" ]] || \
error "$name: non-router skill missing 'disable-model-invocation: true'"
fi
# ── (g) Warn on unknown category ─────────────────────────
if [[ -n "$cat" && "$role" != "router" ]]; then
local known=false
for kc in "${KNOWN_CATEGORIES[@]}"; do
[[ "$cat" == "$kc" ]] && known=true && break
done
$known || warn "$name: unknown category '$cat'"
fi
# ── (d) Parent must exist and be a router ────────────────
if [[ -n "$parent" ]]; then
local parent_role
parent_role="$(skill_get "$parent" role)"
if [[ -z "$(skill_get "$parent" file)" ]]; then
error "$name: parent '$parent' does not exist"
elif [[ "$parent_role" != "router" ]]; then
error "$name: parent '$parent' is not a router (role=${parent_role:-none})"
fi
fi
# ── (e) Skill appears in its router's SKILL.md ───────────
if [[ -n "$parent" ]]; then
local router_file
router_file="$(skill_get "$parent" file)"
if [[ -n "$router_file" && -f "$router_file" ]]; then
if ! grep -q "$name" "$router_file" 2>/dev/null; then
error "$name: not listed in router '$parent' ($router_file)"
fi
fi
fi
# ── (f) Breadcrumb links resolve ─────────────────────────
local skill_dir
skill_dir="$(dirname "$skill_file")"
while IFS= read -r breadcrumb_line; do
# Extract only markdown link paths ending in .md: ](path.md)
# Pattern ](path) where path ends with .md (skip http links)
while IFS= read -r link_path; do
[[ "$link_path" =~ ^https?:// ]] && continue
local resolved="$skill_dir/$link_path"
if [[ ! -f "$resolved" ]]; then
error "$name: broken breadcrumb link '$link_path' (resolved: $resolved)"
fi
done < <(echo "$breadcrumb_line" | grep -oE '\]\([^)]+\.md\)' | sed 's/^](\(.*\))$/\1/')
done < <(grep '^> ' "$skill_file" 2>/dev/null || true)
done
}
# ============================================================
# SECTION 6: Run
# ============================================================
echo "Scanning ${TOTAL_SKILLS} skills in ${SKILLS_DIR}/..."
GENERATED="$(generate_skill_tree)"
validate
# ── Stale check / write ──────────────────────────────────────
if [[ -f "$SKILL_TREE_FILE" ]]; then
EXISTING="$(cat "$SKILL_TREE_FILE")"
if [[ "$GENERATED" != "$EXISTING" ]]; then
echo ""
echo "SKILL_TREE.md diff (existing → generated):"
diff <(echo "$EXISTING") <(echo "$GENERATED") || true
echo ""
if $CHECK_ONLY; then
error "SKILL_TREE.md is stale. Run scripts/build-skill-tree.sh to regenerate."
else
echo "SKILL_TREE.md is stale — regenerating..."
printf '%s\n' "$GENERATED" > "$SKILL_TREE_FILE"
echo "SKILL_TREE.md updated."
fi
else
echo "SKILL_TREE.md is up to date."
fi
else
if $CHECK_ONLY; then
error "SKILL_TREE.md does not exist. Run scripts/build-skill-tree.sh to generate."
else
printf '%s\n' "$GENERATED" > "$SKILL_TREE_FILE"
echo "SKILL_TREE.md created."
fi
fi
# ── Summary ──────────────────────────────────────────────────
echo ""
echo "Summary: ${TOTAL_SKILLS} skills scanned, ${TOTAL_ROUTERS} routers, ${#ERRORS[@]} errors"
if [[ ${#ERRORS[@]} -gt 0 ]]; then
echo ""
echo "Errors:"
for e in ${ERRORS[@]+"${ERRORS[@]}"}; do
echo " $e"
done
exit 1
fi
echo "All checks passed."
exit 0