git-oops 1.0.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.
- git_oops-1.0.0.dist-info/METADATA +198 -0
- git_oops-1.0.0.dist-info/RECORD +32 -0
- git_oops-1.0.0.dist-info/WHEEL +5 -0
- git_oops-1.0.0.dist-info/entry_points.txt +2 -0
- git_oops-1.0.0.dist-info/licenses/LICENSE +21 -0
- git_oops-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +16 -0
- src/blackbox.py +119 -0
- src/circuit_breaker.py +129 -0
- src/cli.py +556 -0
- src/consensus.py +58 -0
- src/engine.py +222 -0
- src/firewall.py +48 -0
- src/guardian.py +589 -0
- src/hardware.py +137 -0
- src/hooks.py +525 -0
- src/hud.py +493 -0
- src/local_llm.py +116 -0
- src/mcp_server.py +124 -0
- src/replay.py +100 -0
- src/reviewer.py +157 -0
- src/rules.py +291 -0
- src/salvage.py +92 -0
- src/sentinel.py +79 -0
- src/shadow.py +70 -0
- src/solver.py +158 -0
- src/squeezer.py +64 -0
- src/supply_chain.py +142 -0
- src/tester.py +54 -0
- src/ui.py +124 -0
- src/updater.py +49 -0
- src/watchdog.py +96 -0
src/hud.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-Oops Obsidian Glassmorphism Web HUD Server
|
|
3
|
+
Pure Python zero-dependency web dashboard for live AI agent safety monitoring,
|
|
4
|
+
circuit breaker metrics, crash replay timelines, and instant panic rollback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import socket
|
|
12
|
+
import threading
|
|
13
|
+
import webbrowser
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from src.circuit_breaker import get_daily_spend, get_default_config
|
|
19
|
+
from src.hardware import check_hardware_throttling, get_hardware_telemetry
|
|
20
|
+
from src.blackbox import list_flight_events
|
|
21
|
+
from src.watchdog import acquire_agent_lock, release_agent_lock
|
|
22
|
+
from src.salvage import run_reflog_autopsy
|
|
23
|
+
from src.hooks import run_l3_preflight
|
|
24
|
+
|
|
25
|
+
HTML_TEMPLATE = """<!DOCTYPE html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="UTF-8">
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
30
|
+
<title>GIT-OOPS // HUD</title>
|
|
31
|
+
<style>
|
|
32
|
+
:root {
|
|
33
|
+
--bg-void: #050507;
|
|
34
|
+
--surface-glass: rgba(18, 18, 24, 0.75);
|
|
35
|
+
--surface-glass-hover: rgba(26, 26, 36, 0.85);
|
|
36
|
+
--border-subtle: rgba(255, 255, 255, 0.08);
|
|
37
|
+
--border-glow: rgba(0, 255, 200, 0.3);
|
|
38
|
+
--accent-cyan: #00ffc8;
|
|
39
|
+
--accent-crimson: #ff0055;
|
|
40
|
+
--accent-amber: #ffb700;
|
|
41
|
+
--accent-purple: #a855f7;
|
|
42
|
+
--text-main: #f3f4f6;
|
|
43
|
+
--text-muted: #9ca3af;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
margin: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "JetBrains Mono", monospace;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
background: var(--bg-void);
|
|
55
|
+
color: var(--text-main);
|
|
56
|
+
min-height: 100vh;
|
|
57
|
+
padding: 24px;
|
|
58
|
+
background-image:
|
|
59
|
+
radial-gradient(circle at 15% 15%, rgba(0, 255, 200, 0.05) 0%, transparent 40%),
|
|
60
|
+
radial-gradient(circle at 85% 85%, rgba(168, 85, 247, 0.05) 0%, transparent 40%);
|
|
61
|
+
background-attachment: fixed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.header {
|
|
65
|
+
display: flex;
|
|
66
|
+
justify-content: space-between;
|
|
67
|
+
align-items: center;
|
|
68
|
+
padding-bottom: 24px;
|
|
69
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
70
|
+
margin-bottom: 28px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.brand {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 12px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.brand-icon {
|
|
80
|
+
font-size: 28px;
|
|
81
|
+
text-shadow: 0 0 12px var(--accent-cyan);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.brand h1 {
|
|
85
|
+
font-size: 20px;
|
|
86
|
+
font-weight: 800;
|
|
87
|
+
letter-spacing: 2px;
|
|
88
|
+
background: linear-gradient(135deg, #ffffff, var(--accent-cyan));
|
|
89
|
+
-webkit-background-clip: text;
|
|
90
|
+
-webkit-text-fill-color: transparent;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.status-badge {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 8px;
|
|
97
|
+
background: rgba(0, 255, 200, 0.1);
|
|
98
|
+
border: 1px solid var(--accent-cyan);
|
|
99
|
+
padding: 6px 14px;
|
|
100
|
+
border-radius: 999px;
|
|
101
|
+
font-size: 12px;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
letter-spacing: 1px;
|
|
104
|
+
color: var(--accent-cyan);
|
|
105
|
+
box-shadow: 0 0 16px rgba(0, 255, 200, 0.2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.pulse-dot {
|
|
109
|
+
width: 8px;
|
|
110
|
+
height: 8px;
|
|
111
|
+
background: var(--accent-cyan);
|
|
112
|
+
border-radius: 50%;
|
|
113
|
+
animation: pulse 1.8s infinite;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@keyframes pulse {
|
|
117
|
+
0% { transform: scale(0.9); opacity: 0.7; }
|
|
118
|
+
50% { transform: scale(1.4); opacity: 1; box-shadow: 0 0 8px var(--accent-cyan); }
|
|
119
|
+
100% { transform: scale(0.9); opacity: 0.7; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.grid {
|
|
123
|
+
display: grid;
|
|
124
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
125
|
+
gap: 20px;
|
|
126
|
+
margin-bottom: 28px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.card {
|
|
130
|
+
background: var(--surface-glass);
|
|
131
|
+
backdrop-filter: blur(16px);
|
|
132
|
+
-webkit-backdrop-filter: blur(16px);
|
|
133
|
+
border: 1px solid var(--border-subtle);
|
|
134
|
+
border-radius: 12px;
|
|
135
|
+
padding: 20px;
|
|
136
|
+
transition: all 0.25s ease;
|
|
137
|
+
position: relative;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.card:hover {
|
|
142
|
+
border-color: var(--border-glow);
|
|
143
|
+
transform: translateY(-2px);
|
|
144
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.card-title {
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
font-weight: 700;
|
|
150
|
+
text-transform: uppercase;
|
|
151
|
+
letter-spacing: 1.5px;
|
|
152
|
+
color: var(--text-muted);
|
|
153
|
+
margin-bottom: 12px;
|
|
154
|
+
display: flex;
|
|
155
|
+
justify-content: space-between;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.metric-value {
|
|
159
|
+
font-size: 32px;
|
|
160
|
+
font-weight: 800;
|
|
161
|
+
letter-spacing: -0.5px;
|
|
162
|
+
margin-bottom: 8px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.progress-bar-bg {
|
|
166
|
+
background: rgba(255, 255, 255, 0.06);
|
|
167
|
+
height: 6px;
|
|
168
|
+
border-radius: 999px;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
margin-top: 10px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.progress-bar-fill {
|
|
174
|
+
height: 100%;
|
|
175
|
+
border-radius: 999px;
|
|
176
|
+
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
|
|
177
|
+
width: 0%;
|
|
178
|
+
transition: width 0.5s ease;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.card-detail {
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
color: var(--text-muted);
|
|
184
|
+
margin-top: 8px;
|
|
185
|
+
line-height: 1.5;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.table-card {
|
|
189
|
+
grid-column: 1 / -1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.event-table {
|
|
193
|
+
width: 100%;
|
|
194
|
+
border-collapse: collapse;
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
margin-top: 12px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.event-table th, .event-table td {
|
|
200
|
+
padding: 10px 14px;
|
|
201
|
+
text-align: left;
|
|
202
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.event-table th {
|
|
206
|
+
color: var(--text-muted);
|
|
207
|
+
font-size: 11px;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
letter-spacing: 1px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.btn {
|
|
213
|
+
background: rgba(255, 255, 255, 0.05);
|
|
214
|
+
border: 1px solid var(--border-subtle);
|
|
215
|
+
color: var(--text-main);
|
|
216
|
+
padding: 8px 16px;
|
|
217
|
+
border-radius: 6px;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
transition: all 0.2s;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.btn:hover {
|
|
225
|
+
background: rgba(255, 255, 255, 0.12);
|
|
226
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.btn-danger {
|
|
230
|
+
background: rgba(255, 0, 85, 0.15);
|
|
231
|
+
border-color: var(--accent-crimson);
|
|
232
|
+
color: var(--accent-crimson);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.btn-danger:hover {
|
|
236
|
+
background: var(--accent-crimson);
|
|
237
|
+
color: #fff;
|
|
238
|
+
box-shadow: 0 0 15px rgba(255, 0, 85, 0.5);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.action-bar {
|
|
242
|
+
display: flex;
|
|
243
|
+
gap: 12px;
|
|
244
|
+
margin-top: 20px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.code-block {
|
|
248
|
+
background: rgba(0, 0, 0, 0.4);
|
|
249
|
+
border: 1px solid var(--border-subtle);
|
|
250
|
+
border-radius: 6px;
|
|
251
|
+
padding: 12px;
|
|
252
|
+
font-family: monospace;
|
|
253
|
+
font-size: 12px;
|
|
254
|
+
color: var(--accent-cyan);
|
|
255
|
+
max-height: 180px;
|
|
256
|
+
overflow-y: auto;
|
|
257
|
+
white-space: pre-wrap;
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
260
|
+
</head>
|
|
261
|
+
<body>
|
|
262
|
+
<div class="header">
|
|
263
|
+
<div class="brand">
|
|
264
|
+
<span class="brand-icon">⚡</span>
|
|
265
|
+
<div>
|
|
266
|
+
<h1>GIT-OOPS // HUD</h1>
|
|
267
|
+
<span style="font-size: 11px; color: var(--text-muted); letter-spacing: 1px;">THE FINAL LEGEND EDITION v1.0.0</span>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="status-badge">
|
|
271
|
+
<div class="pulse-dot"></div>
|
|
272
|
+
<span>AUTONOMOUS HARNESS ACTIVE</span>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="grid">
|
|
277
|
+
<!-- Daily Spend Circuit Breaker -->
|
|
278
|
+
<div class="card">
|
|
279
|
+
<div class="card-title">
|
|
280
|
+
<span>Daily Spend Circuit Breaker</span>
|
|
281
|
+
<span id="spend-pct" style="color: var(--accent-cyan);">0%</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="metric-value" id="spend-val" style="color: var(--accent-cyan);">$0.0000</div>
|
|
284
|
+
<div class="progress-bar-bg">
|
|
285
|
+
<div class="progress-bar-fill" id="spend-bar"></div>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="card-detail" id="spend-detail">Daily Ceiling: $1.00 USD</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- Hardware & CPU Throttle -->
|
|
291
|
+
<div class="card">
|
|
292
|
+
<div class="card-title">
|
|
293
|
+
<span>Hardware & Thermal Throttle</span>
|
|
294
|
+
<span id="hw-status" style="color: var(--accent-cyan);">OPTIMAL</span>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="metric-value" id="cpu-prio" style="font-size: 24px;">BELOW_NORMAL</div>
|
|
297
|
+
<div class="card-detail" id="hw-detail">Battery: Nominal • Temperature: Safe • OS Priority: Throttled</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<!-- L3 Pre-Flight Safety Status -->
|
|
301
|
+
<div class="card">
|
|
302
|
+
<div class="card-title">
|
|
303
|
+
<span>L3 Local CI/CD Pre-Flight</span>
|
|
304
|
+
<span style="color: var(--accent-cyan);">READY</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="metric-value" style="font-size: 24px; color: var(--accent-cyan);">ZERO-STEP HOOKS</div>
|
|
307
|
+
<div class="card-detail">Machine-wide core.hooksPath active. Sub-second AST taint inspection on commit/push.</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="grid">
|
|
312
|
+
<!-- Crash Replay Time Machine -->
|
|
313
|
+
<div class="card table-card">
|
|
314
|
+
<div class="card-title">
|
|
315
|
+
<span>Blackbox Flight Events & Crash Timeline</span>
|
|
316
|
+
<button class="btn btn-danger" onclick="panicRevert()">Emergency Salvage Reflog</button>
|
|
317
|
+
</div>
|
|
318
|
+
<table class="event-table">
|
|
319
|
+
<thead>
|
|
320
|
+
<tr>
|
|
321
|
+
<th>Step ID</th>
|
|
322
|
+
<th>Timestamp</th>
|
|
323
|
+
<th>Actor</th>
|
|
324
|
+
<th>Target File</th>
|
|
325
|
+
<th>Action & Motivation</th>
|
|
326
|
+
</tr>
|
|
327
|
+
</thead>
|
|
328
|
+
<tbody id="events-tbody">
|
|
329
|
+
<tr>
|
|
330
|
+
<td colspan="5" style="text-align: center; color: var(--text-muted);">No crash events recorded. Codebase nominal.</td>
|
|
331
|
+
</tr>
|
|
332
|
+
</tbody>
|
|
333
|
+
</table>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div class="grid">
|
|
338
|
+
<div class="card table-card">
|
|
339
|
+
<div class="card-title">
|
|
340
|
+
<span>Live L3 Security & CodeQL Console</span>
|
|
341
|
+
<button class="btn" onclick="runL3Check()">Run Pre-Flight Now</button>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="code-block" id="l3-log">Waiting for pre-flight check trigger...</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<script>
|
|
348
|
+
async function refreshData() {
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetch('/api/telemetry');
|
|
351
|
+
const data = await res.json();
|
|
352
|
+
|
|
353
|
+
// Spend
|
|
354
|
+
document.getElementById('spend-val').innerText = '$' + data.daily_spend.toFixed(4);
|
|
355
|
+
const pct = Math.min(100, (data.daily_spend / data.budget) * 100);
|
|
356
|
+
document.getElementById('spend-pct').innerText = pct.toFixed(1) + '%';
|
|
357
|
+
document.getElementById('spend-bar').style.width = pct + '%';
|
|
358
|
+
document.getElementById('spend-detail').innerText = 'Daily Ceiling: $' + data.budget.toFixed(2) + ' USD';
|
|
359
|
+
|
|
360
|
+
// Hardware
|
|
361
|
+
document.getElementById('cpu-prio').innerText = data.hw.process_priority;
|
|
362
|
+
document.getElementById('hw-detail').innerText = 'Battery: ' + (data.hw.battery_percent ? data.hw.battery_percent + '%' : 'N/A') + ' • Throttle: ' + (data.hw.is_throttled ? 'ACTIVE' : 'NOMINAL');
|
|
363
|
+
|
|
364
|
+
// Events
|
|
365
|
+
const tbody = document.getElementById('events-tbody');
|
|
366
|
+
if (data.events && data.events.length > 0) {
|
|
367
|
+
tbody.innerHTML = data.events.map(e => `
|
|
368
|
+
<tr>
|
|
369
|
+
<td>#${e.step_id}</td>
|
|
370
|
+
<td>${e.timestamp}</td>
|
|
371
|
+
<td>${e.actor}</td>
|
|
372
|
+
<td><code>${e.target_file}</code></td>
|
|
373
|
+
<td>${e.action} - ${e.motivation}</td>
|
|
374
|
+
</tr>
|
|
375
|
+
`).join('');
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error("Telemetry fetch error:", err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function panicRevert() {
|
|
383
|
+
if (!confirm("Execute emergency reflog salvage to rescue uncommitted/lost work?")) return;
|
|
384
|
+
try {
|
|
385
|
+
const res = await fetch('/api/panic_salvage', { method: 'POST' });
|
|
386
|
+
const r = await res.json();
|
|
387
|
+
alert(r.message || "Salvage executed.");
|
|
388
|
+
refreshData();
|
|
389
|
+
} catch (e) {
|
|
390
|
+
alert("Salvage error: " + e);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function runL3Check() {
|
|
395
|
+
document.getElementById('l3-log').innerText = "Running L3 local AST taint and secret scan in <0.2s...";
|
|
396
|
+
try {
|
|
397
|
+
const res = await fetch('/api/l3_check', { method: 'POST' });
|
|
398
|
+
const r = await res.json();
|
|
399
|
+
document.getElementById('l3-log').innerText = r.report;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
document.getElementById('l3-log').innerText = "Error running check: " + e;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
setInterval(refreshData, 2500);
|
|
406
|
+
refreshData();
|
|
407
|
+
</script>
|
|
408
|
+
</body>
|
|
409
|
+
</html>
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class OopsHUDHandler(BaseHTTPRequestHandler):
|
|
414
|
+
"""Zero-dependency HTTP Handler for git-oops HUD."""
|
|
415
|
+
|
|
416
|
+
def log_message(self, format, *args):
|
|
417
|
+
# Silence HTTP server access logs in terminal
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
def do_GET(self):
|
|
421
|
+
if self.path == "/" or self.path.startswith("/?"):
|
|
422
|
+
self.send_response(200)
|
|
423
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
424
|
+
self.end_headers()
|
|
425
|
+
self.wfile.write(HTML_TEMPLATE.encode("utf-8"))
|
|
426
|
+
elif self.path == "/api/telemetry":
|
|
427
|
+
self.send_response(200)
|
|
428
|
+
self.send_header("Content-Type", "application/json")
|
|
429
|
+
self.end_headers()
|
|
430
|
+
|
|
431
|
+
cfg = get_default_config()
|
|
432
|
+
spend = get_daily_spend()
|
|
433
|
+
hw_telemetry = get_hardware_telemetry()
|
|
434
|
+
events = list_flight_events(limit=10)
|
|
435
|
+
|
|
436
|
+
data = {
|
|
437
|
+
"daily_spend": spend,
|
|
438
|
+
"budget": cfg.get("daily_budget", 1.0),
|
|
439
|
+
"hw": hw_telemetry,
|
|
440
|
+
"events": events
|
|
441
|
+
}
|
|
442
|
+
self.wfile.write(json.dumps(data).encode("utf-8"))
|
|
443
|
+
else:
|
|
444
|
+
self.send_response(404)
|
|
445
|
+
self.end_headers()
|
|
446
|
+
|
|
447
|
+
def do_POST(self):
|
|
448
|
+
if self.path == "/api/panic_salvage":
|
|
449
|
+
recovered = run_reflog_autopsy()
|
|
450
|
+
self.send_response(200)
|
|
451
|
+
self.send_header("Content-Type", "application/json")
|
|
452
|
+
self.end_headers()
|
|
453
|
+
msg = f"Salvage complete. Rescued {len(recovered)} lost commit snapshots from reflog."
|
|
454
|
+
self.wfile.write(json.dumps({"success": True, "message": msg}).encode("utf-8"))
|
|
455
|
+
elif self.path == "/api/l3_check":
|
|
456
|
+
passed, violations, report = run_l3_preflight()
|
|
457
|
+
self.send_response(200)
|
|
458
|
+
self.send_header("Content-Type", "application/json")
|
|
459
|
+
self.end_headers()
|
|
460
|
+
self.wfile.write(json.dumps({"passed": passed, "report": report}).encode("utf-8"))
|
|
461
|
+
else:
|
|
462
|
+
self.send_response(404)
|
|
463
|
+
self.end_headers()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def start_hud_server(port: int = 8443, open_browser: bool = True):
|
|
467
|
+
"""Starts the HUD server on specified port (or auto-finds open port)."""
|
|
468
|
+
server = None
|
|
469
|
+
target_port = port
|
|
470
|
+
for p in range(port, port + 20):
|
|
471
|
+
try:
|
|
472
|
+
server = HTTPServer(("127.0.0.1", p), OopsHUDHandler)
|
|
473
|
+
target_port = p
|
|
474
|
+
break
|
|
475
|
+
except OSError:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
if not server:
|
|
479
|
+
print(f"Error: Unable to bind HUD server to any port in range {port}-{port+20}.")
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
url = f"http://127.0.0.1:{target_port}"
|
|
483
|
+
print(f"\n⚡ Git-Oops Obsidian Web HUD active at: {url}")
|
|
484
|
+
print("Press Ctrl+C to stop the dashboard server.\n")
|
|
485
|
+
|
|
486
|
+
if open_browser:
|
|
487
|
+
threading.Thread(target=lambda: (time.sleep(0.5), webbrowser.open(url)), daemon=True).start()
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
server.serve_forever()
|
|
491
|
+
except KeyboardInterrupt:
|
|
492
|
+
print("\nStopping HUD server...")
|
|
493
|
+
server.server_close()
|
src/local_llm.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-Oops Pure-Offline Local LLM Adapter
|
|
3
|
+
Connects directly to local Ollama (http://localhost:11434) or vLLM / LM Studio (http://localhost:8000).
|
|
4
|
+
100% offline, zero data exfiltration, zero token cost, zero external Python dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import urllib.request
|
|
11
|
+
import urllib.error
|
|
12
|
+
from typing import Tuple, Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
DEFAULT_OLLAMA_URL = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
|
|
15
|
+
DEFAULT_VLLM_URL = os.environ.get("VLLM_HOST", "http://localhost:8000")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def probe_local_llm(timeout: float = 0.35) -> Tuple[bool, str, str]:
|
|
19
|
+
"""
|
|
20
|
+
Sub-second probe to detect running local LLM daemon.
|
|
21
|
+
Returns: (is_available: bool, provider_name: str, detected_model: str)
|
|
22
|
+
"""
|
|
23
|
+
# 1. Probe Ollama
|
|
24
|
+
try:
|
|
25
|
+
req = urllib.request.Request(f"{DEFAULT_OLLAMA_URL}/api/tags")
|
|
26
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
27
|
+
if res.status == 200:
|
|
28
|
+
data = json.loads(res.read().decode("utf-8"))
|
|
29
|
+
models = data.get("models", [])
|
|
30
|
+
chosen_model = models[0]["name"] if models else "llama3"
|
|
31
|
+
return True, "Ollama", chosen_model
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
# 2. Probe vLLM / OpenAI-compatible local server
|
|
36
|
+
try:
|
|
37
|
+
req = urllib.request.Request(f"{DEFAULT_VLLM_URL}/v1/models")
|
|
38
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
39
|
+
if res.status == 200:
|
|
40
|
+
data = json.loads(res.read().decode("utf-8"))
|
|
41
|
+
models = data.get("data", [])
|
|
42
|
+
chosen_model = models[0]["id"] if models else "default"
|
|
43
|
+
return True, "vLLM", chosen_model
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
return False, "None", "none"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def call_local_llm(
|
|
51
|
+
prompt: str,
|
|
52
|
+
system_prompt: str = "You are a senior systems engineer repairing code crashes.",
|
|
53
|
+
model: Optional[str] = None,
|
|
54
|
+
timeout: float = 30.0
|
|
55
|
+
) -> Tuple[bool, str]:
|
|
56
|
+
"""
|
|
57
|
+
Invokes local LLM to produce code fix or diagnostic analysis.
|
|
58
|
+
Returns: (success: bool, response_text: str)
|
|
59
|
+
"""
|
|
60
|
+
avail, provider, detected_model = probe_local_llm()
|
|
61
|
+
if not avail:
|
|
62
|
+
return False, "No local LLM daemon detected. Please ensure Ollama or vLLM is running locally."
|
|
63
|
+
|
|
64
|
+
active_model = model or detected_model
|
|
65
|
+
|
|
66
|
+
if provider == "Ollama":
|
|
67
|
+
url = f"{DEFAULT_OLLAMA_URL}/api/chat"
|
|
68
|
+
payload = {
|
|
69
|
+
"model": active_model,
|
|
70
|
+
"messages": [
|
|
71
|
+
{"role": "system", "content": system_prompt},
|
|
72
|
+
{"role": "user", "content": prompt}
|
|
73
|
+
],
|
|
74
|
+
"stream": False,
|
|
75
|
+
"options": {"temperature": 0.1}
|
|
76
|
+
}
|
|
77
|
+
try:
|
|
78
|
+
req = urllib.request.Request(
|
|
79
|
+
url,
|
|
80
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
81
|
+
headers={"Content-Type": "application/json"}
|
|
82
|
+
)
|
|
83
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
84
|
+
if res.status == 200:
|
|
85
|
+
resp_json = json.loads(res.read().decode("utf-8"))
|
|
86
|
+
content = resp_json.get("message", {}).get("content", "")
|
|
87
|
+
return True, content
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return False, f"Ollama local inference failed: {str(e)}"
|
|
90
|
+
|
|
91
|
+
elif provider == "vLLM":
|
|
92
|
+
url = f"{DEFAULT_VLLM_URL}/v1/chat/completions"
|
|
93
|
+
payload = {
|
|
94
|
+
"model": active_model,
|
|
95
|
+
"messages": [
|
|
96
|
+
{"role": "system", "content": system_prompt},
|
|
97
|
+
{"role": "user", "content": prompt}
|
|
98
|
+
],
|
|
99
|
+
"temperature": 0.1
|
|
100
|
+
}
|
|
101
|
+
try:
|
|
102
|
+
req = urllib.request.Request(
|
|
103
|
+
url,
|
|
104
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
105
|
+
headers={"Content-Type": "application/json"}
|
|
106
|
+
)
|
|
107
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
108
|
+
if res.status == 200:
|
|
109
|
+
resp_json = json.loads(res.read().decode("utf-8"))
|
|
110
|
+
choices = resp_json.get("choices", [])
|
|
111
|
+
content = choices[0]["message"]["content"] if choices else ""
|
|
112
|
+
return True, content
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return False, f"vLLM local inference failed: {str(e)}"
|
|
115
|
+
|
|
116
|
+
return False, "Unsupported local LLM provider."
|