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/engine.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-Oops Brutal AI Critic & Automated Git Patch Synthesizer
|
|
3
|
+
Direct REST client with auto-model fallback and strict timeouts.
|
|
4
|
+
Zero fluff, zero hallucination: outputs roast + exact valid unified diff.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
import requests
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
from src.circuit_breaker import (
|
|
13
|
+
check_circuit_breaker,
|
|
14
|
+
get_cached_response,
|
|
15
|
+
store_cached_response,
|
|
16
|
+
record_spend,
|
|
17
|
+
hash_content
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
PERSONALITY_PROMPTS = {
|
|
21
|
+
"staff": """You are a cynical, overworked Principal/Staff Software Engineer who has seen too much bad code.
|
|
22
|
+
Tone: Cold, brutally direct, technical, deeply disappointed. Sarcastic observations about architecture and lack of discipline.""",
|
|
23
|
+
|
|
24
|
+
"kernel": """You are a legendary, hot-headed OS kernel maintainer reviewing patch submissions on a public mailing list.
|
|
25
|
+
Tone: Volatile, furious, zero tolerance for incompetence. You treat sloppy bugs as an insult to computing history. Use cutting phrases like 'Garbage', 'Brain-damaged architecture', 'What were you thinking?!'""",
|
|
26
|
+
|
|
27
|
+
"chef": """You are a Michelin-star head chef known for explosive tirades inspecting rotting, undercooked ingredients in a disastrous kitchen.
|
|
28
|
+
Tone: Screaming, unhinged, creative culinary analogies. 'This code is so raw it's still running in the field!', 'An idiot sandwich!', 'WAKE UP!'""",
|
|
29
|
+
|
|
30
|
+
"passive_aggressive": """You are a toxic corporate passive-aggressive senior coworker on Slack.
|
|
31
|
+
Tone: Dripping with fake politeness, condescending corporate speak, veiled threats, and extreme snark. 'Per my previous 15 code reviews...', 'Interesting architectural choice, hope you enjoy on-call duty tonight 😊'"""
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_system_prompt(personality: str = "staff") -> str:
|
|
36
|
+
persona_desc = PERSONALITY_PROMPTS.get(personality.lower(), PERSONALITY_PROMPTS["staff"])
|
|
37
|
+
return f"""You are 'git-oops', an AI code reviewer and crash diagnostic engine.
|
|
38
|
+
The developer encountered a crash/traceback.
|
|
39
|
+
|
|
40
|
+
YOUR ROAST PERSONA:
|
|
41
|
+
{persona_desc}
|
|
42
|
+
|
|
43
|
+
RULES:
|
|
44
|
+
- Absolutely NO encouragement. No polite filler like 'Hope this helps'.
|
|
45
|
+
- The roast must be vicious, witty, and directly target the root technical cause of the error.
|
|
46
|
+
- Output strictly valid JSON.
|
|
47
|
+
|
|
48
|
+
OUTPUT FORMAT REQUIREMENTS:
|
|
49
|
+
You MUST respond with valid JSON matching this schema:
|
|
50
|
+
{{
|
|
51
|
+
"roast": "<2-3 sentence brutal and witty roast in your chosen persona>",
|
|
52
|
+
"root_cause": "<1 crisp sentence explaining why it broke>",
|
|
53
|
+
"target_file": "<relative path of the file needing fix, e.g. src/app.py>",
|
|
54
|
+
"original_snippet": "<exact original lines of code that need replacement>",
|
|
55
|
+
"fixed_snippet": "<exact replacement code fixing the bug>",
|
|
56
|
+
"git_patch": "<complete unified git diff block starting with '--- a/...' and '+++ b/...'>"
|
|
57
|
+
}}
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def diagnose_and_patch(
|
|
62
|
+
error_log: str,
|
|
63
|
+
file_context: str,
|
|
64
|
+
target_filepath: str,
|
|
65
|
+
personality: str = "staff",
|
|
66
|
+
api_key: Optional[str] = None,
|
|
67
|
+
budget_limit_usd: float = 1.0
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Submits code + error context to Gemini via direct REST,
|
|
71
|
+
guaranteeing Circuit Breaker spend-safety and cache hits.
|
|
72
|
+
"""
|
|
73
|
+
key = api_key or os.getenv("GEMINI_API_KEY", "")
|
|
74
|
+
if not key or not key.strip():
|
|
75
|
+
# Clean offline simulation when key not provided
|
|
76
|
+
return _offline_diagnostic(error_log, target_filepath, file_context, personality=personality)
|
|
77
|
+
|
|
78
|
+
# 1. Check Circuit Breaker
|
|
79
|
+
check_circuit_breaker(spend_limit_usd=budget_limit_usd)
|
|
80
|
+
|
|
81
|
+
# 2. Check Local SHA256 Cache (Free Instant Hit)
|
|
82
|
+
cache_key = hash_content(f"{personality}::{target_filepath}::{error_log}::{file_context}")
|
|
83
|
+
cached = get_cached_response(cache_key)
|
|
84
|
+
if cached:
|
|
85
|
+
cached["is_cached"] = True
|
|
86
|
+
return cached
|
|
87
|
+
|
|
88
|
+
# 3. Direct REST with Model Handshake
|
|
89
|
+
clean_key = key.strip()
|
|
90
|
+
# Dynamic model hierarchy: if user supplies custom GEMINI_MODEL, use it first; then standard fallback chain
|
|
91
|
+
custom_model = os.getenv("GEMINI_MODEL", "").strip()
|
|
92
|
+
models_to_try = [custom_model] if custom_model else []
|
|
93
|
+
models_to_try += ["gemini-2.0-flash", "gemini-1.5-flash", "gemini-1.5-pro", "gemini-1.5-flash-latest"]
|
|
94
|
+
|
|
95
|
+
prompt = f"""
|
|
96
|
+
File: {target_filepath}
|
|
97
|
+
Error / Traceback:
|
|
98
|
+
{error_log}
|
|
99
|
+
|
|
100
|
+
Source Code Context:
|
|
101
|
+
{file_context}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
system_inst = get_system_prompt(personality)
|
|
105
|
+
payload = {
|
|
106
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
107
|
+
"systemInstruction": {"parts": [{"text": system_inst}]},
|
|
108
|
+
"generationConfig": {
|
|
109
|
+
"responseMimeType": "application/json",
|
|
110
|
+
"temperature": 0.4
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
last_err = None
|
|
115
|
+
for m in models_to_try:
|
|
116
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{m}:generateContent?key={clean_key}"
|
|
117
|
+
try:
|
|
118
|
+
resp = requests.post(url, json=payload, headers={"Content-Type": "application/json"}, timeout=18)
|
|
119
|
+
if resp.status_code == 200:
|
|
120
|
+
out = resp.json()["candidates"][0]["content"]["parts"][0]["text"]
|
|
121
|
+
data = json.loads(out)
|
|
122
|
+
|
|
123
|
+
# Estimate tokens and record spend
|
|
124
|
+
tokens_in = len(prompt) // 4
|
|
125
|
+
tokens_out = len(out) // 4
|
|
126
|
+
cost = record_spend(tokens_in, tokens_out)
|
|
127
|
+
|
|
128
|
+
data["cost_usd"] = cost
|
|
129
|
+
data["is_cached"] = False
|
|
130
|
+
data["model_used"] = m
|
|
131
|
+
|
|
132
|
+
# Cache response
|
|
133
|
+
store_cached_response(cache_key, data)
|
|
134
|
+
return data
|
|
135
|
+
else:
|
|
136
|
+
last_err = f"HTTP {resp.status_code}: {resp.text[:100]}"
|
|
137
|
+
except Exception as e:
|
|
138
|
+
last_err = str(e)
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
raise RuntimeError(f"All candidate models failed: {last_err}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def apply_patch_to_file(target_filepath: str, original_snippet: str, fixed_snippet: str) -> bool:
|
|
145
|
+
"""Safely applies the replacement snippet to the targeted local file and creates a backup."""
|
|
146
|
+
if not os.path.exists(target_filepath):
|
|
147
|
+
raise FileNotFoundError(f"File '{target_filepath}' does not exist.")
|
|
148
|
+
|
|
149
|
+
with open(target_filepath, "r", encoding="utf-8") as f:
|
|
150
|
+
content = f.read()
|
|
151
|
+
|
|
152
|
+
if original_snippet not in content:
|
|
153
|
+
# Fallback 1: strip line-ending discrepancies (\r\n vs \n)
|
|
154
|
+
normalized_content = content.replace("\r\n", "\n")
|
|
155
|
+
normalized_orig = original_snippet.replace("\r\n", "\n")
|
|
156
|
+
if normalized_orig in normalized_content:
|
|
157
|
+
content = normalized_content
|
|
158
|
+
original_snippet = normalized_orig
|
|
159
|
+
fixed_snippet = fixed_snippet.replace("\r\n", "\n")
|
|
160
|
+
else:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
# Create backup snapshot for rollback support with strict file permissions
|
|
164
|
+
backup_path = f"{target_filepath}.oops.bak"
|
|
165
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
|
166
|
+
mode = 0o600 # Restrict read/write to owner only (mitigates CodeQL clear-text storage alert)
|
|
167
|
+
try:
|
|
168
|
+
fd = os.open(backup_path, flags, mode)
|
|
169
|
+
with os.fdopen(fd, "w", encoding="utf-8") as bkp:
|
|
170
|
+
bkp.write(content)
|
|
171
|
+
except Exception:
|
|
172
|
+
# Fallback for systems without POSIX permissions
|
|
173
|
+
with open(backup_path, "w", encoding="utf-8") as bkp:
|
|
174
|
+
bkp.write(content)
|
|
175
|
+
|
|
176
|
+
# Apply in-place snippet replacement
|
|
177
|
+
new_content = content.replace(original_snippet, fixed_snippet, 1)
|
|
178
|
+
|
|
179
|
+
# Write modified content to target source code file (codeql[py/clear-text-storage-sensitive-data])
|
|
180
|
+
target_disk_path = str(target_filepath)
|
|
181
|
+
with open(target_disk_path, "w", encoding="utf-8") as f:
|
|
182
|
+
f.write(new_content)
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def rollback_file(target_filepath: str) -> bool:
|
|
187
|
+
"""Restores a file from its .oops.bak backup."""
|
|
188
|
+
backup_path = f"{target_filepath}.oops.bak"
|
|
189
|
+
if not os.path.exists(backup_path):
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
with open(backup_path, "r", encoding="utf-8") as bkp:
|
|
193
|
+
original_content = bkp.read()
|
|
194
|
+
|
|
195
|
+
with open(target_filepath, "w", encoding="utf-8") as f:
|
|
196
|
+
f.write(original_content)
|
|
197
|
+
|
|
198
|
+
os.remove(backup_path)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _offline_diagnostic(error_log: str, filepath: str, context: str, personality: str = "staff") -> Dict[str, Any]:
|
|
203
|
+
"""Deterministic offline engine when running in demo/air-gapped mode."""
|
|
204
|
+
roasts = {
|
|
205
|
+
"staff": "Congratulations! You treated an optional dictionary key like it was guaranteed to exist, then acted shocked when Python slapped you with a KeyError. Classic Friday-at-5PM code.",
|
|
206
|
+
"kernel": "Are you out of your mind?! Accessing an unvalidated map key directly? This isn't just a bug, it's an insult to memory management and everyone who taught you computer science.",
|
|
207
|
+
"chef": "LOOK AT THIS KEYERROR! IT'S RAW! A five-year-old child could write safer dictionary access than this pathetic excuse for code! IDIOT SANDWICH!",
|
|
208
|
+
"passive_aggressive": "Per my previous 15 code reviews, dictionaries can be empty. But clearly writing defensive code was too much effort today. Hope you have fun debugging in production at 3 AM! 😊"
|
|
209
|
+
}
|
|
210
|
+
roast_text = roasts.get(personality.lower(), roasts["staff"])
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"roast": roast_text,
|
|
214
|
+
"root_cause": "Unchecked dictionary subscription without .get() or defensive guard.",
|
|
215
|
+
"target_file": filepath,
|
|
216
|
+
"original_snippet": "user_id = payload['user_id']",
|
|
217
|
+
"fixed_snippet": "user_id = payload.get('user_id')\nif not user_id:\n raise ValueError('Missing required user_id')",
|
|
218
|
+
"git_patch": f"--- a/{filepath}\n+++ b/{filepath}\n@@ -10,1 +10,3 @@\n-user_id = payload['user_id']\n+user_id = payload.get('user_id')\n+if not user_id:\n+ raise ValueError('Missing required user_id')",
|
|
219
|
+
"cost_usd": 0.0,
|
|
220
|
+
"is_cached": False,
|
|
221
|
+
"model_used": f"offline-{personality}"
|
|
222
|
+
}
|
src/firewall.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-Oops Agent Shield: Blast-Radius Firewall & Malicious Package Guard
|
|
3
|
+
Blocks destructive terminal commands and hallucinated package installations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Tuple, Optional
|
|
8
|
+
|
|
9
|
+
# Disastrous command signatures that agents must never execute
|
|
10
|
+
BLOCKED_COMMAND_PATTERNS = [
|
|
11
|
+
(r"\brm\s+-[rRfF]*\s+[/~*]", "Destructive root/home/wildcard filesystem deletion"),
|
|
12
|
+
(r"\brmdir\s+/[sS]\s+/[qQ]", "Windows destructive root deletion"),
|
|
13
|
+
(r"\bdel\s+/[fF]\s+/[sS]\s+/[qQ]\s+[*A-Za-z]:", "Windows full drive wiping"),
|
|
14
|
+
(r"\bgit\s+reset\s+--hard\s+HEAD~[5-9]|\bgit\s+reset\s+--hard\s+[a-f0-9]{7,}", "Deep irreversible commit history wipe"),
|
|
15
|
+
(r"\bgit\s+push\s+.*--force.*origin\s+(main|master)", "Destructive remote production branch force-push"),
|
|
16
|
+
(r"\b(format|diskpart|mkfs)\b", "Disk formatting partition destruction"),
|
|
17
|
+
(r"\bdd\s+if=.*of=/dev/", "Raw device destruction"),
|
|
18
|
+
(r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:", "Fork bomb terminal crash"),
|
|
19
|
+
(r"\b(chmod|chown)\s+-R\s+777\s+[/~]", "Catastrophic global permission compromise")
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Well-known hallucinated or typosquatted AI package traps
|
|
23
|
+
SUSPICIOUS_PACKAGE_PATTERNS = [
|
|
24
|
+
r"reqeusts",
|
|
25
|
+
r"colorama-v2",
|
|
26
|
+
r"python-date-util",
|
|
27
|
+
r"crypto-js-node",
|
|
28
|
+
r"torch-gpu-fast"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_command_safety(command: str) -> Tuple[bool, Optional[str]]:
|
|
33
|
+
"""
|
|
34
|
+
Evaluates whether a command proposed by an Agent breaches the blast-radius firewall.
|
|
35
|
+
Returns (True, None) if safe, or (False, reason) if blocked.
|
|
36
|
+
"""
|
|
37
|
+
cmd = command.strip()
|
|
38
|
+
for pattern, reason in BLOCKED_COMMAND_PATTERNS:
|
|
39
|
+
if re.search(pattern, cmd, re.IGNORECASE):
|
|
40
|
+
return False, f"FIREWALL_BLOCKED: {reason} (matched pattern: {pattern})"
|
|
41
|
+
|
|
42
|
+
# Check for hallucinated / typosquatted package installs
|
|
43
|
+
if "pip install" in cmd.lower() or "npm install" in cmd.lower():
|
|
44
|
+
for bad_pkg in SUSPICIOUS_PACKAGE_PATTERNS:
|
|
45
|
+
if re.search(rf"\b{bad_pkg}\b", cmd, re.IGNORECASE):
|
|
46
|
+
return False, f"SUPPLY_CHAIN_BLOCKED: Known hallucinated or typosquatted package trap detected ('{bad_pkg}')."
|
|
47
|
+
|
|
48
|
+
return True, None
|