-
Notifications
You must be signed in to change notification settings - Fork 12
452 lines (408 loc) · 23.8 KB
/
Copy pathvalidate.yml
File metadata and controls
452 lines (408 loc) · 23.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
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"