| name | Config Validator |
| category | meta |
| description | Validate aeon.yml and .github/workflows/aeon.yml for structural invariants that have caused past outages — checkout step ordering, duplicate skill keys, missing skill files |
| tags | ["meta","dev"] |
Today is ${today}. Your task is to validate the structural correctness of aeon.yml and .github/workflows/aeon.yml.
This skill exists because two incident classes have caused major outages:
- Checkout-ordering class:
Early checkout step missing or conditionally gated — can cause every skill to fail in a single run.
- Duplicate-key class: duplicate YAML keys in
aeon.yml — silently disable any skill whose key is shadowed.
The same checks also run as a pre-merge CI workflow at .github/workflows/ci-config-validate.yml (via scripts/validate-config.js) — that workflow blocks PRs that break these invariants. This skill is the weekly safety net for state that drifts on main outside of PRs (manual edits, scheduled rewrites, post-process commits). Run all checks. Report findings. Alert if any fail.
Fast path — invoke the shared validator
The fastest, most consistent way to run all three checks is the shared script the CI workflow uses:
node scripts/validate-config.js
Exit code 0 = CLEAN (no notification needed). Non-zero exit + FAIL[*]: lines on stdout = ISSUES (skip to step 4).
If the script is unavailable for any reason, fall back to the manual checks in steps 1–3.
Steps
1. Check workflow step ordering (checkout-ordering class)
Read .github/workflows/aeon.yml.
Find the jobs.run.steps array. Verify:
a. A step named Early checkout (or uses: actions/checkout) exists.
b. That step has no if: condition — it must run unconditionally.
c. That step is positioned before any step that has an if: condition.
Use node inline to parse and check:
node -e "
const fs = require('fs');
const text = fs.readFileSync('.github/workflows/aeon.yml', 'utf8');
const lines = text.split('\n');
let inSteps = false, stepDepth = 0;
let steps = [], cur = null, lineNum = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (/^\s{4,6}steps:/.test(line)) { inSteps = true; continue; }
if (!inSteps) continue;
if (/^\s{4,6}[a-z]/.test(line) && !/^\s{6,}/.test(line) && i > 0) { inSteps = false; continue; }
if (/^\s{6}- /.test(line)) {
if (cur) steps.push(cur);
cur = { lineNum: i + 1, name: null, hasIf: false, isCheckout: false };
}
if (cur) {
if (/name:/.test(trimmed)) cur.name = trimmed.replace(/^name:\s*/, '').replace(/[\"']/g, '');
if (/uses:\s*actions\/checkout/.test(trimmed)) cur.isCheckout = true;
if (/Early checkout/.test(trimmed)) cur.isCheckout = true;
if (/^\s{6}if:/.test(line)) cur.hasIf = true;
}
}
if (cur) steps.push(cur);
let issues = [];
const checkoutIdx = steps.findIndex(s => s.isCheckout);
if (checkoutIdx === -1) {
issues.push('FAIL: No checkout step (actions/checkout or Early checkout) found in jobs.run.steps');
} else {
const cs = steps[checkoutIdx];
if (cs.hasIf) {
issues.push('FAIL: Checkout step at line ' + cs.lineNum + ' has an if: condition — must be unconditional');
}
const firstConditional = steps.findIndex(s => s.hasIf);
if (firstConditional !== -1 && firstConditional < checkoutIdx) {
issues.push('FAIL: Checkout step appears after a conditional step at line ' + steps[firstConditional].lineNum);
}
if (issues.length === 0) {
console.log('PASS checkout: Early checkout is unconditional and first (line ' + cs.lineNum + ')');
}
}
if (issues.length > 0) { issues.forEach(i => console.log(i)); process.exit(1); }
"
If the check fails, record the finding. Continue to next check regardless.
2. Check for duplicate skill keys (duplicate-key class)
Read aeon.yml. Scan the skills: block for duplicate top-level keys.
node -e "
const fs = require('fs');
const text = fs.readFileSync('aeon.yml', 'utf8');
const lines = text.split('\n');
let inSkills = false;
const seen = {};
const dupes = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/^skills:/.test(line)) { inSkills = true; continue; }
if (inSkills && /^[a-z]/.test(line)) { inSkills = false; continue; }
if (!inSkills) continue;
const match = line.match(/^ ([a-z][a-z0-9-]+):/);
if (match) {
const key = match[1];
if (seen[key]) {
dupes.push('FAIL: Duplicate skill key \"' + key + '\" at line ' + (i+1) + ' (first seen line ' + seen[key] + ')');
} else {
seen[key] = i + 1;
}
}
}
if (dupes.length > 0) { dupes.forEach(d => console.log(d)); process.exit(1); }
else { console.log('PASS duplicates: no duplicate skill keys found (' + Object.keys(seen).length + ' skills)'); }
"
If duplicates are found, record them. Continue to next check regardless.
3. Check all enabled skills have SKILL.md (missing-file class)
Read aeon.yml. For every skill with enabled: true, verify skills/<name>/SKILL.md exists.
node -e "
const fs = require('fs');
const text = fs.readFileSync('aeon.yml', 'utf8');
const lines = text.split('\n');
let inSkills = false;
const issues = [];
const ok = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/^skills:/.test(line)) { inSkills = true; continue; }
if (inSkills && /^[a-z]/.test(line)) { inSkills = false; continue; }
if (!inSkills) continue;
const match = line.match(/^ ([a-z][a-z0-9-]+):\s*\{(.+)\}/);
if (match) {
const name = match[1];
const props = match[2];
if (/enabled:\s*true/.test(props)) {
const skillFile = 'skills/' + name + '/SKILL.md';
if (!fs.existsSync(skillFile)) {
issues.push('WARN: enabled skill \"' + name + '\" has no SKILL.md at ' + skillFile);
} else {
ok.push(name);
}
}
}
}
if (issues.length > 0) { issues.forEach(i => console.log(i)); }
console.log('PASS skill-files: ' + ok.length + ' enabled skills have SKILL.md' + (issues.length > 0 ? ', ' + issues.length + ' missing' : ''));
if (issues.length > 0) process.exit(1);
"
4. Summarize findings
After running all three checks, collect results:
- Count PASSes and FAILs
- If all checks passed: status = CLEAN
- If any FAIL or WARN: status = ISSUES
5. Decide whether to notify
- CLEAN: Log only, no notification. Silent runs are expected — the value is the alert when something breaks.
- ISSUES: Send notification via
./notify.
If ISSUES, write notification to .pending-notify-temp/config-validator-${today}.md then send with ./notify -f:
*Config Validator — ${today}*
STATUS: ISSUES FOUND
[list each finding, one per line]
These invariants have caused full outages before.
Check aeon.yml and .github/workflows/aeon.yml immediately.
log: memory/logs/${today}.md
6. Log results
Append to memory/logs/${today}.md:
## Config Validator
- **Status:** CLEAN / ISSUES
- **Checkout step:** PASS / FAIL — [detail]
- **Duplicate keys:** PASS / FAIL — [detail]
- **Skill files:** PASS / N warnings — [detail]
- **Notification:** sent / skipped (clean)
Sandbox Note
All checks use local file reads only — no external network calls needed. No prefetch/postprocess wrapper required.
Environment Variables Required
None — reads only local files.