Skip to content

Commit b83ea7c

Browse files
hyperpolymathclaude
andcommitted
chore: sync local changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8378a43 commit b83ea7c

14 files changed

Lines changed: 1295 additions & 7 deletions

File tree

docs/BRANCH-PROTECTION-SETUP.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Branch Protection Setup: Static Analysis Gate
2+
3+
> SPDX-License-Identifier: PMPL-1.0-or-later
4+
5+
This document explains how to wire the `static-analysis-gate.yml` workflow into
6+
GitHub branch protection so that every PR must pass panic-attack and hypatia
7+
before merging.
8+
9+
---
10+
11+
## Required Status Checks
12+
13+
In your repository's **Settings > Branches > Branch protection rules** for
14+
`main` (and/or `master`), enable:
15+
16+
| Status check name | Source workflow |
17+
|-------------------------------------------|-----------------------------|
18+
| `panic-attack assail` | `static-analysis-gate.yml` |
19+
| `Hypatia neurosymbolic scan` | `static-analysis-gate.yml` |
20+
| `Deposit findings for gitbot-fleet` | `static-analysis-gate.yml` |
21+
22+
### Recommended branch protection settings
23+
24+
- **Require status checks to pass before merging** — enabled
25+
- **Require branches to be up to date before merging** — enabled
26+
- **Include administrators** — enabled (lead by example)
27+
- **Restrict who can push to matching branches** — optional, recommended
28+
29+
> **Note:** The `Hypatia Security Scan` check from the older `hypatia-scan.yml`
30+
> workflow is a *separate* status check. You may keep both or retire the older
31+
> one; `static-analysis-gate.yml` is the unified replacement.
32+
33+
---
34+
35+
## Enabling the Workflow in Any Repo
36+
37+
### From the RSR template
38+
39+
Repos bootstrapped from `rsr-template-repo` already include
40+
`.github/workflows/static-analysis-gate.yml`. Replace `{{OWNER}}` with your
41+
GitHub org/user name:
42+
43+
```bash
44+
sed -i 's/{{OWNER}}/hyperpolymath/g' .github/workflows/static-analysis-gate.yml
45+
```
46+
47+
### Adding to an existing repo
48+
49+
Copy the workflow file into the target repo:
50+
51+
```bash
52+
cp /path/to/rsr-template-repo/.github/workflows/static-analysis-gate.yml \
53+
your-repo/.github/workflows/static-analysis-gate.yml
54+
55+
cd your-repo
56+
sed -i 's/{{OWNER}}/hyperpolymath/g' .github/workflows/static-analysis-gate.yml
57+
git add .github/workflows/static-analysis-gate.yml
58+
git commit -m "ci: add static analysis gate for branch protection"
59+
git push
60+
```
61+
62+
Then add the status checks in **Settings > Branches** as described above.
63+
64+
---
65+
66+
## Local Pre-Push Hook
67+
68+
A local hook mirrors the CI gate so developers catch critical findings before
69+
pushing. Install it from `gitbot-fleet/hooks/pre-push-gate.sh`:
70+
71+
```bash
72+
# Symlink (recommended — always picks up updates)
73+
ln -sf ~/Documents/hyperpolymath-repos/gitbot-fleet/hooks/pre-push-gate.sh \
74+
.git/hooks/pre-push
75+
76+
# Or copy
77+
cp ~/Documents/hyperpolymath-repos/gitbot-fleet/hooks/pre-push-gate.sh \
78+
.git/hooks/pre-push
79+
chmod +x .git/hooks/pre-push
80+
```
81+
82+
The hook gracefully degrades: if neither panic-attack nor hypatia is installed
83+
locally, it prints a notice and allows the push (CI will catch it).
84+
85+
---
86+
87+
## How Findings Flow Back to Hypatia for Learning
88+
89+
```
90+
Developer pushes / opens PR
91+
|
92+
v
93+
static-analysis-gate.yml runs
94+
|
95+
+---> panic-attack assail ---> findings JSON artifact
96+
+---> hypatia scan ---> findings JSON artifact
97+
|
98+
v
99+
deposit-findings job
100+
|
101+
+---> Combines both into unified-findings.json
102+
+---> Uploads as "unified-findings" artifact (90-day retention)
103+
|
104+
v
105+
gitbot-fleet scanner (scheduled)
106+
|
107+
+---> Queries GitHub API for repos with unified-findings artifacts
108+
+---> Downloads and ingests into hypatia's learning corpus
109+
+---> Feeds rhodibot / echidnabot / sustainabot for pattern recognition
110+
|
111+
v
112+
Hypatia learning engine
113+
|
114+
+---> Updates neurosymbolic rules based on recurring patterns
115+
+---> Feeds improved rules back into hypatia-cli.sh
116+
+---> Cycle repeats with better detection on next scan
117+
```
118+
119+
### Artifact-based ingestion
120+
121+
The `deposit-findings` job uploads a `unified-findings` artifact to each
122+
workflow run. The gitbot-fleet's `learning-monitor.sh` script periodically:
123+
124+
1. Lists recent workflow runs across enrolled repos.
125+
2. Downloads `unified-findings` artifacts.
126+
3. Parses the JSON envelope (`schema_version`, `repository`, `commit_sha`,
127+
`timestamp`, `findings[]`).
128+
4. Deduplicates findings already in the learning corpus.
129+
5. Submits new findings to hypatia's pattern database.
130+
131+
No secrets or special tokens are needed beyond the default `GITHUB_TOKEN`
132+
artifacts are readable by the repo owner.
133+
134+
---
135+
136+
## Note on oikos/sustainabot
137+
138+
The `sustainabot` bot was registered in the gitbot-fleet as a sustainability
139+
and maintenance scanner, but was never fully wired into the CI pipeline.
140+
`static-analysis-gate.yml` replaces the role sustainabot was intended to fill:
141+
142+
- **What sustainabot was supposed to do:** Run periodic checks and flag
143+
maintenance debt.
144+
- **What static-analysis-gate does instead:** Runs on every PR and push,
145+
combining panic-attack (code quality / dangerous patterns) and hypatia
146+
(neurosymbolic security analysis) into a single required gate.
147+
- **sustainabot's remaining role:** It can still operate as a *scheduled*
148+
scanner for repos that have not yet adopted the gate workflow. Over time,
149+
as all repos adopt `static-analysis-gate.yml`, sustainabot's workload
150+
naturally decreases to zero.
151+
152+
The gitbot-fleet coordinator (`fleet-coordinator.sh`) should be updated to
153+
recognise `unified-findings` artifacts as the primary input channel, replacing
154+
the ad-hoc sustainabot submission path.
155+
156+
---
157+
158+
## Checklist for New Repos
159+
160+
- [ ] Copy `static-analysis-gate.yml` into `.github/workflows/`
161+
- [ ] Replace `{{OWNER}}` placeholder
162+
- [ ] Push to trigger first run
163+
- [ ] Add required status checks in branch protection settings
164+
- [ ] Install local pre-push hook (optional but recommended)
165+
- [ ] Verify `unified-findings` artifact appears after first run

hooks/post-scan.sh

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# post-scan.sh — Post-scan hook for gitbot-fleet
5+
#
6+
# Called automatically by run-fleet.sh after a scan completes.
7+
# If critical findings are detected, triggers immediate fix dispatch.
8+
#
9+
# Arguments:
10+
# $1 Number of critical findings
11+
# $2 Number of high findings
12+
# $3 Number of repos scanned
13+
#
14+
# Logs triggers to shared-context/learning/triggers.jsonl
15+
16+
set -euo pipefail
17+
18+
FLEET_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19+
LEARNING_DIR="$FLEET_DIR/shared-context/learning"
20+
TRIGGERS_LOG="$LEARNING_DIR/triggers.jsonl"
21+
22+
mkdir -p "$LEARNING_DIR"
23+
24+
CRITICAL="${1:-0}"
25+
HIGH="${2:-0}"
26+
REPOS_SCANNED="${3:-0}"
27+
TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
28+
29+
# Log the scan completion trigger
30+
jq -n \
31+
--arg ts "$TIMESTAMP" \
32+
--argjson critical "$CRITICAL" \
33+
--argjson high "$HIGH" \
34+
--argjson repos "$REPOS_SCANNED" \
35+
--arg event "scan_complete" \
36+
'{timestamp: $ts, event: $event, critical: $critical, high: $high, repos_scanned: $repos}' \
37+
>> "$TRIGGERS_LOG"
38+
39+
# If critical findings exist, immediately trigger fix for critical severity
40+
if [[ "$CRITICAL" -gt 0 ]]; then
41+
echo "[post-scan] $CRITICAL critical findings detected -- triggering critical fix dispatch"
42+
43+
jq -n \
44+
--arg ts "$TIMESTAMP" \
45+
--argjson critical "$CRITICAL" \
46+
--arg event "critical_autofix_trigger" \
47+
--arg action "run-fleet.sh fix --severity critical" \
48+
'{timestamp: $ts, event: $event, critical_count: $critical, action: $action}' \
49+
>> "$TRIGGERS_LOG"
50+
51+
# Run the fix command for critical severity only (dry run -- safety first)
52+
# To auto-apply, change to: --apply --severity critical
53+
"$FLEET_DIR/run-fleet.sh" fix --severity critical
54+
else
55+
echo "[post-scan] No critical findings. $HIGH high-severity findings across $REPOS_SCANNED repos."
56+
fi

hooks/pre-push-gate.sh

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# pre-push-gate.sh — Local pre-push hook for static analysis.
4+
#
5+
# Runs panic-attack and hypatia locally before allowing a push.
6+
# Exits non-zero if critical findings are detected.
7+
#
8+
# Installation (symlink into any repo):
9+
# ln -sf /path/to/gitbot-fleet/hooks/pre-push-gate.sh .git/hooks/pre-push
10+
#
11+
# Or copy it:
12+
# cp /path/to/gitbot-fleet/hooks/pre-push-gate.sh .git/hooks/pre-push
13+
# chmod +x .git/hooks/pre-push
14+
15+
set -euo pipefail
16+
17+
# Colours (disabled when stdout is not a terminal)
18+
if [ -t 1 ]; then
19+
RED='\033[0;31m'
20+
YELLOW='\033[0;33m'
21+
GREEN='\033[0;32m'
22+
BOLD='\033[1m'
23+
RESET='\033[0m'
24+
else
25+
RED='' YELLOW='' GREEN='' BOLD='' RESET=''
26+
fi
27+
28+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
29+
CRITICAL_COUNT=0
30+
HIGH_COUNT=0
31+
TOTAL_COUNT=0
32+
SCANNERS_RUN=0
33+
34+
# --------------------------------------------------------------------------
35+
# Helper: parse JSON finding counts (requires jq)
36+
# --------------------------------------------------------------------------
37+
count_findings() {
38+
local json_file="$1"
39+
if [ ! -s "$json_file" ] || ! jq empty "$json_file" 2>/dev/null; then
40+
return
41+
fi
42+
local c h m l
43+
c=$(jq '[.[] | select(.severity == "critical")] | length' "$json_file" 2>/dev/null || echo 0)
44+
h=$(jq '[.[] | select(.severity == "high")] | length' "$json_file" 2>/dev/null || echo 0)
45+
m=$(jq '[.[] | select(.severity == "medium")] | length' "$json_file" 2>/dev/null || echo 0)
46+
l=$(jq '[.[] | select(.severity == "low")] | length' "$json_file" 2>/dev/null || echo 0)
47+
CRITICAL_COUNT=$(( CRITICAL_COUNT + c ))
48+
HIGH_COUNT=$(( HIGH_COUNT + h ))
49+
TOTAL_COUNT=$(( TOTAL_COUNT + c + h + m + l ))
50+
}
51+
52+
# --------------------------------------------------------------------------
53+
# Scanner 1: panic-attack assail
54+
# --------------------------------------------------------------------------
55+
run_panic_attack() {
56+
if ! command -v panic-attack >/dev/null 2>&1; then
57+
echo -e "${YELLOW}[skip]${RESET} panic-attack not installed — skipping assail"
58+
return
59+
fi
60+
61+
echo -e "${BOLD}[1/2]${RESET} Running panic-attack assail..."
62+
local tmp
63+
tmp=$(mktemp /tmp/pa-findings-XXXXXX.json)
64+
65+
set +e
66+
panic-attack assail --format json "$REPO_ROOT" > "$tmp" 2>/dev/null
67+
local rc=$?
68+
set -e
69+
70+
if [ "$rc" -ne 0 ] && [ ! -s "$tmp" ]; then
71+
echo -e "${YELLOW}[warn]${RESET} panic-attack exited $rc with no output"
72+
rm -f "$tmp"
73+
return
74+
fi
75+
76+
count_findings "$tmp"
77+
SCANNERS_RUN=$(( SCANNERS_RUN + 1 ))
78+
79+
local pa_total
80+
pa_total=$(jq '. | length' "$tmp" 2>/dev/null || echo 0)
81+
echo -e " panic-attack: ${pa_total} finding(s)"
82+
rm -f "$tmp"
83+
}
84+
85+
# --------------------------------------------------------------------------
86+
# Scanner 2: hypatia-cli.sh scan
87+
# --------------------------------------------------------------------------
88+
run_hypatia() {
89+
# Look for hypatia-cli.sh in common locations
90+
local hypatia_bin=""
91+
for candidate in \
92+
"$(command -v hypatia-cli.sh 2>/dev/null || true)" \
93+
"$HOME/hypatia/hypatia-cli.sh" \
94+
"$HOME/.local/bin/hypatia-cli.sh"; do
95+
if [ -x "$candidate" ]; then
96+
hypatia_bin="$candidate"
97+
break
98+
fi
99+
done
100+
101+
if [ -z "$hypatia_bin" ]; then
102+
echo -e "${YELLOW}[skip]${RESET} hypatia-cli.sh not found — skipping scan"
103+
return
104+
fi
105+
106+
echo -e "${BOLD}[2/2]${RESET} Running hypatia scan..."
107+
local tmp
108+
tmp=$(mktemp /tmp/hyp-findings-XXXXXX.json)
109+
110+
set +e
111+
HYPATIA_FORMAT=json "$hypatia_bin" scan "$REPO_ROOT" > "$tmp" 2>/dev/null
112+
local rc=$?
113+
set -e
114+
115+
if [ "$rc" -ne 0 ] && [ ! -s "$tmp" ]; then
116+
echo -e "${YELLOW}[warn]${RESET} hypatia exited $rc with no output"
117+
rm -f "$tmp"
118+
return
119+
fi
120+
121+
count_findings "$tmp"
122+
SCANNERS_RUN=$(( SCANNERS_RUN + 1 ))
123+
124+
local hyp_total
125+
hyp_total=$(jq '. | length' "$tmp" 2>/dev/null || echo 0)
126+
echo -e " hypatia: ${hyp_total} finding(s)"
127+
rm -f "$tmp"
128+
}
129+
130+
# --------------------------------------------------------------------------
131+
# Main
132+
# --------------------------------------------------------------------------
133+
echo -e "${BOLD}Static Analysis Gate (pre-push)${RESET}"
134+
echo "Repository: $REPO_ROOT"
135+
echo ""
136+
137+
run_panic_attack
138+
run_hypatia
139+
140+
echo ""
141+
echo -e "${BOLD}--- Summary ---${RESET}"
142+
echo "Scanners run : $SCANNERS_RUN"
143+
echo "Total findings: $TOTAL_COUNT"
144+
echo " Critical : $CRITICAL_COUNT"
145+
echo " High : $HIGH_COUNT"
146+
147+
if [ "$SCANNERS_RUN" -eq 0 ]; then
148+
echo ""
149+
echo -e "${YELLOW}No scanners available — push allowed (install panic-attack or hypatia for local gating).${RESET}"
150+
exit 0
151+
fi
152+
153+
if [ "$CRITICAL_COUNT" -gt 0 ]; then
154+
echo ""
155+
echo -e "${RED}BLOCKED: $CRITICAL_COUNT critical finding(s) detected. Fix before pushing.${RESET}"
156+
exit 1
157+
fi
158+
159+
echo ""
160+
echo -e "${GREEN}No critical findings — push allowed.${RESET}"
161+
exit 0

0 commit comments

Comments
 (0)