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.
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."