mirror of
https://github.com/getsentry/sentry-for-ai.git
synced 2026-09-14 14:28:22 +08:00
a2bd28368d
Dissolving the last routers (#310) emptied both category buckets, but the generator still emitted their headings, intro prose, and a bare table header with no rows underneath. That ships to every agent that reads SKILL_TREE.md as two sections advertising capabilities with nothing in them. Removing just those sections would leave a worse state behind: a skill carrying a category would still be scanned and validated, but nothing would render it, so it would vanish from the tree with the script reporting success. So retire the concept instead of half of it. Nothing needs it. No skill carries category, parent, role, or disable-model-invocation; outside this script nothing reads those fields at all. What remained was ~120 lines validating and rendering a model the project has abandoned -- router roles, parent-must-be-a-router, leaf-listed-in-its-router, per-category tables -- none of it reachable. The hierarchy validation is replaced by the rule that actually holds now: those four fields are rejected outright, so a stale copy-paste or an attempt to reintroduce routing fails loudly rather than dropping a skill silently. The categorize pass collapses with it: one flat table means every skill is a row, so ALL_SKILLS is the list and the STANDALONE/ROUTERS split has nothing left to distinguish. The heading becomes "Available Skills" -- "Standalone" only meant something in contrast to the category sections that are gone. The inert breadcrumb check goes too. It skipped every skill that was neither a router nor categorized, which is all of them, so no link has been checked for some time; replacing it is a separate change.
252 lines
8.5 KiB
Bash
Executable File
252 lines
8.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ============================================================
|
|
# build-skill-tree.sh — Generate and validate the Sentry skill tree
|
|
# ============================================================
|
|
# Scans all src/skills/*/SKILL.md files, regenerates src/SKILL_TREE.md, and
|
|
# validates each skill's frontmatter.
|
|
#
|
|
# Link checking lives in validate-skill-links.py — it needs the reference
|
|
# manifests, which this script does not read.
|
|
#
|
|
# 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="src/SKILL_TREE.md"
|
|
SKILLS_DIR="src/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: Generate SKILL_TREE.md content
|
|
# ============================================================
|
|
|
|
# Escape characters that would break a markdown table cell.
|
|
escape_cell() {
|
|
printf '%s' "$1" | tr '\n' ' ' | sed 's/|/\\|/g'
|
|
}
|
|
|
|
# Build a markdown row per skill, using the full description — it is the
|
|
# routing signal now that there is nothing else to route through.
|
|
build_skill_rows() {
|
|
for name in "${ALL_SKILLS[@]}"; do
|
|
local file desc
|
|
file="$(skill_get "$name" file)"
|
|
desc="$(escape_cell "$(skill_get "$name" desc)")"
|
|
printf "| [\`%s\`](%s) | %s |\n" "$name" "${file#src/}" "$desc"
|
|
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.
|
|
|
|
## 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:
|
|
- **Set up Sentry** — Add error monitoring, performance tracing, session replay, or AI/LLM monitoring to a project
|
|
- **Debug a production issue** — Investigate errors and exceptions using Sentry data
|
|
- **Configure a feature** — alerts, OpenTelemetry pipelines
|
|
- **Review code** — Resolve Sentry bot comments or check for predicted bugs
|
|
|
|
2. **Wait for their answer.** Do not proceed until the user tells you what they want.
|
|
|
|
3. **Read the matching skill** from the table 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
|
|
|
|
cat <<'SKILLS_HEADER'
|
|
|
|
## Available Skills
|
|
|
|
Each one is self-contained and named for the job it does. If you're not sure what the user needs, read `sentry-get-started`; it orients you and points to the right skill.
|
|
|
|
| Skill | What it does |
|
|
|---|---|
|
|
SKILLS_HEADER
|
|
build_skill_rows
|
|
|
|
printf "\n"
|
|
}
|
|
|
|
# ============================================================
|
|
# SECTION 4: Validate
|
|
# ============================================================
|
|
|
|
# Frontmatter fields from the retired router model. Skills are flat and
|
|
# task-shaped now: one skill = one job, discoverable from its own description.
|
|
# A skill carrying any of these is either a stale copy-paste or an attempt to
|
|
# reintroduce routing -- both worth stopping on, since nothing renders them.
|
|
# Stored field name : the frontmatter key to name in the error.
|
|
RETIRED_FIELDS=("category:category" "parent:parent" "role:role" "disable:disable-model-invocation")
|
|
|
|
validate() {
|
|
for name in "${ALL_SKILLS[@]}"; do
|
|
[[ -n "$(skill_get "$name" desc)" ]] || \
|
|
error "$name: missing 'description' field"
|
|
|
|
for entry in "${RETIRED_FIELDS[@]}"; do
|
|
[[ -z "$(skill_get "$name" "${entry%%:*}")" ]] || \
|
|
error "$name: '${entry#*:}' is no longer supported -- every skill is standalone"
|
|
done
|
|
done
|
|
}
|
|
|
|
# ============================================================
|
|
# SECTION 5: 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, ${#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
|