docs(v-onboard --refresh): refresh architecture KB against post-2.14 … #47
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 Plugin | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| validate: | |
| name: Validate plugin structure | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install jq | |
| run: sudo apt-get install -y jq | |
| - name: Validate plugin.json is valid JSON | |
| run: jq empty .claude-plugin/plugin.json | |
| - name: Validate marketplace.json is valid JSON | |
| run: jq empty .claude-plugin/marketplace.json | |
| - name: Validate hooks.json is valid JSON | |
| run: jq empty hooks/hooks.json | |
| - name: Validate job_result.schema.json is valid JSON | |
| run: jq empty schemas/job_result.schema.json | |
| - name: Verify required plugin.json fields | |
| run: | | |
| set -e | |
| for field in name description version author license; do | |
| value=$(jq -r ".$field // \"\"" .claude-plugin/plugin.json) | |
| if [ -z "$value" ] || [ "$value" = "null" ]; then | |
| echo "❌ plugin.json missing required field: $field" | |
| exit 1 | |
| fi | |
| echo "✅ plugin.json.$field = $value" | |
| done | |
| - name: Verify plugin.json and marketplace.json versions match | |
| run: | | |
| set -e | |
| pv=$(jq -r '.version' .claude-plugin/plugin.json) | |
| mv=$(jq -r '.plugins[] | select(.name=="superpowers-v") | .version' .claude-plugin/marketplace.json) | |
| if [ "$pv" != "$mv" ]; then | |
| echo "❌ version mismatch: plugin.json=$pv marketplace.json=$mv" | |
| exit 1 | |
| fi | |
| echo "✅ versions in lockstep: $pv" | |
| - name: Verify CHANGELOG top version matches plugin.json (lockstep guard) | |
| run: | | |
| PLUGIN_VERSION=$(jq -r .version .claude-plugin/plugin.json) | |
| # First release heading OUTSIDE fenced code blocks; [Unreleased] (non-numeric) is skipped. | |
| # Fences per CommonMark: ``` or ~~~ (≤3 leading spaces); closer = same char, run ≥ opener's. | |
| CHANGELOG_VERSION=$(awk ' | |
| NR==1 { sub(/^\xef\xbb\xbf/, "") } | |
| /^ {0,3}(```|~~~)/ { | |
| match($0, /(`+|~+)/); c = substr($0, RSTART, 1); len = RLENGTH | |
| rest = substr($0, RSTART + RLENGTH) | |
| if (!fence) { | |
| # a backtick opener whose info string contains a backtick is NOT a fence (CommonMark) | |
| if (!(c == "`" && rest ~ /`/)) { fence = 1; fc = c; flen = len } | |
| } else if (c == fc && len >= flen && rest ~ /^[ \t]*$/) { fence = 0 } | |
| next | |
| } | |
| !fence && /^ {0,3}##[ \t]+\[[0-9]+\.[0-9]+\.[0-9]+\]/ { | |
| match($0, /\[[0-9]+\.[0-9]+\.[0-9]+\]/); print substr($0, RSTART+1, RLENGTH-2); exit | |
| } | |
| ' CHANGELOG.md) | |
| if [ -z "$CHANGELOG_VERSION" ]; then | |
| echo "::error::No release heading '## [x.y.z]' found in CHANGELOG.md (outside code fences)." | |
| exit 1 | |
| fi | |
| if [ "$PLUGIN_VERSION" != "$CHANGELOG_VERSION" ]; then | |
| echo "::error::CHANGELOG top entry ($CHANGELOG_VERSION) != plugin.json version ($PLUGIN_VERSION). Bump plugin.json, marketplace.json AND the CHANGELOG heading together." | |
| exit 1 | |
| fi | |
| - name: Verify agent files have required frontmatter | |
| run: | | |
| set -e | |
| for agent in agents/*.md; do | |
| echo "Checking $agent..." | |
| # Frontmatter must have name and description | |
| head -20 "$agent" | grep -q "^name:" || { echo "❌ $agent missing 'name' in frontmatter"; exit 1; } | |
| head -20 "$agent" | grep -q "^description:" || { echo "❌ $agent missing 'description' in frontmatter"; exit 1; } | |
| # Per project policy: no Haiku | |
| if head -20 "$agent" | grep -qi "^model:.*haiku"; then | |
| echo "❌ $agent specifies Haiku — project policy forbids Haiku" | |
| exit 1 | |
| fi | |
| echo "✅ $agent OK" | |
| done | |
| - name: Verify SKILL.md frontmatter | |
| run: | | |
| set -e | |
| for skill in skills/*/SKILL.md; do | |
| echo "Checking $skill..." | |
| head -20 "$skill" | grep -q "^name:" || { echo "❌ $skill missing 'name'"; exit 1; } | |
| head -20 "$skill" | grep -q "^description:" || { echo "❌ $skill missing 'description'"; exit 1; } | |
| echo "✅ $skill OK" | |
| done | |
| - name: Set up Python for linters and validators | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: Run frontmatter linter (description length, missing fields, no-Haiku policy) | |
| run: python3 scripts/lint-frontmatter.py . | |
| - name: Validate example manifest against manifest invariants | |
| run: | | |
| set -e | |
| # The deterministic manifest validator is built in Batch A. Run it against | |
| # the committed example fixture so CI exercises the gate on real data. | |
| if [ -f scripts/compound-v-validate-manifest.py ]; then | |
| python3 scripts/compound-v-validate-manifest.py examples/manifest.example.yaml | |
| echo "✅ example manifest passes the invariant gate" | |
| else | |
| echo "⚠️ scripts/compound-v-validate-manifest.py not present yet (Batch A) — skipping" | |
| fi | |
| - name: Validate example job_result against the schema | |
| run: | | |
| set -e | |
| # Prefer the project's own collector validator when present; otherwise fall | |
| # back to a stdlib jsonschema-style check via a tiny inline Python guard. | |
| python3 - <<'PY' | |
| import json, sys | |
| schema = json.load(open("schemas/job_result.schema.json")) | |
| inst = json.load(open("examples/job_result.example.json")) | |
| req = schema.get("required", []) | |
| props = schema.get("properties", {}) | |
| missing = [k for k in req if k not in inst] | |
| if missing: | |
| print("❌ example job_result missing required keys:", missing); sys.exit(1) | |
| if schema.get("additionalProperties") is False: | |
| extra = [k for k in inst if k not in props] | |
| if extra: | |
| print("❌ example job_result has keys not in schema:", extra); sys.exit(1) | |
| enum = props.get("status", {}).get("enum") | |
| if enum and inst.get("status") not in enum: | |
| print("❌ status not in enum:", inst.get("status")); sys.exit(1) | |
| print("✅ example job_result conforms to job_result.schema.json") | |
| PY | |
| - name: No fabricated cost/token metrics (anti-ruflo gate) | |
| run: | | |
| set -e | |
| # The anti-ruflo charter forbids printing token-cost numbers we cannot measure. | |
| # Guard scripts/ and docs/ for fabricated metric output. We match phrases that | |
| # would only appear if code/docs CLAIMED measured savings (e.g. "tokens saved", | |
| # "cost savings: N", a hardcoded "baseline = 1000"). Pure mentions of the | |
| # anti-pattern (the word "anti-ruflo", "fabricated", "do not print") are allowed. | |
| fail=0 | |
| patterns='tokens? saved|token-cost (saved|savings)|cost savings:|saved [0-9]+ tokens|baseline ?= ?1000|\$[0-9]+\.[0-9]+ saved' | |
| while IFS= read -r -d '' f; do | |
| # Skip this workflow file itself (it names the patterns) and the PRD/plan, | |
| # which discuss the anti-pattern by name. | |
| case "$f" in | |
| *docs/superpowers/specs/*|*docs/superpowers/plans/*) continue ;; | |
| esac | |
| if grep -inE "$patterns" "$f" >/dev/null 2>&1; then | |
| echo "❌ possible fabricated cost/token metric in $f:" | |
| grep -inE "$patterns" "$f" | |
| fail=1 | |
| fi | |
| done < <(find scripts docs -type f \( -name "*.sh" -o -name "*.py" -o -name "*.md" \) -print0 2>/dev/null) | |
| [ "$fail" = "0" ] || exit 1 | |
| echo "✅ no fabricated cost/token metrics in scripts/ or docs/" | |
| - name: Verify hook scripts are executable | |
| run: | | |
| set -e | |
| for hook in hooks/*.sh; do | |
| if [ ! -x "$hook" ]; then | |
| echo "❌ $hook is not executable (run: chmod +x $hook)" | |
| exit 1 | |
| fi | |
| echo "✅ $hook is executable" | |
| done | |
| - name: Lint hook scripts with shellcheck | |
| run: | | |
| sudo apt-get install -y shellcheck | |
| shellcheck hooks/*.sh | |
| # FINAL step — run the repo-wide dead-link scan only after every file exists, | |
| # so cross-refs to files authored by later batches resolve at integration time. | |
| - name: Check for dead intra-plugin cross-refs | |
| run: | | |
| set -e | |
| # Find every (path).md link and verify the target exists | |
| # Allow external links and absolute /docs/ links | |
| fail=0 | |
| while IFS= read -r -d '' file; do | |
| grep -oE '\]\([^)]+\.md[^)]*\)' "$file" 2>/dev/null | sed 's/^](//;s/)$//' | while read -r link; do | |
| # Strip anchor | |
| path="${link%%#*}" | |
| # Skip URLs and absolute /docs paths | |
| case "$path" in | |
| http*|/docs/*|""|"#"*) continue ;; | |
| esac | |
| # Resolve relative to the source file's dir | |
| dir=$(dirname "$file") | |
| target="$dir/$path" | |
| if [ ! -f "$target" ]; then | |
| echo "❌ Dead link in $file → $path (resolved: $target)" | |
| fail=1 | |
| fi | |
| done | |
| done < <(find . -name "*.md" -not -path "./node_modules/*" -not -path "./.git/*" -print0) | |
| [ "$fail" = "0" ] || exit 1 | |
| echo "✅ All intra-plugin .md links resolve" | |
| # Marathon-mode (v2.10) selftests run under Python 3.9 — the documented floor for | |
| # scripts/compound-v-epic-state.py and scripts/compound-v-epic-arbiter.py (both pure | |
| # stdlib, no PyYAML dependency). Installed LAST in the job so re-pointing PATH at a | |
| # 3.9 interpreter can never affect the earlier PyYAML-dependent steps above. | |
| - name: Set up Python 3.9 (marathon selftest floor) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.9' | |
| - name: Run epic-state.py selftest (Python 3.9 floor) | |
| run: python3 scripts/compound-v-epic-state.py --selftest | |
| - name: Run epic-arbiter.py selftest (Python 3.9 floor) | |
| run: python3 scripts/compound-v-epic-arbiter.py --selftest | |
| - name: Run epic-watch.py selftest (Python 3.9 floor) | |
| run: python3 scripts/compound-v-epic-watch.py --selftest | |
| - name: Run headless-shim.py selftest (Python 3.9 floor) | |
| run: python3 scripts/compound-v-headless-shim.py --selftest |