diff --git a/scripts/git-identity-hook/git-identity-hook.sh b/scripts/git-identity-hook/git-identity-hook.sh new file mode 100755 index 0000000..c0d0e2e --- /dev/null +++ b/scripts/git-identity-hook/git-identity-hook.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Soma — Git Identity Pre-Commit Hook +# +# Validates git user.email before allowing commits. +# Two modes: +# 1. If .soma/settings.json has guard.gitIdentity.email → enforces that exact email +# 2. Otherwise → just checks that user.email is set (not empty) +# +# Note: this hook checks the first email value only. For multiple valid emails +# (array format), the soma-guard.ts runtime check handles the full validation. +# The hook is a lightweight safety net, not the primary enforcement. +# +# Install: copy to .git/hooks/pre-commit (or use `soma init --hooks`) +# Or symlink: ln -sf ../../.soma/scripts/git-identity-hook.sh .git/hooks/pre-commit + +set -euo pipefail + +CURRENT_EMAIL=$(git config user.email 2>/dev/null || echo "") +CURRENT_NAME=$(git config user.name 2>/dev/null || echo "") + +# --- Check if email is set at all --- +if [ -z "$CURRENT_EMAIL" ]; then + echo "❌ git user.email is not set!" + echo "" + echo "Set it with:" + echo " git config user.name \"Your Name\"" + echo " git config user.email \"you@example.com\"" + echo "" + echo "Or add an includeIf to ~/.gitconfig for this directory." + exit 1 +fi + +# --- Check against .soma/settings.json if it exists --- +SOMA_DIR="" +# Walk up to find .soma/ +DIR="$(pwd)" +while [ "$DIR" != "/" ]; do + if [ -d "$DIR/.soma" ]; then + SOMA_DIR="$DIR/.soma" + break + fi + DIR="$(dirname "$DIR")" +done + +if [ -n "$SOMA_DIR" ] && [ -f "$SOMA_DIR/settings.json" ]; then + # Extract expected email (lightweight — no jq dependency) + EXPECTED_EMAIL=$(grep -o '"email"[[:space:]]*:[[:space:]]*"[^"]*"' "$SOMA_DIR/settings.json" 2>/dev/null | head -1 | sed 's/.*: *"\(.*\)"/\1/') + + if [ -n "$EXPECTED_EMAIL" ] && [ "$CURRENT_EMAIL" != "$EXPECTED_EMAIL" ]; then + echo "⚠️ Git identity mismatch!" + echo "" + echo " Current: $CURRENT_NAME <$CURRENT_EMAIL>" + echo " Expected: <$EXPECTED_EMAIL> (from .soma/settings.json)" + echo "" + echo "Fix with:" + echo " git config user.email \"$EXPECTED_EMAIL\"" + echo "" + echo "Or update .soma/settings.json guard.gitIdentity.email" + echo "" + echo "To commit anyway: git commit --no-verify" + exit 1 + fi +fi + +# Identity looks good +exit 0 diff --git a/scripts/soma-code/soma-code.sh b/scripts/soma-code/soma-code.sh index 5a49f73..72b71bc 100755 --- a/scripts/soma-code/soma-code.sh +++ b/scripts/soma-code/soma-code.sh @@ -33,10 +33,10 @@ set -euo pipefail # ── Theme ── -source "$(dirname "$0")/soma-theme.sh" 2>/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m' - SOMA_GREEN='\033[0;32m'; SOMA_YELLOW='\033[0;33m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}" +SOMA_GREEN="${SOMA_GREEN:-\033[0;32m}"; SOMA_YELLOW="${SOMA_YELLOW:-\033[0;33m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" SHELL_DIR="${SOMA_SHELL_DIR:-$(pwd)}" RED='\033[0;31m' diff --git a/scripts/soma-compat/soma-compat.sh b/scripts/soma-compat/soma-compat.sh new file mode 100644 index 0000000..118dc40 --- /dev/null +++ b/scripts/soma-compat/soma-compat.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# σ Soma Compatibility Check +# Detects overlap, redundancy, and conflicting directives across protocols + muscles. +# Usage: bash soma-compat.sh [path-to-soma-dir] +set -o pipefail + +SOMA_DIR="${1:-$(pwd)/.soma}" +[ ! -d "$SOMA_DIR" ] && SOMA_DIR="$(pwd)" + +PROTO_DIR="$SOMA_DIR/protocols" +MUSCLE_DIR="$SOMA_DIR/memory/muscles" + +SCORE=100 +WARNINGS=0 +ISSUES="" + +# ── Helpers ────────────────────────────────────────────────────── + +extract_fm() { + awk '/^---$/{n++; next} n==1{print}' "$1" +} + +extract_body() { + awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' "$1" +} + +get_field() { + echo "$1" | grep -i "^$2:" | head -1 | sed "s/^$2: *//i" | tr -d '"' | tr -d "'" +} + +get_array() { + echo "$1" | grep -i "^$2:" | head -1 | sed "s/^$2: *//i" | tr -d '[]"'"'" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' +} + +get_name() { + local fm; fm=$(extract_fm "$1") + local name; name=$(get_field "$fm" "name") + [ -z "$name" ] && name=$(basename "$1" .md) + echo "$name" +} + +warn() { + WARNINGS=$((WARNINGS + 1)) + ISSUES="${ISSUES}\n ⚠️ $1" + SCORE=$((SCORE - $2)) +} + +# ── Collect items ──────────────────────────────────────────────── + +declare -a FILES=() +declare -a NAMES=() +declare -a TYPES=() +declare -a TAG_SETS=() +declare -a APPLIES_SETS=() +declare -a DIRECTIVES=() + +idx=0 +for dir_info in "protocol:$PROTO_DIR" "muscle:$MUSCLE_DIR"; do + type="${dir_info%%:*}" + dir="${dir_info#*:}" + [ ! -d "$dir" ] && continue + for f in "$dir"/*.md; do + [ ! -f "$f" ] && continue + [[ "$(basename "$f")" == _* ]] && continue + [[ "$(basename "$f")" == "README.md" ]] && continue + + fm=$(extract_fm "$f") + body=$(extract_body "$f") + name=$(get_field "$fm" "name") + [ -z "$name" ] && name=$(basename "$f" .md) + + tags=$(get_array "$fm" "tags" | tr '\n' '|') + applies=$(get_array "$fm" "applies-to" | tr '\n' '|') + [ -z "$applies" ] && applies=$(get_array "$fm" "appliesTo" | tr '\n' '|') + + # Extract directive words with context + directives=$(echo "$body" | grep -oiE "(always|never|must|don.t|stop|avoid|require|forbid|prohibited)[^.!]*[.!]" | head -20 | tr '\n' '|') + + FILES[$idx]="$f" + NAMES[$idx]="$name" + TYPES[$idx]="$type" + TAG_SETS[$idx]="$tags" + APPLIES_SETS[$idx]="$applies" + DIRECTIVES[$idx]="$directives" + idx=$((idx + 1)) + done +done + +TOTAL=$idx + +if [ "$TOTAL" -lt 2 ]; then + echo "σ Compatibility Check — $TOTAL items (need 2+ to compare)" + exit 0 +fi + +# ── Compare pairs ──────────────────────────────────────────────── + +tag_overlap() { + local a="$1" b="$2" + local shared=0 total_a=0 total_b=0 + + IFS='|' read -ra A <<< "$a" + IFS='|' read -ra B <<< "$b" + total_a=${#A[@]} + total_b=${#B[@]} + + for ta in "${A[@]}"; do + [ -z "$ta" ] && continue + for tb in "${B[@]}"; do + [ -z "$tb" ] && continue + [ "$ta" = "$tb" ] && shared=$((shared + 1)) + done + done + + local max=$total_a + [ $total_b -gt $max ] && max=$total_b + [ $max -eq 0 ] && echo 0 && return + echo $((shared * 100 / max)) +} + +directive_conflicts() { + local a="$1" b="$2" + local conflicts="" + + # Check for always/never on same topic + IFS='|' read -ra DA <<< "$a" + IFS='|' read -ra DB <<< "$b" + + for da in "${DA[@]}"; do + [ -z "$da" ] && continue + da_lower=$(echo "$da" | tr '[:upper:]' '[:lower:]') + for db in "${DB[@]}"; do + [ -z "$db" ] && continue + db_lower=$(echo "$db" | tr '[:upper:]' '[:lower:]') + + # "always X" vs "never X" or "don't X" + if echo "$da_lower" | grep -q "^always" && echo "$db_lower" | grep -q "^never\|^don.t\|^stop\|^avoid"; then + # Check if they share a key noun (3+ char words) + for word in $(echo "$da_lower" | tr -cs '[:alpha:]' '\n' | awk 'length>=4'); do + if echo "$db_lower" | grep -qi "$word"; then + conflicts="${conflicts}CONFLICT: \"${da:0:60}\" vs \"${db:0:60}\"\n" + break + fi + done + fi + # Reverse + if echo "$db_lower" | grep -q "^always" && echo "$da_lower" | grep -q "^never\|^don.t\|^stop\|^avoid"; then + for word in $(echo "$db_lower" | tr -cs '[:alpha:]' '\n' | awk 'length>=4'); do + if echo "$da_lower" | grep -qi "$word"; then + conflicts="${conflicts}CONFLICT: \"${db:0:60}\" vs \"${da:0:60}\"\n" + break + fi + done + fi + done + done + + echo -e "$conflicts" +} + +scope_overlap() { + local a="$1" b="$2" + [ -z "$a" ] || [ -z "$b" ] && echo 0 && return + [ "$a" = "|" ] || [ "$b" = "|" ] && echo 0 && return + + # "always" applies to everything — high overlap with anything + if echo "$a" | grep -q "always" && echo "$b" | grep -q "always"; then + echo 50 + return + fi + + tag_overlap "$a" "$b" +} + +# ── Run comparisons ───────────────────────────────────────────── + +for ((i=0; i/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" # ── Project root discovery ── find_project_root() { local dir="$PWD" diff --git a/scripts/soma-plans/soma-plans.sh b/scripts/soma-plans/soma-plans.sh index c0688d9..686765a 100755 --- a/scripts/soma-plans/soma-plans.sh +++ b/scripts/soma-plans/soma-plans.sh @@ -23,9 +23,9 @@ set -euo pipefail # ── Theme ── -source "$(dirname "$0")/soma-theme.sh" 2>/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" SOMA_DIR="" for d in .soma "$HOME/.soma"; do [[ -d "$d" ]] && SOMA_DIR="$d" && break diff --git a/scripts/soma-query/soma-query.sh b/scripts/soma-query/soma-query.sh index 14ba3c7..b291b6d 100755 --- a/scripts/soma-query/soma-query.sh +++ b/scripts/soma-query/soma-query.sh @@ -37,9 +37,9 @@ set -uo pipefail # ── Theme ── -source "$(dirname "$0")/soma-theme.sh" 2>/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" # ── Paths ──────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" diff --git a/scripts/soma-reflect/soma-reflect.sh b/scripts/soma-reflect/soma-reflect.sh index fcd684d..4ec7b62 100755 --- a/scripts/soma-reflect/soma-reflect.sh +++ b/scripts/soma-reflect/soma-reflect.sh @@ -41,10 +41,10 @@ set -uo pipefail # ── Theme ── -source "$(dirname "$0")/soma-theme.sh" 2>/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' - SOMA_GREEN='\033[0;32m'; SOMA_RED='\033[0;31m'; SOMA_YELLOW='\033[0;33m'; SOMA_MAGENTA='\033[0;35m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" +SOMA_GREEN="${SOMA_GREEN:-\033[0;32m}"; SOMA_RED="${SOMA_RED:-\033[0;31m}"; SOMA_YELLOW="${SOMA_YELLOW:-\033[0;33m}"; SOMA_MAGENTA="${SOMA_MAGENTA:-\033[0;35m}" BOLD="${SOMA_BOLD:-\033[1m}" DIM="${SOMA_DIM:-\033[2m}" NC="${SOMA_NC:-\033[0m}" diff --git a/scripts/soma-scrape/soma-scrape.sh b/scripts/soma-scrape/soma-scrape.sh index d122a6c..972141d 100755 --- a/scripts/soma-scrape/soma-scrape.sh +++ b/scripts/soma-scrape/soma-scrape.sh @@ -39,9 +39,9 @@ set -o pipefail # ── Theme ── -source "$(dirname "$0")/soma-theme.sh" 2>/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" # ── Configurable Variables ────────────────────────────────────────────────── # Where scraped docs live diff --git a/scripts/soma-seam/soma-seam.sh b/scripts/soma-seam/soma-seam.sh index b5e9de4..cb0bae7 100755 --- a/scripts/soma-seam/soma-seam.sh +++ b/scripts/soma-seam/soma-seam.sh @@ -34,9 +34,9 @@ set -euo pipefail # ── Theme ── -source "$(dirname "$0")/soma-theme.sh" 2>/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SOMA_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" @@ -1276,7 +1276,6 @@ audit_range() { echo -e " ${GREEN}✓${RESET} No fallback mismatches detected" fi } - # ─── HELP ───────────────────────────────────────────────────────────────── case "$CMD" in diff --git a/scripts/soma-snapshot/soma-snapshot.sh b/scripts/soma-snapshot/soma-snapshot.sh new file mode 100755 index 0000000..30e1a1c --- /dev/null +++ b/scripts/soma-snapshot/soma-snapshot.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# soma-snapshot.sh — Rolling zip snapshots of project directories +# +# Usage: +# soma-snapshot.sh [label] +# soma-snapshot.sh /path/to/project "pre-reorg" +# soma-snapshot.sh . # current dir, auto-labeled +# +# Features: +# - Respects .zipignore (like .gitignore but for snapshots) +# - Falls back to .gitignore if no .zipignore +# - Rolling window: keeps last 3 snapshots per project +# - Syncs to external drive if mounted +# - Excludes node_modules, .git, dist, build by default + +set -euo pipefail + +# --- Config --- +SNAPSHOT_DIR="${SOMA_SNAPSHOT_DIR:-$HOME/.soma/snapshots}" +EXTERNAL_MOUNT="${SOMA_EXTERNAL_DRIVE:-/Volumes/Backup}" # macOS external drive +MAX_SNAPSHOTS=3 +TIMESTAMP=$(date +%Y%m%d-%H%M%S) + +# Default excludes (always skip these) +DEFAULT_EXCLUDES=( + "node_modules/*" + ".git/*" + "dist/*" + "build/*" + ".next/*" + ".astro/*" + ".vercel/*" + "__pycache__/*" + "*.pyc" + ".DS_Store" + "Thumbs.db" + "vendor/*" + ".cache/*" + "coverage/*" + "*.log" +) + +# --- Args --- +PROJECT_DIR="${1:-.}" +PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)" +PROJECT_NAME="$(basename "$PROJECT_DIR")" +LABEL="${2:-auto}" + +# --- Build exclude list --- +EXCLUDE_FILE=$(mktemp) +trap 'rm -f "$EXCLUDE_FILE"' EXIT + +# Start with defaults +for pattern in "${DEFAULT_EXCLUDES[@]}"; do + echo "$pattern" >> "$EXCLUDE_FILE" +done + +# Add .zipignore if it exists +if [ -f "$PROJECT_DIR/.zipignore" ]; then + echo " 📋 Using .zipignore" + # Filter out comments and blank lines + grep -v '^#' "$PROJECT_DIR/.zipignore" | grep -v '^$' >> "$EXCLUDE_FILE" +elif [ -f "$PROJECT_DIR/.gitignore" ]; then + echo " 📋 Falling back to .gitignore" + grep -v '^#' "$PROJECT_DIR/.gitignore" | grep -v '^$' | grep -v '^!' >> "$EXCLUDE_FILE" +fi + +# --- Create snapshot --- +mkdir -p "$SNAPSHOT_DIR/$PROJECT_NAME" + +SNAPSHOT_FILE="$SNAPSHOT_DIR/$PROJECT_NAME/${PROJECT_NAME}_${TIMESTAMP}_${LABEL}.zip" + +echo "σ Snapshotting: $PROJECT_NAME ($LABEL)" +echo " 📁 Source: $PROJECT_DIR" + +cd "$PROJECT_DIR" +zip -r -q "$SNAPSHOT_FILE" . -x@"$EXCLUDE_FILE" 2>/dev/null + +SIZE=$(du -sh "$SNAPSHOT_FILE" | awk '{print $1}') +echo " ✓ Snapshot: $(basename "$SNAPSHOT_FILE") ($SIZE)" + +# --- Rolling cleanup --- +SNAPSHOTS=($(ls -t "$SNAPSHOT_DIR/$PROJECT_NAME/"*.zip 2>/dev/null)) +TOTAL=${#SNAPSHOTS[@]} + +if [ "$TOTAL" -gt "$MAX_SNAPSHOTS" ]; then + REMOVED=$((TOTAL - MAX_SNAPSHOTS)) + for ((i=MAX_SNAPSHOTS; i/dev/null)) + EXT_TOTAL=${#EXT_SNAPSHOTS[@]} + if [ "$EXT_TOTAL" -gt 10 ]; then + for ((i=10; i/dev/null || { - SOMA_BOLD='\033[1m'; SOMA_DIM='\033[2m'; SOMA_NC='\033[0m'; SOMA_CYAN='\033[0;36m' -} +_sd="$(dirname "$0")" +if [ -f "$_sd/soma-theme.sh" ]; then source "$_sd/soma-theme.sh"; fi +SOMA_BOLD="${SOMA_BOLD:-\033[1m}"; SOMA_DIM="${SOMA_DIM:-\033[2m}"; SOMA_NC="${SOMA_NC:-\033[0m}"; SOMA_CYAN="${SOMA_CYAN:-\033[0;36m}" # ── Spelling rules: American → Canadian ── # Format: american|canadian # Only includes words that appear in technical/agent writing diff --git a/scripts/validate-content/validate-content.sh b/scripts/validate-content/validate-content.sh new file mode 100755 index 0000000..ad524e8 --- /dev/null +++ b/scripts/validate-content/validate-content.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash +# validate-content.sh — validate AMPS content files before submitting a PR +# +# Usage: +# bash scripts/validate-content.sh # validate one file +# bash scripts/validate-content.sh # validate all .md in dir +# bash scripts/validate-content.sh --type protocol . # filter by type +# +# Checks: +# - Valid YAML frontmatter (type, name, status, heat-default) +# - Breadcrumb present and under 200 chars +# - TL;DR section (protocols) +# - Digest markers (muscles) +# - File naming (kebab-case) +# - No PII patterns (emails, API keys) +# - applies-to is valid array + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +PASS=0 FAIL=0 WARN=0 TOTAL=0 +pass() { PASS=$((PASS + 1)); TOTAL=$((TOTAL + 1)); echo -e " ${GREEN}✓${NC} $1"; } +fail() { FAIL=$((FAIL + 1)); TOTAL=$((TOTAL + 1)); echo -e " ${RED}✗${NC} $1"; } +warn() { WARN=$((WARN + 1)); TOTAL=$((TOTAL + 1)); echo -e " ${YELLOW}⚠${NC} $1"; } + +# --------------------------------------------------------------------------- +# Parse args +# --------------------------------------------------------------------------- + +TYPE_FILTER="" +FILES=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --type) TYPE_FILTER="$2"; shift 2 ;; + *) FILES+=("$1"); shift ;; + esac +done + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "Usage: validate-content.sh [--type protocol|muscle|automation|skill] " + exit 1 +fi + +# Expand directories to .md files +EXPANDED=() +for f in "${FILES[@]}"; do + if [[ -d "$f" ]]; then + while IFS= read -r md; do + EXPANDED+=("$md") + done < <(find "$f" -name "*.md" -not -name "_*" -not -path "*/_archive/*" | sort) + elif [[ -f "$f" ]]; then + EXPANDED+=("$f") + else + echo -e "${RED}Not found: $f${NC}" + fi +done + +if [[ ${#EXPANDED[@]} -eq 0 ]]; then + echo -e "${RED}No .md files found${NC}" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Validate each file +# --------------------------------------------------------------------------- + +for file in "${EXPANDED[@]}"; do + # Extract frontmatter + if ! head -1 "$file" | grep -q "^---"; then + continue # Skip non-frontmatter files + fi + + fm=$(sed -n '/^---$/,/^---$/p' "$file" | head -50) + body=$(sed '1,/^---$/!d; 1d' "$file" | tail -n +2) # after second --- + full_body=$(tail -n +2 "$file" | sed '1,/^---$/d') + + # Extract frontmatter fields + fm_type=$(echo "$fm" | grep "^type:" | head -1 | sed 's/type:\s*//' | tr -d ' "'"'"'') + fm_name=$(echo "$fm" | grep "^name:" | head -1 | sed 's/name:\s*//' | tr -d ' "'"'"'') + fm_status=$(echo "$fm" | grep "^status:" | head -1 | sed 's/status:\s*//' | tr -d ' "'"'"'') + fm_heat=$(echo "$fm" | grep "^heat-default:" | head -1 | sed 's/heat-default:\s*//' | tr -d ' "'"'"'') + fm_breadcrumb=$(echo "$fm" | grep "^breadcrumb:" | head -1 | sed 's/breadcrumb:\s*//') + fm_applies=$(echo "$fm" | grep "^applies-to:" | head -1) + + # Type filter + if [[ -n "$TYPE_FILTER" && "$fm_type" != "$TYPE_FILTER" ]]; then + continue + fi + + echo -e "\n${CYAN}── $(basename "$file") ──${NC} (${fm_type:-unknown})" + + # --- Required fields --- + if [[ -n "$fm_type" ]]; then + pass "type: $fm_type" + else + fail "missing: type" + fi + + if [[ -n "$fm_name" ]]; then + pass "name: $fm_name" + else + fail "missing: name" + fi + + if [[ -n "$fm_status" ]]; then + case "$fm_status" in + active|draft|deprecated|experimental) pass "status: $fm_status" ;; + *) warn "unusual status: $fm_status (expected: active|draft|deprecated|experimental)" ;; + esac + else + fail "missing: status" + fi + + # --- Type-specific checks --- + case "$fm_type" in + protocol) + # heat-default required + if [[ -n "$fm_heat" ]]; then + case "$fm_heat" in + hot|warm|cold) pass "heat-default: $fm_heat" ;; + *) fail "invalid heat-default: $fm_heat (expected: hot|warm|cold)" ;; + esac + else + fail "missing: heat-default (required for protocols)" + fi + + # breadcrumb required + if [[ -n "$fm_breadcrumb" ]]; then + bc_len=${#fm_breadcrumb} + if [[ $bc_len -gt 200 ]]; then + warn "breadcrumb too long (${bc_len} chars, max 200)" + else + pass "breadcrumb present (${bc_len} chars)" + fi + else + fail "missing: breadcrumb (required for protocols — this is ALL the agent sees when warm)" + fi + + # applies-to required + if [[ -n "$fm_applies" ]]; then + pass "applies-to present" + else + fail "missing: applies-to" + fi + + # TL;DR section + if echo "$full_body" | grep -q "^## TL;DR"; then + pass "has TL;DR section" + else + warn "missing TL;DR section (recommended for protocols)" + fi + + # Rule section + if echo "$full_body" | grep -q "^## Rule\|^## Rules"; then + pass "has Rule section" + else + warn "missing Rule section" + fi + ;; + + muscle) + # Digest markers + if grep -q "\|" "$file"; then + pass "has digest markers" + else + warn "missing digest markers ( ... )" + fi + + # topic field + if echo "$fm" | grep -q "^topic:"; then + pass "has topic field" + else + warn "missing: topic (helps with discovery)" + fi + + # keywords field + if echo "$fm" | grep -q "^keywords:"; then + pass "has keywords field" + else + warn "missing: keywords" + fi + ;; + + automation) + # steps or procedure section + if echo "$full_body" | grep -qi "^## Steps\|^## Procedure\|^## Workflow"; then + pass "has Steps/Procedure section" + else + warn "missing Steps/Procedure section" + fi + ;; + + skill) + # Knowledge content check + if [[ $(wc -l < "$file") -lt 10 ]]; then + warn "very short skill ($(wc -l < "$file") lines) — is there enough content?" + else + pass "skill has content ($(wc -l < "$file") lines)" + fi + ;; + esac + + # --- File naming --- + basename_file=$(basename "$file" .md) + if echo "$basename_file" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then + pass "filename: kebab-case ✓" + else + warn "filename '$basename_file' — convention is kebab-case (e.g., my-protocol)" + fi + + # --- PII / secrets scan --- + if grep -qiE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' "$file" 2>/dev/null; then + # Exclude frontmatter author fields + if grep -v "^author:" "$file" | grep -qiE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' 2>/dev/null; then + warn "possible email address in content (check for PII)" + fi + fi + + if grep -qiE 'sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|AKIA[0-9A-Z]{16}' "$file" 2>/dev/null; then + fail "POSSIBLE API KEY/SECRET detected — do NOT commit this" + fi + + # --- Size check --- + lines=$(wc -l < "$file" | tr -d ' ') + if [[ $lines -gt 300 ]]; then + warn "large file ($lines lines) — consider splitting" + fi + +done + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +echo "" +echo "═══════════════════════════════════════" +if [[ $FAIL -eq 0 && $WARN -eq 0 ]]; then + echo -e " ${GREEN}All clear: $PASS passed, $TOTAL total${NC}" +elif [[ $FAIL -eq 0 ]]; then + echo -e " ${YELLOW}$PASS passed, $WARN warnings, $TOTAL total${NC}" +else + echo -e " ${RED}$PASS passed, $FAIL failed, $WARN warnings, $TOTAL total${NC}" +fi +echo "═══════════════════════════════════════" + +[[ $FAIL -eq 0 ]] && exit 0 || exit 1