Skip to content

docs(v-onboard --refresh): refresh architecture KB against post-2.14 … #47

docs(v-onboard --refresh): refresh architecture KB against post-2.14 …

docs(v-onboard --refresh): refresh architecture KB against post-2.14 … #47

Workflow file for this run

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