Skip to content

feat(skills): add 3 bundled systems/stewardship skills + capacity sti… #177

feat(skills): add 3 bundled systems/stewardship skills + capacity sti…

feat(skills): add 3 bundled systems/stewardship skills + capacity sti… #177

Workflow file for this run

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"