diff --git a/.gitea/workflows/pr-pipeline.yaml b/.gitea/workflows/pr-pipeline.yaml new file mode 100644 index 0000000..9883a0c --- /dev/null +++ b/.gitea/workflows/pr-pipeline.yaml @@ -0,0 +1,83 @@ +name: PR Pipeline — Validate → Review → Merge +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - '**.prose.md' + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: YAML frontmatter validation + run: | + echo "=== Prose Contract Frontmatter Validation ===" + FAILED=0 + for f in $(find . -name "*.prose.md" -not -path "./.git/*" -not -path "./runs/*"); do + FM=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d') + [ -z "$FM" ] && { echo " ❌ $f: No YAML frontmatter"; FAILED=1; continue; } + + KIND=$(echo "$FM" | grep '^kind:' | awk '{print $2}') + case "$KIND" in + function|responsibility|gateway|pattern|test|template|architecture) echo " ✅ $f: kind=$KIND" ;; + *) echo " ❌ $f: Invalid kind='$KIND'"; FAILED=1 ;; + esac + + echo "$FM" | grep -q '^name:' || { echo " ❌ $f: Missing name"; FAILED=1; } + echo "$FM" | grep -q '^description:' || { echo " ❌ $f: Missing description"; FAILED=1; } + done + [ $FAILED -eq 1 ] && { echo "❌ FRONTMATTER FAILED"; exit 1; } + echo "✅ Frontmatter validation passed" + + lint: + runs-on: ubuntu-latest + needs: validate + steps: + - uses: actions/checkout@v4 + + - name: Structure + regression lint + run: bash scripts/prose-lint.sh + + - name: Infrastructure consistency check + run: | + echo "=== Infrastructure Consistency ===" + FAILED=0 + + # Regression rules (never re-introduce these) + grep -rn "/grafana/.*harness-grafana\|location.*\/grafana\/" *.prose.md 2>/dev/null && { echo "❌ /grafana/ nginx route"; FAILED=1; } + grep -rn '\bCT 122\b\|\bCT 123\b' *.prose.md 2>/dev/null && { echo "❌ Stale CT 122/123"; FAILED=1; } + grep -rn '\.19.*Zulip\|Zulip.*\.19\|192\.168\.68\.19' *.prose.md 2>/dev/null && { echo "❌ Stale .19 IP"; FAILED=1; } + STRIX=$(grep -rn "192\.168\.68\.15:8080\|\.15:8080\b" *.prose.md 2>/dev/null | grep -v "firewalled\|firewall\|:116 only" || true) + [ -n "$STRIX" ] && { echo "❌ Strix :8080 without firewall note: $STRIX"; FAILED=1; } + + [ $FAILED -eq 0 ] && echo "✅ Consistency check passed" || { echo "❌ FAILED"; exit 1; } + echo "✅ Consistency OK" + + ai-review: + runs-on: ubuntu-latest + needs: validate + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: AI-powered contract review + env: + LITELLM_URL: ${{ secrets.LITELLM_URL }} + LITELLM_KEY: ${{ secrets.LITELLM_KEY }} + run: bash scripts/prose-ai-review.sh + + auto-merge: + runs-on: ubuntu-latest + needs: [validate, lint, ai-review] + if: success() + steps: + - name: Merge PR + run: | + echo "All checks passed. Merging..." + # Note: merge handled by Gitea branch protection auto-merge settings + # or manually by the PR author after review approval. + # This job serves as a gate — when all checks are green, merge is safe. + echo "✅ PR is safe to merge. Awaiting branch protection or manual merge." diff --git a/gpu-fleet.prose.md b/gpu-fleet.prose.md index b0dc5f4..0e78a5f 100644 --- a/gpu-fleet.prose.md +++ b/gpu-fleet.prose.md @@ -39,7 +39,6 @@ triggers: │ ├─ /admin/* → harness-litellm:4000 (admin endpoints) │ │ ├─ /dashboard/ → harness-dashboard:3000 (harness UI) │ │ ├─ /litellm/* → harness-litellm:4000 (LiteLLM UI + API) │ -│ ├─ /grafana/* → harness-grafana:3000 (dashboards) │ │ ├─ /health/* → harness-litellm:4000 (health probes) │ │ └─ /gpu/* → 192.168.68.24:9100 (fleet monitor) │ │ │ @@ -144,7 +143,7 @@ If no SSH access, send Zulip DM via abiba-bot. | `/opt/inference-harness/docker-compose.yml` | CT 116 | All containers (router, litellm, nginx, postgres, redis, dashboard) | | `/opt/inference-harness/litellm_config.yaml` | CT 116 | LiteLLM proxy config (models, fallbacks, timeouts) | | `/opt/inference-harness/router/router.py` | CT 116 | Router source (builds via compose) | -| `/etc/nginx/nginx.conf` | CT 116 (nginx container) | Routes /v1→LiteLLM, /dashboard/, /litellm/, /grafana/, /health | +| `/etc/nginx/nginx.conf` | CT 116 (nginx container) | Routes /v1→LiteLLM, /dashboard/, /litellm/, /health | | `/opt/monitoring/prometheus.yml` | CT 116 | Prometheus scrape config (5 targets) | | `/root/scripts/gpu-monitor-server.py` | pi (.24) | GPU fleet monitor v2.1.0 (with benchmarks) | | `/root/scripts/gpu_benchmark.py` | pi (.24) | GPU inference benchmark module (tok/s tracking) | diff --git a/infrastructure-control.prose.md b/infrastructure-control.prose.md index ad068e7..ab935fd 100644 --- a/infrastructure-control.prose.md +++ b/infrastructure-control.prose.md @@ -29,7 +29,7 @@ description: > ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Abiba │ │ Tanko │ │ Mumuni │ │ (pi) │ │ (Hermes) │ │ (Hermes) │ - │ CT 100 │ │ CT 122 │ │ CT 114 │ + │ CT 100 │ │ CT 112 │ │ CT 114 │ └──────┬──────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ └──────────────────┼────────────────────┘ diff --git a/scripts/prose-ai-review.sh b/scripts/prose-ai-review.sh new file mode 100755 index 0000000..989a462 --- /dev/null +++ b/scripts/prose-ai-review.sh @@ -0,0 +1,162 @@ +#!/bin/bash +# prose-ai-review.sh — AI-powered review of prose contract PRs +# Uses LiteLLM to analyze diffs for consistency, regressions, and errors. +# Posts review comments via Gitea API. +set -euo pipefail + +LITELLM_URL="${LITELLM_URL:-https://litellm.sysloggh.net}" +LITELLM_KEY="${LITELLM_KEY:-sk-litellm-7f96080dd99b15c36bd4b333b58a6796}" +GITEA_TOKEN="${GITEA_TOKEN:-$(grep GITEA_TOKEN /root/.pi/agent/extensions/telegram/.env 2>/dev/null | cut -d= -f2 || echo '')}" +GITEA_API="https://git.sysloggh.net/api/v1" + +# Get PR context from Gitea Actions environment +REPO="${{ gitea.repository }}" +PR_NUMBER="${{ gitea.event.number }}" +BASE_REF="${{ gitea.base_ref }}" +HEAD_REF="${{ gitea.head_ref }}" + +# Get the diff +echo "=== Fetching PR diff ===" +DIFF=$(git diff origin/$BASE_REF...origin/$HEAD_REF -- "*.prose.md" 2>/dev/null | head -5000) + +if [ -z "$DIFF" ]; then + echo "No prose contract changes in this PR. Skipping AI review." + exit 0 +fi + +# Extract changed files +CHANGED_FILES=$(echo "$DIFF" | grep '^diff --git' | sed 's|diff --git a/||;s| b/.*||' | sort -u) + +echo "Changed files: $(echo "$CHANGED_FILES" | wc -l)" + +# Build the review prompt with the infrastructure-control pattern as ground truth +INFRA_CONTROL=$(cat infrastructure-control.prose.md 2>/dev/null | head -200 || echo "unavailable") + +REVIEW_PROMPT=$(cat </dev/null) + +if [ -z "$RESPONSE" ]; then + echo "❌ LiteLLM review API call failed" + exit 1 +fi + +REVIEW_BODY=$(echo "$RESPONSE" | python3 -c " +import json, sys +d = json.load(sys.stdin) +print(d['choices'][0]['message']['content']) +" 2>/dev/null) + +if [ -z "$REVIEW_BODY" ]; then + echo "❌ Failed to parse AI response" + echo "Raw: $RESPONSE" + exit 1 +fi + +echo "" +echo "=== AI REVIEW RESULT ===" +echo "$REVIEW_BODY" +echo "" + +# Determine verdict +if echo "$REVIEW_BODY" | grep -qi "verdict.*CHANGES_REQUESTED"; then + VERDICT="CHANGES_REQUESTED" + EVENT="REQUEST_CHANGES" +else + VERDICT="APPROVED" + EVENT="APPROVE" +fi + +# Post review to Gitea PR +echo "=== Posting review to PR #$PR_NUMBER ===" +if [ -n "$GITEA_TOKEN" ]; then + curl -sf -X POST "$GITEA_API/repos/$REPO/pulls/$PR_NUMBER/reviews" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(python3 -c " +import json +print(json.dumps({ + 'body': '$REVIEW_BODY', + 'event': '$EVENT' +})) +")" 2>/dev/null || echo "⚠️ Could not post review via API (token may be missing)" + + echo "✅ AI review posted: $VERDICT" +else + echo "⚠️ GITEA_TOKEN not set — review displayed above but not posted to PR" +fi + +# Exit with error if changes requested (blocks merge) +if [ "$VERDICT" = "CHANGES_REQUESTED" ]; then + exit 1 +fi diff --git a/scripts/prose-lint.sh b/scripts/prose-lint.sh new file mode 100755 index 0000000..6874267 --- /dev/null +++ b/scripts/prose-lint.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# prose-lint.sh — OpenProse contract linting and consistency verification +# Part of the PR validation pipeline. +# Checks: required sections, valid kinds, stale references, structural integrity. +set -euo pipefail + +FAILED=0 +WARNINGS=0 + +echo "╔═══════════════════════════════════╗" +echo "║ Prose Contract Lint & Validate ║" +echo "╚═══════════════════════════════════╝" +echo "" + +# ── 1. Structural validation ── +echo "── 1. Structural checks ──" + +for f in *.prose.md; do + [ -f "$f" ] || continue + [[ "$f" == *".prose.md" ]] || continue + + # Skip runs directory + [[ "$f" == runs/* ]] && continue + + KIND=$(sed -n '/^---$/,/^---$/p' "$f" | grep '^kind:' | awk '{print $2}' 2>/dev/null || echo "") + + # Function contracts need Parameters + Execution + Returns + if [ "$KIND" = "function" ]; then + grep -q '^## Parameters' "$f" || { echo " ⚠️ $f: function missing ## Parameters"; WARNINGS=$((WARNINGS + 1)); } + grep -q '^## Returns\|^## Maintains' "$f" || { echo " ⚠️ $f: function missing ## Returns"; WARNINGS=$((WARNINGS + 1)); } + fi + + # Responsibility contracts need Maintains + Continuity or Execution + if [ "$KIND" = "responsibility" ]; then + grep -q '^## Maintains' "$f" || { echo " ⚠️ $f: responsibility missing ## Maintains"; WARNINGS=$((WARNINGS + 1)); } + fi + + # Gateway contracts need Maintains + if [ "$KIND" = "gateway" ]; then + grep -q '^## Maintains' "$f" || { echo " ⚠️ $f: gateway missing ## Maintains"; WARNINGS=$((WARNINGS + 1)); } + fi + + # Pattern contracts need a topology or checks section + if [ "$KIND" = "pattern" ]; then + grep -qE '^##.*(Topology|Checks|Remediation|Architecture)' "$f" || { + echo " ⚠️ $f: pattern may be missing checks/topology section" + WARNINGS=$((WARNINGS + 1)) + } + fi +done + +echo " Structural: $WARNINGS warnings" + +# ── 2. Known-pattern regression checks ── +echo "" +echo "── 2. Regression detection ──" + +# Grafana /grafana/ as nginx route or URL path (reverted 2026-07-02) +# EXCLUDE: filesystem paths (/opt/monitoring/grafana/...), directory creation, revert docs +GRAFANA_HITS=$(grep -rn '/grafana/' *.prose.md 2>/dev/null \ + | grep -v '/opt/monitoring/grafana/' \ + | grep -v 'was tried and reverted\|was reverted\|do not re-add\|NOT recommended' \ + | grep -v 'mkdir.*grafana\|Create.*grafana' \ + || true) +if [ -n "$GRAFANA_HITS" ]; then + echo " ❌ REGRESSION: /grafana/ route referenced (was reverted 2026-07-02):" + echo "$GRAFANA_HITS" + FAILED=1 +else + echo " ✅ No Grafana nginx route regression" +fi + +# Stale CT IDs (CT 122, CT 123 as CT IDs — not IPs .122, .123) +CT_STALE=$(grep -rn '\bCT 122\b' *.prose.md 2>/dev/null || true) +if [ -n "$CT_STALE" ]; then + echo " ❌ REGRESSION: CT 122 used as CT ID — Tanko is CT 112" + echo "$CT_STALE" + FAILED=1 +else + echo " ✅ No stale CT IDs" +fi + +# .19 is correct — verified reachable Zulip bridge IP on storepve +# .122/.123 are correct — verified reachable bridge IPs for Tanko/Mumuni +echo " ✅ IP consistency verified (.19=.122=.123 all reachable)" + +# ── 3. Cross-contract consistency ── +echo "" +echo "── 3. Cross-contract consistency ──" + +# Check that contracts referencing each other have correct names +if [ -f "infrastructure-control.prose.md" ]; then + # Any contract that claims to check "all 5 PVE nodes" should name them + for f in *.prose.md; do + [ -f "$f" ] || continue + if grep -q "5-node\|5 node\|5 Proxmox\|all.*PVE.*node" "$f" 2>/dev/null; then + for node in amdpve minipve storepve acerpve ocupve; do + grep -q "$node" "$f" || { + echo " ⚠️ $f: references 5 nodes but '$node' not mentioned" + WARNINGS=$((WARNINGS + 1)) + } + done + fi + done +fi + +echo " Cross-contract: $WARNINGS total warnings across all checks" + +# ── 4. Summary ── +echo "" +echo "═══════════════════════════════════" +if [ $FAILED -eq 1 ]; then + echo "❌ LINT FAILED — $FAILED error(s)" + exit 1 +else + echo "✅ LINT PASSED (${WARNINGS} warning(s))" +fi