arbiter-dev 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arbiter/__init__.py +3 -0
- arbiter/__main__.py +570 -0
- arbiter/agent_registry.py +120 -0
- arbiter/analyzers/__init__.py +1 -0
- arbiter/analyzers/base.py +44 -0
- arbiter/analyzers/complexity_analyzer.py +66 -0
- arbiter/analyzers/dead_code_analyzer.py +52 -0
- arbiter/analyzers/duplication_analyzer.py +96 -0
- arbiter/analyzers/ruff_analyzer.py +78 -0
- arbiter/analyzers/security_analyzer.py +64 -0
- arbiter/api.py +173 -0
- arbiter/bus_bridge.py +82 -0
- arbiter/diff_analyzer.py +161 -0
- arbiter/git_historian.py +209 -0
- arbiter/scoring.py +127 -0
- arbiter/store.py +249 -0
- arbiter_dev-0.2.0.dist-info/METADATA +216 -0
- arbiter_dev-0.2.0.dist-info/RECORD +22 -0
- arbiter_dev-0.2.0.dist-info/WHEEL +5 -0
- arbiter_dev-0.2.0.dist-info/entry_points.txt +2 -0
- arbiter_dev-0.2.0.dist-info/licenses/LICENSE +21 -0
- arbiter_dev-0.2.0.dist-info/top_level.txt +1 -0
arbiter/store.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Store — SQLite persistence for quality metrics, trends, and agent scores."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Generator
|
|
12
|
+
|
|
13
|
+
from arbiter.scoring import AgentScore, RepoScore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_DEFAULT_DB = Path("arbiter_data.db")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class FileScore:
|
|
21
|
+
"""Quality info for a single file."""
|
|
22
|
+
|
|
23
|
+
file_path: str
|
|
24
|
+
finding_count: int
|
|
25
|
+
worst_severity: str
|
|
26
|
+
last_agent: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Store:
|
|
30
|
+
"""SQLite store for Arbiter quality data."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, db_path: Path | str = _DEFAULT_DB):
|
|
33
|
+
self._db_path = Path(db_path)
|
|
34
|
+
self._init_db()
|
|
35
|
+
|
|
36
|
+
@contextmanager
|
|
37
|
+
def _conn(self) -> Generator[sqlite3.Connection, None, None]:
|
|
38
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
39
|
+
conn.row_factory = sqlite3.Row
|
|
40
|
+
try:
|
|
41
|
+
yield conn
|
|
42
|
+
conn.commit()
|
|
43
|
+
finally:
|
|
44
|
+
conn.close()
|
|
45
|
+
|
|
46
|
+
def _init_db(self) -> None:
|
|
47
|
+
with self._conn() as conn:
|
|
48
|
+
conn.executescript("""
|
|
49
|
+
CREATE TABLE IF NOT EXISTS commit_quality (
|
|
50
|
+
commit_hash TEXT,
|
|
51
|
+
repo_name TEXT NOT NULL DEFAULT '',
|
|
52
|
+
timestamp TEXT NOT NULL,
|
|
53
|
+
agent TEXT NOT NULL,
|
|
54
|
+
files_changed INTEGER DEFAULT 0,
|
|
55
|
+
loc_added INTEGER DEFAULT 0,
|
|
56
|
+
loc_removed INTEGER DEFAULT 0,
|
|
57
|
+
overall_score REAL,
|
|
58
|
+
lint_score REAL,
|
|
59
|
+
security_score REAL,
|
|
60
|
+
complexity_score REAL,
|
|
61
|
+
total_findings INTEGER DEFAULT 0,
|
|
62
|
+
findings_json TEXT,
|
|
63
|
+
created_at TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS file_quality (
|
|
67
|
+
file_path TEXT,
|
|
68
|
+
repo_name TEXT NOT NULL DEFAULT '',
|
|
69
|
+
finding_count INTEGER DEFAULT 0,
|
|
70
|
+
worst_severity TEXT,
|
|
71
|
+
last_agent TEXT,
|
|
72
|
+
updated_at TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS repo_snapshots (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
repo_name TEXT NOT NULL DEFAULT '',
|
|
78
|
+
timestamp TEXT NOT NULL,
|
|
79
|
+
overall_score REAL,
|
|
80
|
+
lint_score REAL,
|
|
81
|
+
security_score REAL,
|
|
82
|
+
complexity_score REAL,
|
|
83
|
+
total_findings INTEGER,
|
|
84
|
+
total_loc INTEGER,
|
|
85
|
+
findings_json TEXT
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_commit_repo ON commit_quality(commit_hash, repo_name);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_commit_agent ON commit_quality(agent);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_commit_repo_name ON commit_quality(repo_name);
|
|
91
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_file_repo ON file_quality(file_path, repo_name);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_snapshot_repo ON repo_snapshots(repo_name);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_commit_timestamp ON commit_quality(timestamp);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_snapshot_timestamp ON repo_snapshots(timestamp);
|
|
95
|
+
""")
|
|
96
|
+
|
|
97
|
+
def record_commit(
|
|
98
|
+
self,
|
|
99
|
+
commit_hash: str,
|
|
100
|
+
timestamp: str,
|
|
101
|
+
agent: str,
|
|
102
|
+
files_changed: int,
|
|
103
|
+
loc_added: int,
|
|
104
|
+
loc_removed: int,
|
|
105
|
+
score: RepoScore,
|
|
106
|
+
repo_name: str = "",
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Record quality data for a single commit."""
|
|
109
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
110
|
+
with self._conn() as conn:
|
|
111
|
+
conn.execute(
|
|
112
|
+
"""INSERT OR REPLACE INTO commit_quality
|
|
113
|
+
(commit_hash, repo_name, timestamp, agent, files_changed, loc_added, loc_removed,
|
|
114
|
+
overall_score, lint_score, security_score, complexity_score,
|
|
115
|
+
total_findings, findings_json, created_at)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
117
|
+
(commit_hash, repo_name, timestamp, agent, files_changed, loc_added, loc_removed,
|
|
118
|
+
score.overall, score.lint_score, score.security_score, score.complexity_score,
|
|
119
|
+
score.total_findings, json.dumps(score.findings_by_severity), now),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def record_snapshot(self, score: RepoScore, total_loc: int, repo_name: str = "") -> None:
|
|
123
|
+
"""Record a point-in-time repo quality snapshot."""
|
|
124
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
125
|
+
with self._conn() as conn:
|
|
126
|
+
conn.execute(
|
|
127
|
+
"""INSERT INTO repo_snapshots
|
|
128
|
+
(repo_name, timestamp, overall_score, lint_score, security_score, complexity_score,
|
|
129
|
+
total_findings, total_loc, findings_json)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
131
|
+
(repo_name, now, score.overall, score.lint_score, score.security_score,
|
|
132
|
+
score.complexity_score, score.total_findings, total_loc,
|
|
133
|
+
json.dumps(score.findings_by_tool)),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def update_file_quality(self, file_path: str, finding_count: int, worst_severity: str, agent: str, repo_name: str = "") -> None:
|
|
137
|
+
"""Update quality record for a single file."""
|
|
138
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
139
|
+
with self._conn() as conn:
|
|
140
|
+
conn.execute(
|
|
141
|
+
"""INSERT OR REPLACE INTO file_quality
|
|
142
|
+
(file_path, repo_name, finding_count, worst_severity, last_agent, updated_at)
|
|
143
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
144
|
+
(file_path, repo_name, finding_count, worst_severity, agent, now),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def get_agent_leaderboard(self) -> list[AgentScore]:
|
|
148
|
+
"""Get quality scores aggregated by agent."""
|
|
149
|
+
with self._conn() as conn:
|
|
150
|
+
rows = conn.execute("""
|
|
151
|
+
SELECT agent,
|
|
152
|
+
COUNT(*) as commit_count,
|
|
153
|
+
AVG(overall_score) as avg_score,
|
|
154
|
+
SUM(loc_added) as total_loc
|
|
155
|
+
FROM commit_quality
|
|
156
|
+
GROUP BY agent
|
|
157
|
+
ORDER BY avg_score DESC
|
|
158
|
+
""").fetchall()
|
|
159
|
+
|
|
160
|
+
return [
|
|
161
|
+
AgentScore(
|
|
162
|
+
agent_name=row["agent"],
|
|
163
|
+
commit_count=row["commit_count"],
|
|
164
|
+
avg_score=round(row["avg_score"] or 0, 1),
|
|
165
|
+
total_loc=row["total_loc"] or 0,
|
|
166
|
+
)
|
|
167
|
+
for row in rows
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
def get_trend(self, days: int = 30) -> list[dict[str, Any]]:
|
|
171
|
+
"""Get repo quality snapshots over time."""
|
|
172
|
+
with self._conn() as conn:
|
|
173
|
+
rows = conn.execute("""
|
|
174
|
+
SELECT timestamp, overall_score, lint_score, security_score,
|
|
175
|
+
complexity_score, total_findings, total_loc
|
|
176
|
+
FROM repo_snapshots
|
|
177
|
+
ORDER BY timestamp DESC
|
|
178
|
+
LIMIT ?
|
|
179
|
+
""", (days,)).fetchall()
|
|
180
|
+
return [dict(row) for row in reversed(rows)]
|
|
181
|
+
|
|
182
|
+
def get_worst_files(self, limit: int = 20) -> list[FileScore]:
|
|
183
|
+
"""Get files ranked by worst quality."""
|
|
184
|
+
with self._conn() as conn:
|
|
185
|
+
rows = conn.execute("""
|
|
186
|
+
SELECT file_path, finding_count, worst_severity, last_agent
|
|
187
|
+
FROM file_quality
|
|
188
|
+
WHERE finding_count > 0
|
|
189
|
+
ORDER BY finding_count DESC
|
|
190
|
+
LIMIT ?
|
|
191
|
+
""", (limit,)).fetchall()
|
|
192
|
+
return [
|
|
193
|
+
FileScore(
|
|
194
|
+
file_path=row["file_path"],
|
|
195
|
+
finding_count=row["finding_count"],
|
|
196
|
+
worst_severity=row["worst_severity"] or "LOW",
|
|
197
|
+
last_agent=row["last_agent"] or "unknown",
|
|
198
|
+
)
|
|
199
|
+
for row in rows
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
def get_agent_trend(self, agent: str, days: int = 30) -> list[dict[str, Any]]:
|
|
203
|
+
"""Get quality trend for a specific agent over time."""
|
|
204
|
+
with self._conn() as conn:
|
|
205
|
+
rows = conn.execute("""
|
|
206
|
+
SELECT timestamp, overall_score, lint_score, security_score,
|
|
207
|
+
complexity_score, total_findings, loc_added, commit_hash
|
|
208
|
+
FROM commit_quality
|
|
209
|
+
WHERE agent = ?
|
|
210
|
+
ORDER BY timestamp DESC
|
|
211
|
+
LIMIT ?
|
|
212
|
+
""", (agent, days)).fetchall()
|
|
213
|
+
return [dict(row) for row in reversed(rows)]
|
|
214
|
+
|
|
215
|
+
def get_recent_commits(self, agent: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
|
|
216
|
+
"""Get recent commits with quality data."""
|
|
217
|
+
with self._conn() as conn:
|
|
218
|
+
if agent:
|
|
219
|
+
rows = conn.execute("""
|
|
220
|
+
SELECT * FROM commit_quality
|
|
221
|
+
WHERE agent = ?
|
|
222
|
+
ORDER BY timestamp DESC LIMIT ?
|
|
223
|
+
""", (agent, limit)).fetchall()
|
|
224
|
+
else:
|
|
225
|
+
rows = conn.execute("""
|
|
226
|
+
SELECT * FROM commit_quality
|
|
227
|
+
ORDER BY timestamp DESC LIMIT ?
|
|
228
|
+
""", (limit,)).fetchall()
|
|
229
|
+
return [dict(row) for row in rows]
|
|
230
|
+
|
|
231
|
+
def get_fleet_report(self) -> list[dict[str, Any]]:
|
|
232
|
+
"""Get quality summary per repo — the fleet-wide view."""
|
|
233
|
+
with self._conn() as conn:
|
|
234
|
+
rows = conn.execute("""
|
|
235
|
+
SELECT repo_name,
|
|
236
|
+
overall_score,
|
|
237
|
+
lint_score,
|
|
238
|
+
security_score,
|
|
239
|
+
complexity_score,
|
|
240
|
+
total_findings,
|
|
241
|
+
total_loc,
|
|
242
|
+
timestamp
|
|
243
|
+
FROM repo_snapshots
|
|
244
|
+
WHERE id IN (
|
|
245
|
+
SELECT MAX(id) FROM repo_snapshots GROUP BY repo_name
|
|
246
|
+
)
|
|
247
|
+
ORDER BY overall_score DESC
|
|
248
|
+
""").fetchall()
|
|
249
|
+
return [dict(row) for row in rows]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arbiter-dev
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Agent-aware code quality system for multi-agent codebases
|
|
5
|
+
Author: Reuben Bowlby, Daniel Matha
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: analyzers
|
|
16
|
+
Requires-Dist: ruff>=0.4.0; extra == "analyzers"
|
|
17
|
+
Requires-Dist: radon>=6.0; extra == "analyzers"
|
|
18
|
+
Requires-Dist: vulture>=2.10; extra == "analyzers"
|
|
19
|
+
Requires-Dist: bandit>=1.7.0; extra == "analyzers"
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: arbiter[analyzers,test]; extra == "all"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Arbiter
|
|
27
|
+
|
|
28
|
+
**Agent-aware code quality system for multi-agent codebases.**
|
|
29
|
+
|
|
30
|
+
In 2026, code is written by fleets of AI agents. Arbiter knows *who* wrote each line — human or AI — and scores quality accordingly.
|
|
31
|
+
|
|
32
|
+
## What Makes Arbiter Different
|
|
33
|
+
|
|
34
|
+
| Feature | Traditional Tools | Arbiter |
|
|
35
|
+
|---------|------------------|---------|
|
|
36
|
+
| Agent attribution | None | First-class: tracks Claude, Codex, Gemini, Copilot, humans |
|
|
37
|
+
| Per-commit scoring | Repo-wide only | Scores each commit's changed files individually |
|
|
38
|
+
| Diff analysis | N/A | Score only what changed in a PR/branch |
|
|
39
|
+
| Transparency | Opaque score | Every score decomposes into lint + security + complexity |
|
|
40
|
+
| Agent-specific gates | N/A | Different quality thresholds per agent trust tier |
|
|
41
|
+
| Tool integration | Proprietary | Wraps tools you already trust: ruff, Bandit, radon, vulture |
|
|
42
|
+
| Dashboard | SaaS login | Single HTML file with per-agent timelines, commit feed, fleet view |
|
|
43
|
+
| Dependencies | Heavy | Analysis tools only; core is stdlib Python |
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/hummbl-dev/arbiter.git
|
|
49
|
+
cd arbiter
|
|
50
|
+
|
|
51
|
+
# Install (makes `arbiter` command available)
|
|
52
|
+
pip install ".[analyzers]"
|
|
53
|
+
|
|
54
|
+
# Quick score (no persistence)
|
|
55
|
+
arbiter score /path/to/your/repo
|
|
56
|
+
|
|
57
|
+
# Full analysis with per-commit agent attribution
|
|
58
|
+
arbiter analyze /path/to/your/repo
|
|
59
|
+
|
|
60
|
+
# Score only files changed since main
|
|
61
|
+
arbiter diff /path/to/your/repo --base main
|
|
62
|
+
|
|
63
|
+
# Agent leaderboard
|
|
64
|
+
arbiter agents
|
|
65
|
+
|
|
66
|
+
# Start dashboard
|
|
67
|
+
arbiter serve --port 8080
|
|
68
|
+
# Open http://localhost:8080
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Without install (PYTHONPATH)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
PYTHONPATH=src python -m arbiter score /path/to/your/repo
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### With Docker
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
docker build -t arbiter .
|
|
81
|
+
docker run -p 8080:8080 -v /path/to/repo:/repo:ro arbiter
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Git Repo ──→ [Git Historian] ──→ [Analyzer Runner] ──→ [Scoring Engine] ──→ [SQLite Store]
|
|
88
|
+
│ │ │ │
|
|
89
|
+
agent attribution tool invocation weighted rubric trend data
|
|
90
|
+
(Co-Authored-By, (ruff, radon, (lint 35%, │
|
|
91
|
+
email matching) vulture, bandit) security 30%, ├──→ REST API
|
|
92
|
+
complexity 35%) └──→ Dashboard
|
|
93
|
+
┌────────────┐
|
|
94
|
+
│Diff Analyzer│ ←── v0.2: scores only changed files per commit/branch
|
|
95
|
+
└────────────┘
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Per-Commit Scoring (v0.2)
|
|
99
|
+
|
|
100
|
+
Every commit is scored against only the files it changed, not the entire repo. This makes the agent leaderboard meaningful — a commit that touches 1 clean file scores differently than one that touches 10 messy files.
|
|
101
|
+
|
|
102
|
+
### Diff Mode (v0.2)
|
|
103
|
+
|
|
104
|
+
`arbiter diff` scores only files changed since a base branch. Ideal for CI/PR quality gates — fast, scoped, actionable.
|
|
105
|
+
|
|
106
|
+
### Agent Attribution
|
|
107
|
+
|
|
108
|
+
Arbiter identifies which agent authored each commit:
|
|
109
|
+
|
|
110
|
+
1. **Co-Authored-By trailer** — `Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>`
|
|
111
|
+
2. **Author email** — maps `noreply@anthropic.com` → claude, `codex@openai.com` → codex
|
|
112
|
+
3. **Default** — "human" if no agent pattern matches
|
|
113
|
+
|
|
114
|
+
Configure in `agents.yml`:
|
|
115
|
+
```yaml
|
|
116
|
+
agents:
|
|
117
|
+
- name: claude
|
|
118
|
+
emails: [noreply@anthropic.com]
|
|
119
|
+
co_author_patterns: ["Claude\\s+(Opus|Sonnet|Haiku)"]
|
|
120
|
+
trust_tier: verified
|
|
121
|
+
quality_threshold: 70.0
|
|
122
|
+
- name: gemini
|
|
123
|
+
trust_tier: probation
|
|
124
|
+
quality_threshold: 80.0 # Higher bar for probationary agents
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Analyzers (pluggable)
|
|
128
|
+
|
|
129
|
+
| Analyzer | Tool | What It Finds |
|
|
130
|
+
|----------|------|--------------|
|
|
131
|
+
| Lint | ruff | Style violations, import errors, bugbear patterns |
|
|
132
|
+
| Complexity | radon | Cyclomatic complexity (grade A-F per function) |
|
|
133
|
+
| Security | bandit | Hardcoded secrets, shell injection, dangerous patterns |
|
|
134
|
+
| Dead Code | vulture | Unused functions, imports, variables |
|
|
135
|
+
| Duplication | AST hash | Near-duplicate function bodies |
|
|
136
|
+
|
|
137
|
+
### Scoring
|
|
138
|
+
|
|
139
|
+
Deterministic. Same code → same score. Always.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
Overall = Lint (35%) + Security (30%) + Complexity (35%)
|
|
143
|
+
|
|
144
|
+
Penalty points by severity:
|
|
145
|
+
CRITICAL: 50 | HIGH: 20 | MEDIUM: 5 | LOW: 1
|
|
146
|
+
|
|
147
|
+
Score = 100 - (total_penalty / LOC) * normalization_factor
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Grades: A (90+) | B (80+) | C (70+) | D (60+) | F (<60)
|
|
151
|
+
|
|
152
|
+
### Dashboard (v2)
|
|
153
|
+
|
|
154
|
+
Single HTML file with Chart.js. No build step, no React, no npm.
|
|
155
|
+
|
|
156
|
+
- **Score Card** — Big number + breakdown bars
|
|
157
|
+
- **Agent Leaderboard** — Who writes the best code? Color-coded by agent
|
|
158
|
+
- **Per-Agent Quality Timeline** — Score over time per agent (not just repo-wide)
|
|
159
|
+
- **Commit Feed** — Recent commits with agent, score, changes, timestamp
|
|
160
|
+
- **Hotspot Files** — Ranked by finding count
|
|
161
|
+
- **Fleet View** — Multi-repo quality grid with color-coded scores
|
|
162
|
+
- **Tabbed UI** — Overview, Commits, Fleet tabs
|
|
163
|
+
|
|
164
|
+
### API
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
GET /api/score Current repo score
|
|
168
|
+
GET /api/agents Agent leaderboard
|
|
169
|
+
GET /api/agents/{name}/trend Per-agent quality over time
|
|
170
|
+
GET /api/trend?days=30 Quality over time
|
|
171
|
+
GET /api/worst?limit=20 Worst files
|
|
172
|
+
GET /api/commits Recent commits with scores
|
|
173
|
+
GET /api/commits/{hash} Detail for one commit
|
|
174
|
+
GET /api/fleet Fleet report (multi-repo)
|
|
175
|
+
GET /api/health System health
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## CLI Commands
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
arbiter analyze <repo> # Full analysis + per-commit scoring + persist
|
|
182
|
+
arbiter score <repo> [--json] [--exclude] # Quick score (no persist)
|
|
183
|
+
arbiter diff <repo> [--base main] [--json] # Score only changed files vs base branch
|
|
184
|
+
arbiter agents # Agent leaderboard
|
|
185
|
+
arbiter trend [--days 30] # Quality trend
|
|
186
|
+
arbiter worst [--limit 20] # Worst files
|
|
187
|
+
arbiter commits [--agent claude] # Recent commits
|
|
188
|
+
arbiter audit-fleet <directory> # Audit all repos in a directory
|
|
189
|
+
arbiter fleet-report # Fleet quality summary
|
|
190
|
+
arbiter triage # Auto-classify repos: green/yellow/red/archive
|
|
191
|
+
arbiter fix <repo> [--dry-run] # Auto-fix ruff findings + before/after score
|
|
192
|
+
arbiter serve [--port 8080] # API + dashboard
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Tests
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install ".[test]"
|
|
199
|
+
PYTHONPATH=src python -m pytest tests/ -v
|
|
200
|
+
# 78 tests, <7 seconds
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Requirements
|
|
204
|
+
|
|
205
|
+
- Python 3.11+
|
|
206
|
+
- git (for historian)
|
|
207
|
+
- Optional: ruff, radon, vulture, bandit (for full analysis)
|
|
208
|
+
- Docker (for containerized deployment)
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT — see [LICENSE](LICENSE).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
Built by [HUMMBL LLC](https://hummbl.io) from production experience coordinating Claude, Codex, Gemini, and human engineers on a 6,000+ test codebase.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
arbiter/__init__.py,sha256=lSICWpF66jOKqVJoxYj0vZledRMNPty4ea779jJfU1M,74
|
|
2
|
+
arbiter/__main__.py,sha256=KUJxV2q_wgm7TFqX9je6IU4E9lAvxR_nU_XGU0GY80Q,23171
|
|
3
|
+
arbiter/agent_registry.py,sha256=IpEPKCUwlUuJA1GkIK6qsUnNSF9K48wLcm0697mh_GY,3777
|
|
4
|
+
arbiter/api.py,sha256=z0jannzi2DFrjo33cwu8Sleueweqzsbb4pkqhq0vcOA,6240
|
|
5
|
+
arbiter/bus_bridge.py,sha256=us_NNncATORbLYJya8k1g3hGw33CDX306xyyJleamJg,2496
|
|
6
|
+
arbiter/diff_analyzer.py,sha256=qkyuPHDC2Ck2ne9qbUPoh-37qNkG5TNp0NCKPbKV1Ts,4873
|
|
7
|
+
arbiter/git_historian.py,sha256=Gd9JWoYKt__sKpaI90uZxR5ILRb5tY_ujCngFc_CK6U,6411
|
|
8
|
+
arbiter/scoring.py,sha256=3RU_fvC3JWTuyf5_llkEuFe9tzlrfwjLjPB7Gz4vc60,3742
|
|
9
|
+
arbiter/store.py,sha256=tut9zJtONxrG0xHThDd9XSigkpqLfTTpPxuThyGNCd4,10072
|
|
10
|
+
arbiter/analyzers/__init__.py,sha256=0uuTqh9PRPE43SYBYoN2y2YXcLkROW6gBjfiqqgpE0g,67
|
|
11
|
+
arbiter/analyzers/base.py,sha256=hNrZTG4YQsXLfxbbhgrBtmal8HP_8KK33fALFoamxXI,1185
|
|
12
|
+
arbiter/analyzers/complexity_analyzer.py,sha256=oe-oq22L-h1GAhxdvTWq1QywrS-yPh2BbpA2ZwOujzE,2144
|
|
13
|
+
arbiter/analyzers/dead_code_analyzer.py,sha256=QIwnrdMdF3H3PiU3A0HSj_UM4y3_iviYkb7xgU4Uwu0,1704
|
|
14
|
+
arbiter/analyzers/duplication_analyzer.py,sha256=F2pcmFNnkJ5CGukMjuMI4uTGsPNUyQl1LRL2xpD4uFI,3553
|
|
15
|
+
arbiter/analyzers/ruff_analyzer.py,sha256=tANAuRri1X3jW6RdRjl9zaFplY7ufD2Alv_Wx5SQJdI,2449
|
|
16
|
+
arbiter/analyzers/security_analyzer.py,sha256=5qtbOMn-lAegvh9cZcf8kysbW3S5EBuoqTTjmPiO3-g,1910
|
|
17
|
+
arbiter_dev-0.2.0.dist-info/licenses/LICENSE,sha256=x6CmZmL3qaXphWZWt02ZP2p_BcsoaSQxLj_R9IyalcM,1097
|
|
18
|
+
arbiter_dev-0.2.0.dist-info/METADATA,sha256=FVND_2CIzLrIpx74dv66AQhgnrkpl74gQ9TnUO_V8-U,7647
|
|
19
|
+
arbiter_dev-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
20
|
+
arbiter_dev-0.2.0.dist-info/entry_points.txt,sha256=5UnDdGkHgctdc9b73lvcfv3lh8tUVaq2uc8HoleTy2I,50
|
|
21
|
+
arbiter_dev-0.2.0.dist-info/top_level.txt,sha256=6xckoxU5fjdxvk-5J6uCv2PcPz11wqzqL1DXldzj8ZQ,8
|
|
22
|
+
arbiter_dev-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reuben Bowlby, Daniel Matha / HUMMBL LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
arbiter
|