feat(skills): add 3 bundled systems/stewardship skills + capacity sti… #177
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: validate | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| structure: | |
| name: Repo structure | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Required top-level files exist | |
| run: | | |
| set -e | |
| for f in README.md CLAUDE.md SETUP.md TOOLS.md INTENT.md USER.md LICENSE NOTICE CHANGELOG.md; do | |
| test -f "$f" || { echo "::error::Missing required file: $f"; exit 1; } | |
| done | |
| echo "OK: all required top-level files present" | |
| - name: Required infra folders + custom/ subfolders exist | |
| run: | | |
| set -e | |
| # Framework-level infra layers (shared resources, not plugin-scoped) | |
| for layer in agents skills hooks mcps templates; do | |
| test -d "$layer" || { echo "::error::Missing infra layer: $layer/"; exit 1; } | |
| test -d "$layer/custom" || { echo "::error::Missing custom/ subfolder: $layer/custom/"; exit 1; } | |
| done | |
| # plugins/ holds Claude Code plugins (the aios slash-command plugin + operator custom/ + company-namespaced) | |
| test -d plugins || { echo "::error::Missing plugins/ folder"; exit 1; } | |
| test -d plugins/custom || { echo "::error::Missing plugins/custom/ for operator-built plugins (their own plugins, not nested inside aios)"; exit 1; } | |
| test -d plugins/aios || { echo "::error::Missing plugins/aios/ — the bundled aios plugin"; exit 1; } | |
| test -d plugins/aios/commands || { echo "::error::Missing plugins/aios/commands/ — the aios plugin's slash commands"; exit 1; } | |
| # NOTE: no plugins/aios/commands/custom/ — operator custom commands go in their own plugin under plugins/custom/<name>/, not nested inside the aios plugin (canonical Claude Code convention) | |
| test ! -d plugins/aios/commands/custom || { echo "::error::plugins/aios/commands/custom/ exists but shouldn't — operator-custom slash commands belong in plugins/custom/<your-plugin>/commands/, not nested inside the bundled aios plugin"; exit 1; } | |
| # Plugin manifest co-location (canonical Claude Code convention) | |
| test -f .claude-plugin/marketplace.json || { echo "::error::Missing .claude-plugin/marketplace.json at repo root — the marketplace manifest must live here, not in commands/"; exit 1; } | |
| test -f plugins/aios/.claude-plugin/plugin.json || { echo "::error::Missing plugins/aios/.claude-plugin/plugin.json — plugin manifest must be co-located with plugin source"; exit 1; } | |
| test ! -f commands/marketplace.json || { echo "::error::commands/marketplace.json exists but shouldn't — manifest moved to .claude-plugin/marketplace.json at repo root (canonical location)"; exit 1; } | |
| test ! -f commands/plugin.json || { echo "::error::commands/plugin.json exists but shouldn't — manifest moved to plugins/aios/.claude-plugin/plugin.json (co-located with plugin source)"; exit 1; } | |
| # No top-level commands/ folder — moved into plugins/aios/commands/ during 2026-05-21 canonical refactor | |
| test ! -d commands || { echo "::error::Top-level commands/ folder exists but shouldn't — slash commands now live at plugins/aios/commands/ as part of the aios plugin"; exit 1; } | |
| echo "OK: framework infra layers + plugins structure intact + manifests co-located" | |
| - name: Framework purity — no company-namespaced folders in infra layers | |
| run: | | |
| set -e | |
| # Principle: framework infra ships only bundled paths + custom/ scaffolds. | |
| # Anything else is company namespace (sovra/, acme/, etc.) — those belong in | |
| # operator vaults under <layer>/<company>/ via /aios:company --sync, or in | |
| # a company-template repo, NEVER in this framework bundle. | |
| # | |
| # Each layer below has an allowlist of bundled folder names. Anything else | |
| # at depth 1 fails the check, with the error pointing at the canonical fix. | |
| FOUND="" | |
| # agents/ — allowed: aios (nested bundles inside) + custom | |
| # Nested layout adopted v0.2.0: agents/aios/{sales,strategy,finance-legal,engineering,communication,personal}/ | |
| for dir in agents/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| aios|custom) ;; | |
| *) FOUND+=$'\n - agents/'"$name"'/ (expected: aios/ or custom/)' ;; | |
| esac | |
| done | |
| # agents/aios/ — allowed bundles: sales, strategy, finance-legal, engineering, communication, personal | |
| for dir in agents/aios/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| sales|strategy|finance-legal|engineering|communication|personal) ;; | |
| *) FOUND+=$'\n - agents/aios/'"$name"'/ (expected: one of: sales, strategy, finance-legal, engineering, communication, personal)' ;; | |
| esac | |
| done | |
| # plugins/ — allowed: aios + custom | |
| for dir in plugins/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| aios|custom) ;; | |
| *) FOUND+=$'\n - plugins/'"$name"'/ (expected: aios/ or custom/)' ;; | |
| esac | |
| done | |
| # hooks/ — allowed: claude-identity + custom (files are layer-level; only subfolders checked) | |
| for dir in hooks/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| claude-identity|custom) ;; | |
| *) FOUND+=$'\n - hooks/'"$name"'/ (expected: claude-identity/ or custom/)' ;; | |
| esac | |
| done | |
| # mcps/ — allowed: *-mcp (suffix convention) + custom | |
| for dir in mcps/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| custom) ;; | |
| *-mcp) ;; | |
| *) FOUND+=$'\n - mcps/'"$name"'/ (expected: *-mcp/ or custom/)' ;; | |
| esac | |
| done | |
| # templates/ — bundled templates live in aios/, operator extensions in custom/ | |
| # (company-distributed templates land in {company}/ via /aios:company --sync). | |
| for dir in templates/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| aios|custom) ;; | |
| *) FOUND+=$'\n - templates/'"$name"'/ (expected: aios/ for bundled templates, custom/ for operator extensions — company namespaces land via /aios:company)' ;; | |
| esac | |
| done | |
| # skills/ — source-grouped. Allowed: aios/, superpowers/, df-claude-skills/, custom/ | |
| # Anything else at depth 1 is either a new upstream source (update this allowlist + skills/_index.md) | |
| # or a company namespace that shouldn't be in the framework. | |
| for dir in skills/*/; do | |
| name=$(basename "$dir") | |
| case "$name" in | |
| aios|anthropic|superpowers|custom) ;; | |
| *) FOUND+=$'\n - skills/'"$name"'/ (expected: aios/, anthropic/, superpowers/, or custom/ — vendoring a new upstream? add it to the allowlist in this workflow + document in skills/_index.md)' ;; | |
| esac | |
| done | |
| # skills/ at depth 2 — block known company prefixes that might sneak in under a source folder | |
| shopt -s nullglob | |
| for source_folder in skills/aios skills/anthropic skills/superpowers; do | |
| for company_prefix in sovra acme chuycepeda chuy zineb sarah; do | |
| for dir in "$source_folder/$company_prefix"/ "$source_folder/$company_prefix-"*/; do | |
| [ -d "$dir" ] && FOUND+=$'\n - '"$dir"' (company-namespaced skill — relocate to operator vault under skills/'"$company_prefix"'/)' | |
| done | |
| done | |
| done | |
| shopt -u nullglob | |
| if [ -n "$FOUND" ]; then | |
| echo "::error::Company-namespaced content found in framework infra layers:" | |
| echo "$FOUND" | |
| echo "" | |
| echo "Framework infra ships only operator-agnostic, bundled content. Company-specific paths belong in:" | |
| echo " - operator vaults under <layer>/<company>/ (synced via /aios:company)" | |
| echo " - or a company-template repo (distributed via /aios:company --sync)" | |
| echo "" | |
| echo "If you're adding a NEW bundled framework path that legitimately doesn't fit the allowlist (e.g., a new agent bundle), update this workflow's allowlist in the same PR." | |
| exit 1 | |
| fi | |
| echo "OK: framework infra layers contain only bundled paths + custom/ scaffolds" | |
| - name: Agent bundles exist with README.md + top-level registry | |
| run: | | |
| set -e | |
| test -f "agents/_index.md" || { echo "::error::Missing canonical registry: agents/_index.md"; exit 1; } | |
| for bundle in sales strategy finance-legal engineering communication personal; do | |
| test -d "agents/aios/$bundle" || { echo "::error::Missing agent bundle: agents/aios/$bundle/"; exit 1; } | |
| test -f "agents/aios/$bundle/README.md" || { echo "::error::Missing bundle README: agents/aios/$bundle/README.md"; exit 1; } | |
| done | |
| echo "OK: all 6 agent bundles present at agents/aios/{bundle}/ with README.md, and agents/_index.md is in place" | |
| manifests: | |
| name: Plugin manifests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: marketplace.json + plugin.json parse as valid JSON | |
| run: | | |
| set -e | |
| python3 -c "import json; json.load(open('.claude-plugin/marketplace.json'))" && echo "OK: marketplace.json" | |
| python3 -c "import json; json.load(open('plugins/aios/.claude-plugin/plugin.json'))" && echo "OK: plugin.json" | |
| - name: Manifest versions match | |
| run: | | |
| set -e | |
| MP_VERSION=$(python3 -c "import json; d=json.load(open('.claude-plugin/marketplace.json')); print(d['plugins'][0]['version'])") | |
| PL_VERSION=$(python3 -c "import json; print(json.load(open('plugins/aios/.claude-plugin/plugin.json'))['version'])") | |
| if [ "$MP_VERSION" != "$PL_VERSION" ]; then | |
| echo "::error::Version mismatch — marketplace.json says $MP_VERSION, plugin.json says $PL_VERSION" | |
| exit 1 | |
| fi | |
| echo "OK: both manifests on version $MP_VERSION" | |
| - name: Manifest names match expected (aios @ the-aios) | |
| run: | | |
| set -e | |
| MP_NAME=$(python3 -c "import json; print(json.load(open('.claude-plugin/marketplace.json'))['name'])") | |
| PL_NAME=$(python3 -c "import json; print(json.load(open('plugins/aios/.claude-plugin/plugin.json'))['name'])") | |
| PLUGIN_NAME=$(python3 -c "import json; print(json.load(open('.claude-plugin/marketplace.json'))['plugins'][0]['name'])") | |
| [ "$MP_NAME" = "the-aios" ] || { echo "::error::marketplace.json name should be 'the-aios', got '$MP_NAME'"; exit 1; } | |
| [ "$PL_NAME" = "aios" ] || { echo "::error::plugin.json name should be 'aios', got '$PL_NAME'"; exit 1; } | |
| [ "$PLUGIN_NAME" = "aios" ] || { echo "::error::marketplace plugin entry should be 'aios', got '$PLUGIN_NAME'"; exit 1; } | |
| echo "OK: manifest naming is aios @ the-aios" | |
| frontmatter: | |
| name: YAML frontmatter | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: All command + agent + skill files have parseable frontmatter | |
| run: | | |
| python3 <<'PY' | |
| import sys, yaml, glob | |
| failures = [] | |
| patterns = [ | |
| "plugins/aios/commands/*.md", | |
| "agents/aios/*/*.md", | |
| "skills/*/*/SKILL.md", | |
| "templates/aios/*.md", | |
| ] | |
| for pat in patterns: | |
| for path in glob.glob(pat): | |
| if path.endswith("_index.md"): | |
| continue | |
| with open(path) as f: | |
| content = f.read() | |
| if not content.startswith("---\n"): | |
| continue | |
| try: | |
| end = content.index("\n---\n", 4) | |
| fm = content[4:end] | |
| yaml.safe_load(fm) | |
| except (ValueError, yaml.YAMLError) as e: | |
| failures.append(f"{path}: {e}") | |
| if failures: | |
| print("::error::Frontmatter parse failures:") | |
| for f in failures: | |
| print(f" - {f}") | |
| sys.exit(1) | |
| total = sum(len(glob.glob(p)) for p in patterns) | |
| print(f"OK: frontmatter validated across {total} files") | |
| PY | |
| personalization: | |
| name: Personalization guard | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: PR adds no personal vault content | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -e | |
| ADDED=$(git diff --name-status "$BASE_SHA".."$HEAD_SHA" | awk '$1=="A"{print $2}') | |
| BAD=$(echo "$ADDED" | grep -E '^vault/' | grep -v '\.gitkeep$' | grep -v '^vault/\.obsidian/' || true) | |
| if [ -n "$BAD" ]; then | |
| echo "::error::PR adds personal vault content. Operator data belongs in your own private vault, not the framework:" | |
| echo "$BAD" | sed 's/^/ - /' | |
| echo "" | |
| echo "Only .gitkeep (folder scaffolding) and vault/.obsidian/* (shared workspace config) are allowed under vault/." | |
| exit 1 | |
| fi | |
| echo "OK: no personal vault content added" | |
| - name: USER.md + INTENT.md still template-shaped (EXAMPLE ONLY markers present) | |
| run: | | |
| set -e | |
| for f in USER.md INTENT.md; do | |
| if ! grep -q "EXAMPLE ONLY" "$f"; then | |
| echo "::error::$f no longer contains 'EXAMPLE ONLY' markers — looks like real personal data was committed. The framework ships templates; operators fill them in their own private vault." | |
| exit 1 | |
| fi | |
| done | |
| echo "OK: USER.md + INTENT.md still template-shaped" | |
| - name: No infra files reference personal-vault notes | |
| run: | | |
| set -e | |
| # Per the 'infra never links to personal' rule: commands/, agents/, hooks/, | |
| # mcps/, plugins/, templates/, skills/, and root docs should never reference | |
| # personal-vault notes like [[ai-os]], [[chuy-*]], [[advisory-*]] — those | |
| # wikilinks resolve only in the author's vault, not in other operators'. | |
| if grep -rln -E '\[\[(ai-os|chuy-|advisory-[a-z]+)\]\]' \ | |
| --include="*.md" --include="*.yml" --include="*.yaml" --include="*.json" \ | |
| --exclude-dir=.git --exclude-dir=.github --exclude-dir=vault --exclude="HISTORY-PRE-*.md" --exclude="CHANGELOG.md" .; then | |
| echo "::error::Infra file references personal-vault notes — those wikilinks won't resolve in other operators' vaults" | |
| exit 1 | |
| fi | |
| echo "OK: no infra→personal-vault wikilinks" | |
| credentials: | |
| name: Credentials guard | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: No credential-shaped files in tree (secrets, tokens, cookies, OAuth, certs, keys) | |
| run: | | |
| set -e | |
| # Pattern-based scan for files that look like real credentials. The framework | |
| # ships only *.template files; real credentials are operator-specific and must | |
| # stay gitignored. This is a path/filename check — content-based secret detection | |
| # is handled by GitHub Push Protection at push time (layered defense). | |
| # | |
| # Categories covered: | |
| # - Cryptographic keys: *.key, *.pem, *.pfx, *.p12, *.cer, *.crt | |
| # - Token/credential files: token.json, credentials.json, *-tokens.json | |
| # - Secret-named JSON: *secret*.json, *credentials*.json, *oauth*.json | |
| # - Environment files: .env, .env.local | |
| # - Browser session state: storage_state.json, cookies.json, **/auth/*.json | |
| # - Spotify/Slack/etc. cache: *spotify*cache*.json, *.slack-tokens.json | |
| # - OAuth callback artifacts: auth_link.html | |
| FOUND=$( | |
| { | |
| find . -type f \( \ | |
| -name "*.key" -o \ | |
| -name "*.pem" -o \ | |
| -name "*.pfx" -o \ | |
| -name "*.p12" -o \ | |
| -name "*.cer" -o \ | |
| -name "*.crt" -o \ | |
| -name "token.json" -o \ | |
| -name "credentials.json" -o \ | |
| -name ".env" -o \ | |
| -name ".env.local" -o \ | |
| -name "storage_state.json" -o \ | |
| -name "cookies.json" -o \ | |
| -name "auth_link.html" -o \ | |
| -name "*-tokens.json" -o \ | |
| -name ".slack-mcp-tokens.json" -o \ | |
| -name "spotify-cache.json" \ | |
| \) -not -path "./.git/*" -not -path "./vault/.obsidian/*" 2>/dev/null | |
| find . -type f -name "*oauth*.json" -not -name "*.template" -not -path "./.git/*" 2>/dev/null | |
| find . -type f -name "*secret*.json" -not -name "*.template" -not -path "./.git/*" 2>/dev/null | |
| find . -type f -name "*credentials*.json" -not -name "*.template" -not -path "./.git/*" 2>/dev/null | |
| find . -type f -path "*/auth/*.json" -not -path "./.git/*" 2>/dev/null | |
| } | sort -u | |
| ) | |
| if [ -n "$FOUND" ]; then | |
| echo "::error::Credential-shaped files found in tree. Only *.template files ship; real credentials must stay gitignored:" | |
| echo "$FOUND" | sed 's/^/ - /' | |
| echo "" | |
| echo "If these are legitimate (e.g., a *.key file that's actually not a credential), add the specific path to an allowlist in this workflow. If they're real credentials: 'git rm --cached <file>', verify .gitignore covers them, rotate the leaked credential immediately, and recommit without the file." | |
| exit 1 | |
| fi | |
| echo "OK: no credential-shaped files in tree" | |
| drift: | |
| name: Migration drift | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: No references to retired vault-commands@local naming | |
| run: | | |
| set -e | |
| if grep -rln "vault-commands@local\|aios@local\b" \ | |
| --include="*.md" --include="*.json" --include="*.yml" --include="*.yaml" \ | |
| --exclude="CHANGELOG.md" --exclude="HISTORY-PRE-*.md" \ | |
| --exclude-dir=.git --exclude-dir=vault --exclude-dir=.github .; then | |
| echo "::error::Retired slug found outside CHANGELOG/history files (see grep output above)" | |
| exit 1 | |
| fi | |
| echo "OK: no stale vault-commands@local or aios@local references" | |
| - name: No references to retired folder paths | |
| run: | | |
| set -e | |
| if grep -rln "vault/06 - agents\|vault/02 - templates" \ | |
| --include="*.md" --include="*.json" --include="*.yml" --include="*.yaml" \ | |
| --exclude="CHANGELOG.md" --exclude="HISTORY-PRE-*.md" \ | |
| --exclude-dir=.git --exclude-dir=vault --exclude-dir=.github .; then | |
| echo "::error::Retired folder path found outside CHANGELOG/history files (see grep output above)" | |
| exit 1 | |
| fi | |
| echo "OK: no stale vault/06 - agents or vault/02 - templates references" | |
| - name: No references to retired ~/obsidian path (canonical install is ~/aios) | |
| run: | | |
| set -e | |
| if grep -rln "~/obsidian\|/Users/.*/obsidian\|\$HOME/obsidian" \ | |
| --include="*.md" --include="*.py" --include="*.sh" --include="*.json" --include="*.yml" --include="*.yaml" \ | |
| --exclude="CHANGELOG.md" --exclude="HISTORY-PRE-*.md" \ | |
| --exclude-dir=.git --exclude-dir=vault --exclude-dir=.github .; then | |
| echo "::error::Retired ~/obsidian path reference found. Canonical install location is ~/aios (framework brand). Update any hardcoded paths in the offending files." | |
| exit 1 | |
| fi | |
| echo "OK: no ~/obsidian hardcoded paths — framework is install-path-agnostic with ~/aios canonical default" | |
| counts: | |
| name: Capability counts | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Doc count claims match bundled ground truth | |
| run: | | |
| set -e | |
| # Ground truth = framework-BUNDLED counts, EXCLUDING custom/ and {company}/ | |
| # namespaces (sovra/, acme/, …). Layered folders count their bundled | |
| # source-peers (skills = aios + anthropic + superpowers); flat folders count | |
| # root entries by convention (mcps = *-mcp/). This mirrors the operating rule: | |
| # "AIOS numbers, not sovra, not custom." Counts are hand-maintained in several | |
| # docs (TOOLS.md source-folders line, README.md) + the site repo (separate, not | |
| # checked here) — this job kills the drift class for the docs in THIS repo. | |
| CMD=$(ls plugins/aios/commands/*.md 2>/dev/null | grep -v _index | wc -l | tr -d ' ') | |
| AGENTS=$(find agents/aios -name '*.md' ! -name '_index.md' ! -name 'README.md' | wc -l | tr -d ' ') | |
| S_AIOS=$(ls -d skills/aios/*/ 2>/dev/null | wc -l | tr -d ' ') | |
| S_ANTH=$(ls -d skills/anthropic/*/ 2>/dev/null | wc -l | tr -d ' ') | |
| S_SUP=$(ls -d skills/superpowers/*/ 2>/dev/null | wc -l | tr -d ' ') | |
| S_TOTAL=$((S_AIOS + S_ANTH + S_SUP)) | |
| MCPS=$(ls -d mcps/*-mcp/ 2>/dev/null | wc -l | tr -d ' ') | |
| echo "Ground truth: commands=$CMD agents=$AGENTS skills=${S_AIOS}+${S_ANTH}+${S_SUP}=${S_TOTAL} mcps=$MCPS" | |
| FAIL=0 | |
| check() { # $1=label $2=expected(truth) $3=claimed $4=where | |
| if [ -z "$3" ]; then | |
| echo "::error::$4: no '$1' count claim found (expected $2) — did the count-bearing sentence get reworded? Update this check's regex or restore the phrasing."; FAIL=1 | |
| elif [ "$3" != "$2" ]; then | |
| echo "::error::$4: '$1' claims $3 but bundled ground truth is $2 — update $4 (or this is your reminder to fix the doc in the same PR that changed the count)"; FAIL=1 | |
| fi | |
| } | |
| # TOOLS.md — skill source-folder counts (anchored on stable source names) | |
| check "aios skills" "$S_AIOS" "$(grep -oE 'AIOS-built[^0-9]+[0-9]+ skills' TOOLS.md | grep -oE '[0-9]+' | head -1)" "TOOLS.md" | |
| check "anthropic skills" "$S_ANTH" "$(grep -oE 'anthropics/skills[^0-9]+[0-9]+ skills' TOOLS.md | grep -oE '[0-9]+' | head -1)" "TOOLS.md" | |
| check "superpowers skills" "$S_SUP" "$(grep -oE 'obra/superpowers[^0-9]+[0-9]+ skills' TOOLS.md | grep -oE '[0-9]+' | head -1)" "TOOLS.md" | |
| check "total bundled" "$S_TOTAL" "$(grep -oE 'Total bundled: [0-9]+' TOOLS.md | grep -oE '[0-9]+' | head -1)" "TOOLS.md" | |
| # README.md — command count | |
| check "commands" "$CMD" "$(grep -oE '\*\*[0-9]+ commands' README.md | grep -oE '[0-9]+' | head -1)" "README.md" | |
| # agents/_index.md — total bundled agents (the registry's own claim) | |
| check "agents (index total)" "$AGENTS" "$(grep -oE 'Total bundled agents: [0-9]+' agents/_index.md | grep -oE '[0-9]+' | head -1)" "agents/_index.md" | |
| # cross-check: the per-bundle Count column should also sum to ground truth | |
| BUNDLE_SUM=$(grep -oE '\*\*`aios/[a-z-]+/`\*\* \| [^|]+ \| [0-9]+' agents/_index.md | grep -oE '[0-9]+$' | paste -sd+ - | bc 2>/dev/null || echo "") | |
| if [ -n "$BUNDLE_SUM" ]; then check "agents (bundle-table sum)" "$AGENTS" "$BUNDLE_SUM" "agents/_index.md"; fi | |
| if [ "$FAIL" = "1" ]; then | |
| echo "" | |
| echo "Capability-count drift detected. The numbers in the docs above no longer match the bundled folders. This is the antifragile guard for the count class (added 2026-05-28 after agents/skills counts silently drifted across TOOLS.md + the site)." | |
| exit 1 | |
| fi | |
| echo "OK: all capability-count claims match bundled ground truth" |