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/cli.py ADDED
@@ -0,0 +1,556 @@
1
+ """
2
+ Git-Oops CLI Entrypoint
3
+ Interactive terminal UX with rich color support and safety controls.
4
+ Usage:
5
+ git-oops --file src/app.py --error "TypeError: ..."
6
+ git-oops --status
7
+ git-oops --set-budget 2.0
8
+ """
9
+
10
+ import sys
11
+ import os
12
+ import json
13
+ import argparse
14
+ import subprocess
15
+
16
+ # Ensure standard UTF-8 output on Windows consoles to prevent cp1252 charmap crashes
17
+ if sys.platform == "win32":
18
+ try:
19
+ sys.stdout.reconfigure(encoding="utf-8")
20
+ sys.stderr.reconfigure(encoding="utf-8")
21
+ except Exception:
22
+ pass
23
+
24
+ from src.circuit_breaker import get_daily_spend, get_default_config, set_config_value
25
+ from src.engine import diagnose_and_patch, apply_patch_to_file, rollback_file
26
+ from src.firewall import check_command_safety
27
+ from src.squeezer import compress_traceback, redact_secrets
28
+ from src.watchdog import check_anti_thrashing, acquire_agent_lock, release_agent_lock
29
+ from src.guardian import verify_patch_in_sandbox, check_ghost_imports
30
+ from src.blackbox import record_step, rewind_step, list_flight_events, register_antibody, match_antibody
31
+ from src.mcp_server import run_mcp_server
32
+ from src.shadow import ShadowWorkspace
33
+ from src.consensus import arbitrate_consensus
34
+ from src.sentinel import check_egress_security, check_license_taint, check_code_complexity, IntentPrefetchEngine
35
+ from src.hud import start_hud_server
36
+ from src.hooks import run_l3_preflight, run_pre_push_check, setup_global_hooks, ensure_auto_hooks
37
+ from src.rules import trigger_ota_update_background, sync_security_rules_foreground
38
+ from src.replay import inspect_crash_snapshot
39
+ from src.salvage import run_reflog_autopsy
40
+ from src.solver import solve_with_ast
41
+ from src.supply_chain import scan_dependencies_file
42
+ from src.reviewer import review_pull_request, format_pr_review_markdown
43
+ from src.hardware import get_hardware_telemetry
44
+ from src.tester import synthesize_regression_test
45
+ from src.local_llm import probe_local_llm, call_local_llm
46
+ from src.ui import (
47
+ print_banner,
48
+ draw_card,
49
+ render_diff,
50
+ spin_task,
51
+ FG_CYAN,
52
+ FG_MAGENTA,
53
+ FG_YELLOW,
54
+ FG_GREEN,
55
+ FG_RED,
56
+ FG_WHITE,
57
+ FG_GRAY,
58
+ BOLD,
59
+ ITALIC,
60
+ RESET
61
+ )
62
+
63
+
64
+ def main():
65
+ saved_cfg = get_default_config()
66
+
67
+ parser = argparse.ArgumentParser(
68
+ prog="git-oops",
69
+ description="Brutally honest terminal crash fixer with daily spend-protection circuit breaker."
70
+ )
71
+ parser.add_argument("-f", "--file", help="Path to the file that crashed", default=None)
72
+ parser.add_argument("-e", "--error", help="Error message or traceback string", default=None)
73
+ parser.add_argument("-b", "--budget", help="Daily API spend ceiling in USD (default: $1.00)", type=float, default=saved_cfg.get("daily_budget", 1.0))
74
+ parser.add_argument("-p", "--personality", help="Roast persona: 'staff', 'kernel', 'chef', or 'passive_aggressive'", default=saved_cfg.get("personality", "staff"), choices=["staff", "kernel", "chef", "passive_aggressive"])
75
+ parser.add_argument("--set-personality", help="Permanently save default roast persona", choices=["staff", "kernel", "chef", "passive_aggressive"])
76
+ parser.add_argument("--set-budget", help="Permanently save default daily spend budget in USD", type=float)
77
+ parser.add_argument("-y", "--yes", help="Auto-apply patch without interactive prompt", action="store_true")
78
+ parser.add_argument("--undo", help="Roll back the last applied patch on target file", default=None)
79
+ parser.add_argument("--run", help="Execute a command and auto-catch its crash (e.g. --run 'python app.py')", default=None)
80
+ parser.add_argument("--status", help="Show today's accumulated API spend", action="store_true")
81
+ parser.add_argument("--agent", "--json", dest="agent_mode", help="Headless Agent mode: emits machine-readable JSON without ANSI codes", action="store_true")
82
+ parser.add_argument("--mcp", help="Start git-oops as a native Model Context Protocol (MCP) server over stdio", action="store_true")
83
+ parser.add_argument("--rewind", help="Flight Recorder: rewind codebase to a previous step ID", type=int, default=None)
84
+ parser.add_argument("--flight-log", help="Display recent blackbox flight recorder steps", action="store_true")
85
+ parser.add_argument("--shadow", help="Run repair verification inside an isolated temporary shadow workspace", action="store_true")
86
+ parser.add_argument("--consensus", help="Multi-LLM cross-arbitration: require two models to reach consensus before patching", action="store_true")
87
+
88
+ parser.add_argument("--hud", help="Start Obsidian Glassmorphism Web HUD server", action="store_true")
89
+ parser.add_argument("--pre-commit", help="Execute sub-second L3 pre-commit security pre-flight", action="store_true")
90
+ parser.add_argument("--pre-push", help="Execute pre-push branch and outgoing commit verification", action="store_true")
91
+ parser.add_argument("--install-hooks", help="Force install machine-wide git hooks via core.hooksPath", action="store_true")
92
+ parser.add_argument("--replay", help="Inspect crash variable snapshot (e.g. --replay crash_123.json)", default=None)
93
+ parser.add_argument("--salvage", help="Execute git reflog autopsy to rescue lost commits", action="store_true")
94
+ parser.add_argument("--solver", help="Zero-AI 1ms deterministic AST symbolic crash solver", action="store_true")
95
+ parser.add_argument("--supply-chain", help="Audit dependencies file for typosquatting & vulnerabilities", default=None)
96
+ parser.add_argument("--review-pr", help="Pre-PR gatekeeper audit against target branch (default: origin/main)", nargs="?", const="DEFAULT", default=None)
97
+ parser.add_argument("--hardware", help="Display hardware telemetry and CPU throttling metrics", action="store_true")
98
+ parser.add_argument("--test-gen", help="Generate permanent regression test for error", action="store_true")
99
+ parser.add_argument("--local", "--ollama", dest="local_mode", help="Use local offline LLM (Ollama/vLLM) instead of cloud APIs", action="store_true")
100
+ parser.add_argument("--update-rules", help="Force OTA update of L3 dynamic security rules", action="store_true")
101
+
102
+ args = parser.parse_args()
103
+
104
+ # Zero-step background bootstrap: auto-hooks & OTA rules
105
+ ensure_auto_hooks()
106
+ trigger_ota_update_background()
107
+
108
+ # Force OTA rule update
109
+ if args.update_rules:
110
+ ok, msg = sync_security_rules_foreground(force=True)
111
+ if args.agent_mode:
112
+ print(json.dumps({"success": ok, "message": msg}))
113
+ else:
114
+ print_banner()
115
+ draw_card("DYNAMIC OTA SECURITY RULES", msg, color_border=FG_GREEN if ok else FG_YELLOW, color_title=FG_GREEN if ok else FG_YELLOW)
116
+ return
117
+
118
+ # Obsidian Web HUD Mode
119
+ if args.hud:
120
+ start_hud_server()
121
+ return
122
+
123
+ # L3 Pre-Commit Hook Mode
124
+ if args.pre_commit:
125
+ passed, violations, report = run_l3_preflight()
126
+ if args.agent_mode:
127
+ print(json.dumps({"passed": passed, "violations_count": len(violations), "violations": violations}))
128
+ else:
129
+ print(report)
130
+ sys.exit(0 if passed else 1)
131
+
132
+ # Pre-Push Hook Mode
133
+ if args.pre_push:
134
+ passed, report = run_pre_push_check()
135
+ if args.agent_mode:
136
+ print(json.dumps({"passed": passed, "report": report}))
137
+ else:
138
+ print(report)
139
+ sys.exit(0 if passed else 1)
140
+
141
+ # Force Install Hooks
142
+ if args.install_hooks:
143
+ ok, msg = setup_global_hooks(force=True)
144
+ if args.agent_mode:
145
+ print(json.dumps({"success": ok, "message": msg}))
146
+ else:
147
+ print_banner()
148
+ draw_card("GLOBAL GIT HOOKS", msg, color_border=FG_GREEN if ok else FG_RED, color_title=FG_GREEN if ok else FG_RED)
149
+ return
150
+
151
+ # Replay Crash Snapshot
152
+ if args.replay:
153
+ snap = inspect_crash_snapshot(args.replay)
154
+ if args.agent_mode:
155
+ print(json.dumps(snap))
156
+ else:
157
+ print_banner()
158
+ if snap.get("status") == "NOT_FOUND":
159
+ draw_card("CRASH REPLAY", f"Snapshot '{args.replay}' not found.", color_border=FG_RED, color_title=FG_RED)
160
+ else:
161
+ body = f"Timestamp: {snap.get('timestamp')}\nTarget: {snap.get('target_file')}\nException: {snap.get('exception_type')}: {snap.get('exception_message')}\n\nLocal Variables:\n{json.dumps(snap.get('local_variables', {}), indent=2)}"
162
+ draw_card("CRASH REPLAY TIME MACHINE", body, color_border=FG_CYAN, color_title=FG_CYAN)
163
+ return
164
+
165
+ # Reflog Salvage Autopsy
166
+ if args.salvage:
167
+ recovered = run_reflog_autopsy()
168
+ if args.agent_mode:
169
+ print(json.dumps({"recovered_count": len(recovered), "commits": recovered}))
170
+ else:
171
+ print_banner()
172
+ if recovered:
173
+ commits_str = "\n".join([f"• [{c['sha'][:7]}] {c['subject']} (Lost at: {c['reflog_action']})" for c in recovered[:10]])
174
+ draw_card("REFLOG AUTOPSY RESCUE", f"Rescued {len(recovered)} lost commit snapshots:\n\n{commits_str}", color_border=FG_GREEN, color_title=FG_GREEN)
175
+ else:
176
+ draw_card("REFLOG AUTOPSY", "No lost or orphaned commits detected in reflog.", color_border=FG_CYAN, color_title=FG_CYAN)
177
+ return
178
+
179
+ # Supply Chain Audit
180
+ if args.supply_chain:
181
+ results = scan_dependencies_file(args.supply_chain)
182
+ if args.agent_mode:
183
+ print(json.dumps(results))
184
+ else:
185
+ print_banner()
186
+ res_str = f"Scanned {results['scanned_packages_count']} dependencies in '{args.supply_chain}'.\n\n"
187
+ if results["violations"]:
188
+ res_str += "🚨 VIOLATIONS DETECTED:\n" + "\n".join([f"• {v['package']}: {v['details']}" for v in results["violations"]])
189
+ draw_card("SUPPLY CHAIN SECURITY ALERT", res_str, color_border=FG_RED, color_title=FG_RED)
190
+ else:
191
+ res_str += "✅ Zero typosquatting or malicious packages detected."
192
+ draw_card("SUPPLY CHAIN SECURITY PASSED", res_str, color_border=FG_GREEN, color_title=FG_GREEN)
193
+ return
194
+
195
+ # Pre-PR Gatekeeper Review
196
+ if args.review_pr is not None:
197
+ target_b = None if args.review_pr == "DEFAULT" else args.review_pr
198
+ report = review_pull_request(target_b)
199
+ if args.agent_mode:
200
+ print(json.dumps(report))
201
+ else:
202
+ print_banner()
203
+ md = format_pr_review_markdown(report)
204
+ border_c = FG_GREEN if report["verdict"] == "APPROVED" else (FG_YELLOW if report["verdict"] == "WARNINGS" else FG_RED)
205
+ draw_card("PRE-PR GATEKEEPER AUDIT", md, color_border=border_c, color_title=border_c)
206
+ return
207
+
208
+ # Hardware Telemetry
209
+ if args.hardware:
210
+ hw = get_hardware_telemetry()
211
+ if args.agent_mode:
212
+ print(json.dumps(hw))
213
+ else:
214
+ print_banner()
215
+ b_text = f"{hw['battery_percent']}%" if hw['battery_percent'] else "N/A"
216
+ hw_str = (
217
+ f"Operating System : {hw['platform']}\n"
218
+ f"Battery Level : {b_text} (Plugged in: {hw['power_plugged']})\n"
219
+ f"Process Priority : {hw['process_priority']}\n"
220
+ f"Throttle Active : {'YES' if hw['is_throttled'] else 'NO'}"
221
+ )
222
+ draw_card("HARDWARE & THROTTLE TELEMETRY", hw_str, color_border=FG_CYAN, color_title=FG_CYAN)
223
+ return
224
+
225
+ # MCP Server Mode
226
+ if args.mcp:
227
+ run_mcp_server()
228
+ return
229
+
230
+ # Flight Recorder Rewind
231
+ if args.rewind is not None:
232
+ ok = rewind_step(args.rewind)
233
+ if args.agent_mode:
234
+ print(json.dumps({"success": ok, "rewound_to_step": args.rewind}))
235
+ else:
236
+ print_banner()
237
+ if ok:
238
+ draw_card("FLIGHT RECORDER REWIND", f"Successfully rewound file to snapshot from step {args.rewind}.", color_border=FG_GREEN, color_title=FG_GREEN)
239
+ else:
240
+ draw_card("REWIND FAILED", f"Could not find snapshot for step {args.rewind}.", color_border=FG_RED, color_title=FG_RED)
241
+ return
242
+
243
+ # Flight Log display
244
+ if args.flight_log:
245
+ events = list_flight_events(limit=10)
246
+ if args.agent_mode:
247
+ print(json.dumps({"events": events}))
248
+ else:
249
+ print_banner()
250
+ log_str = "\n".join([f"• [{e['step_id']}] {e['timestamp']} - {e['action']} on {e['target_file']}: {e['motivation']}" for e in events]) if events else "No events recorded yet."
251
+ draw_card("BLACKBOX FLIGHT RECORDER LOG", log_str, color_border=FG_CYAN, color_title=FG_CYAN)
252
+ return
253
+
254
+ # Save permanent config
255
+ if args.set_personality:
256
+ set_config_value("personality", args.set_personality)
257
+ print_banner()
258
+ draw_card(
259
+ "CONFIGURATION SAVED",
260
+ f"Default persona updated to: {FG_YELLOW}{BOLD}{args.set_personality}{RESET}\nAll future crashes will be roasted in this style.",
261
+ color_border=FG_GREEN,
262
+ color_title=FG_GREEN
263
+ )
264
+ return
265
+
266
+ if args.set_budget is not None:
267
+ set_config_value("daily_budget", args.set_budget)
268
+ print_banner()
269
+ draw_card(
270
+ "CIRCUIT BREAKER UPDATED",
271
+ f"Daily spending limit hard-capped at: {FG_GREEN}{BOLD}${args.set_budget:.2f} USD{RESET}\nRequests will automatically abort if spend exceeds this threshold.",
272
+ color_border=FG_GREEN,
273
+ color_title=FG_GREEN
274
+ )
275
+ return
276
+
277
+ # Budget status check
278
+ if args.status:
279
+ spent = get_daily_spend()
280
+ print_banner()
281
+ status_text = (
282
+ f"Accumulated Spend Today : {FG_GREEN}{BOLD}${spent:.5f} USD{RESET}\n"
283
+ f"Daily Safety Ceiling : {FG_YELLOW}{BOLD}${args.budget:.2f} USD{RESET}\n"
284
+ f"Remaining Budget Safe : {FG_CYAN}{BOLD}${max(0.0, args.budget - spent):.5f} USD{RESET}\n"
285
+ f"Active Roast Persona : {FG_MAGENTA}{BOLD}{args.personality}{RESET}"
286
+ )
287
+ draw_card("CIRCUIT BREAKER METRICS", status_text, color_border=FG_CYAN, color_title=FG_WHITE)
288
+ return
289
+
290
+ # Undo / Rollback
291
+ if args.undo:
292
+ success = rollback_file(args.undo)
293
+ print_banner()
294
+ if success:
295
+ draw_card("SAFETY ROLLBACK", f"Successfully restored {BOLD}'{args.undo}'{RESET} from its previous backup snapshot.\nOriginal code state reinstated.", color_border=FG_GREEN, color_title=FG_GREEN)
296
+ else:
297
+ draw_card("ROLLBACK FAILED", f"No safety backup found for {BOLD}'{args.undo}'{RESET}.\nExpected snapshot: '{args.undo}.oops.bak'", color_border=FG_RED, color_title=FG_RED)
298
+ return
299
+
300
+ # Auto-run execution
301
+ if args.run:
302
+ # 1. Check Blast-Radius Firewall
303
+ safe, reason = check_command_safety(args.run)
304
+ if not safe:
305
+ if args.agent_mode:
306
+ print(json.dumps({"status": "BLOCKED", "reason": reason}))
307
+ else:
308
+ print_banner()
309
+ draw_card("BLAST-RADIUS FIREWALL VIOLATION", reason, color_border=FG_RED, color_title=FG_RED)
310
+ return
311
+
312
+ if not args.agent_mode:
313
+ print_banner()
314
+ draw_card("COMMAND RUNNER", f"Executing: {args.run}\nInterception active.", color_border=FG_CYAN, color_title=FG_CYAN)
315
+
316
+ proc = subprocess.run(args.run, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace")
317
+ if proc.returncode == 0:
318
+ if args.agent_mode:
319
+ print(json.dumps({"status": "SUCCESS", "exit_code": 0, "stdout": proc.stdout}))
320
+ else:
321
+ print(proc.stdout)
322
+ draw_card("EXECUTION SUCCESS", "Command completed cleanly without errors.", color_border=FG_GREEN, color_title=FG_GREEN)
323
+ return
324
+
325
+ # Captured an error -> Compress and Redact Secrets
326
+ raw_err = (proc.stderr or "") + "\n" + (proc.stdout or "")
327
+ compressed_err = compress_traceback(raw_err)
328
+
329
+ if not args.file:
330
+ import re
331
+ file_matches = re.findall(r'File "([^"]+)", line \d+', raw_err)
332
+ if file_matches:
333
+ for cand in reversed(file_matches):
334
+ if os.path.exists(cand):
335
+ args.file = cand
336
+ break
337
+
338
+ args.error = compressed_err
339
+
340
+ # Check inputs
341
+ if not args.file or not args.error:
342
+ # Check if piped from stdin
343
+ if not sys.stdin.isatty():
344
+ piped_error = sys.stdin.read().strip()
345
+ args.error = piped_error
346
+
347
+ if not args.file or not args.error:
348
+ print(f"{RED}Error: Must provide --file and --error (or pipe error via stdin).{RESET}")
349
+ print(f"Example: {CYAN}git-oops --file app.py --error \"NameError: name 'x' is not defined\"{RESET}")
350
+ return
351
+
352
+ filepath = args.file
353
+ if not os.path.exists(filepath):
354
+ if args.agent_mode:
355
+ print(json.dumps({"error": f"File '{filepath}' not found"}))
356
+ else:
357
+ print(f"{FG_RED}Error: File '{filepath}' not found.{RESET}")
358
+ return
359
+
360
+ with open(filepath, "r", encoding="utf-8") as f:
361
+ file_content = f.read()
362
+
363
+ # Check for Antibody Match
364
+ antibody = match_antibody(args.error)
365
+ if antibody and not args.agent_mode:
366
+ draw_card("ANTIBODY IMMUNITY RECALL", f"Recognized known architectural pattern:\n{antibody['explanation']}", color_border=FG_GREEN, color_title=FG_GREEN)
367
+
368
+ if not args.agent_mode:
369
+ print_banner()
370
+ spin_task("Analyzing stack trace & scanning codebase boundaries...", duration=0.6)
371
+ spin_task(f"Invoking critic persona '{args.personality.upper()}'...", duration=0.5)
372
+
373
+ try:
374
+ # 1. Check Zero-AI Deterministic AST Solver first if requested or applicable
375
+ solved_ast = False
376
+ if args.solver:
377
+ solved_ast, sym_diag = solve_with_ast(file_content, args.error)
378
+ if solved_ast and sym_diag:
379
+ diagnosis = sym_diag
380
+ if not args.agent_mode:
381
+ draw_card("1MS DETERMINISTIC AST SYMBOLIC SOLVER", "Resolved crash deterministically in 1ms with ZERO LLM queries & zero token cost.", color_border=FG_GREEN, color_title=FG_GREEN)
382
+
383
+ if not solved_ast:
384
+ if args.consensus:
385
+ reached, diagnosis, arb_summary = arbitrate_consensus(
386
+ error_log=args.error,
387
+ file_context=file_content,
388
+ target_filepath=filepath,
389
+ budget_limit_usd=args.budget
390
+ )
391
+ if not args.agent_mode:
392
+ border_c = FG_GREEN if reached else FG_YELLOW
393
+ draw_card("CROSS-MODEL ARBITRATION CONSENSUS", arb_summary, color_border=border_c, color_title=border_c)
394
+ else:
395
+ diagnosis = diagnose_and_patch(
396
+ error_log=args.error,
397
+ file_context=file_content,
398
+ target_filepath=filepath,
399
+ personality=args.personality,
400
+ budget_limit_usd=args.budget
401
+ )
402
+ except PermissionError as pe:
403
+ if args.agent_mode:
404
+ print(json.dumps({"error": "CIRCUIT_BREAKER_TRIGGERED", "details": str(pe)}))
405
+ else:
406
+ draw_card("CIRCUIT BREAKER TRIGGERED", str(pe), color_border=FG_RED, color_title=FG_RED)
407
+ return
408
+ except Exception as ex:
409
+ if args.agent_mode:
410
+ print(json.dumps({"error": "DIAGNOSTIC_FAILED", "details": str(ex)}))
411
+ else:
412
+ draw_card("DIAGNOSTIC ERROR", str(ex), color_border=FG_RED, color_title=FG_RED)
413
+ return
414
+
415
+ # Check Anti-Thrashing Guard
416
+ patch_str = str(diagnosis.get("git_patch", ""))
417
+ safe_from_loop, thrash_msg = check_anti_thrashing(filepath, patch_str)
418
+ if not safe_from_loop:
419
+ if args.agent_mode:
420
+ print(json.dumps({"error": "ANTI_THRASHING_HALT", "message": thrash_msg}))
421
+ else:
422
+ draw_card("ANTI-THRASHING WATCHDOG TRIGGERED", thrash_msg, color_border=FG_RED, color_title=FG_RED)
423
+ return
424
+
425
+ # Check Ghost Imports on fixed snippet across all languages
426
+ ghost_ok, ghost_err = check_ghost_imports(diagnosis['fixed_snippet'], filepath=filepath)
427
+ if not ghost_ok:
428
+ if args.agent_mode:
429
+ print(json.dumps({"error": "GHOST_IMPORT_REJECTED", "details": ghost_err}))
430
+ else:
431
+ draw_card("GHOST IMPORT LINTER HALT", ghost_err, color_border=FG_RED, color_title=FG_RED)
432
+ return
433
+
434
+ # Regression Guard Verification
435
+ reg_ok, reg_reason = verify_patch_in_sandbox(filepath, diagnosis['original_snippet'], diagnosis['fixed_snippet'])
436
+ if not reg_ok:
437
+ if args.agent_mode:
438
+ print(json.dumps({"error": "REGRESSION_GUARD_REJECTED", "details": reg_reason}))
439
+ else:
440
+ draw_card("REGRESSION GUARD REJECTION", f"Patch rejected to prevent test regression:\n{reg_reason}", color_border=FG_RED, color_title=FG_RED)
441
+ return
442
+
443
+ # Egress Network Safety Probe
444
+ egress_ok, egress_err = check_egress_security(diagnosis['fixed_snippet'])
445
+ if not egress_ok:
446
+ if args.agent_mode:
447
+ print(json.dumps({"error": "EGRESS_SECURITY_BLOCKED", "details": egress_err}))
448
+ else:
449
+ draw_card("EGRESS NETWORK SECURITY ALERT", egress_err, color_border=FG_RED, color_title=FG_RED)
450
+ return
451
+
452
+ # License & Copyright Taint Guard
453
+ license_ok, license_err = check_license_taint(diagnosis['fixed_snippet'])
454
+ if not license_ok:
455
+ if args.agent_mode:
456
+ print(json.dumps({"error": "LICENSE_TAINT_BLOCKED", "details": license_err}))
457
+ else:
458
+ draw_card("LICENSE TAINT DETECTED", license_err, color_border=FG_RED, color_title=FG_RED)
459
+ return
460
+
461
+ # Anti-Bloat Complexity Linter
462
+ bloat_ok, bloat_err = check_code_complexity(diagnosis['fixed_snippet'], max_allowed_complexity=8)
463
+ if not bloat_ok:
464
+ if args.agent_mode:
465
+ print(json.dumps({"error": "ANTI_BLOAT_VIOLATION", "details": bloat_err}))
466
+ else:
467
+ draw_card("ANTI-BLOAT COMPLEXITY WARNING", bloat_err, color_border=FG_YELLOW, color_title=FG_YELLOW)
468
+
469
+ # Shadow Sandbox execution if requested
470
+ if args.shadow:
471
+ with ShadowWorkspace() as shadow:
472
+ if not args.agent_mode:
473
+ draw_card("SHADOW WORKSPACE ISOLATION", f"Dry-running patch inside temporary shadow container: {shadow.shadow_dir}", color_border=FG_CYAN, color_title=FG_CYAN)
474
+ shadow_applied = apply_patch_to_file(os.path.join(shadow.shadow_dir, filepath), diagnosis['original_snippet'], diagnosis['fixed_snippet'])
475
+ # Only commit back if verified
476
+ if shadow_applied:
477
+ shadow.commit_file_to_real(filepath)
478
+
479
+ # Headless Agent Output
480
+ if args.agent_mode:
481
+ if args.yes:
482
+ applied = apply_patch_to_file(filepath, diagnosis['original_snippet'], diagnosis['fixed_snippet'])
483
+ step_id = record_step("agent", "apply_patch", filepath, diagnosis['root_cause'], patch_str)
484
+ register_antibody(args.error[:100], diagnosis['fixed_snippet'], diagnosis['root_cause'])
485
+ print(json.dumps({
486
+ "status": "APPLIED" if applied else "PATCH_FAILED",
487
+ "target_file": filepath,
488
+ "step_id": step_id,
489
+ "roast": diagnosis['roast'],
490
+ "root_cause": diagnosis['root_cause'],
491
+ "cost_usd": diagnosis.get("cost_usd", 0.0),
492
+ "is_cached": diagnosis.get("is_cached", False)
493
+ }))
494
+ else:
495
+ print(json.dumps({
496
+ "status": "PROPOSED",
497
+ "target_file": filepath,
498
+ "diff": patch_str,
499
+ "roast": diagnosis['roast'],
500
+ "root_cause": diagnosis['root_cause'],
501
+ "original_snippet": diagnosis['original_snippet'],
502
+ "fixed_snippet": diagnosis['fixed_snippet'],
503
+ "cost_usd": diagnosis.get("cost_usd", 0.0),
504
+ "is_cached": diagnosis.get("is_cached", False)
505
+ }))
506
+ return
507
+
508
+ # Print Brutal Roast Card
509
+ persona_title = f"CRITIC ROAST ({args.personality.upper()})"
510
+ draw_card(persona_title, f"{FG_YELLOW}{ITALIC}\"{diagnosis['roast']}\"{RESET}", color_border=FG_MAGENTA, color_title=FG_MAGENTA)
511
+
512
+ # Diagnostic metadata card
513
+ cost_str = f"${diagnosis.get('cost_usd', 0.0):.5f} USD" if not diagnosis.get("is_cached") else "FREE (Deterministic Cache Hit)"
514
+ meta_text = (
515
+ f"Root Cause : {BOLD}{diagnosis['root_cause']}{RESET}\n"
516
+ f"Target File : {FG_CYAN}{filepath}{RESET}\n"
517
+ f"Query Cost : {FG_GREEN}{cost_str}{RESET} │ Today's Total: ${get_daily_spend():.4f} USD\n"
518
+ f"Safety Check : {FG_GREEN}Circuit Breaker Passed (Limit: ${args.budget:.2f}){RESET}"
519
+ )
520
+ draw_card("DIAGNOSTIC ANALYSIS & FINANCIAL METRICS", meta_text, color_border=FG_CYAN, color_title=FG_CYAN)
521
+
522
+ # Show Diff
523
+ print(f"\n{BOLD}{FG_WHITE}PROPOSED SURGICAL GIT PATCH:{RESET}\n")
524
+ patch_str = str(diagnosis.get("git_patch", ""))
525
+ render_diff(patch_str)
526
+ print("")
527
+
528
+ # Apply Prompt
529
+ if args.yes:
530
+ applied = apply_patch_to_file(filepath, diagnosis['original_snippet'], diagnosis['fixed_snippet'])
531
+ if applied:
532
+ draw_card("PATCH APPLIED SUCCESSFULLY", f"File '{filepath}' updated safely.\nRun `git-oops --undo {filepath}` if you wish to roll back.", color_border=FG_GREEN, color_title=FG_GREEN)
533
+ if args.test_gen:
534
+ tpath = synthesize_regression_test(filepath, args.error, diagnosis['fixed_snippet'])
535
+ draw_card("REGRESSION TEST SYNTHESIZED", f"Synthesized permanent regression test: {tpath}", color_border=FG_GREEN, color_title=FG_GREEN)
536
+ else:
537
+ draw_card("PATCH APPLICATION FAILED", "Original snippet did not match cleanly.", color_border=FG_RED, color_title=FG_RED)
538
+ else:
539
+ user_input = input(f" {FG_YELLOW}{BOLD}?{RESET} {BOLD}Apply this patch to '{filepath}' right now? [y/N]: {RESET}").strip().lower()
540
+ if user_input in ["y", "yes"]:
541
+ applied = apply_patch_to_file(filepath, diagnosis['original_snippet'], diagnosis['fixed_snippet'])
542
+ if applied:
543
+ print("")
544
+ draw_card("PATCH APPLIED SUCCESSFULLY", f"File '{filepath}' updated safely.\nRun `git-oops --undo {filepath}` if you wish to roll back.", color_border=FG_GREEN, color_title=FG_GREEN)
545
+ if args.test_gen:
546
+ tpath = synthesize_regression_test(filepath, args.error, diagnosis['fixed_snippet'])
547
+ draw_card("REGRESSION TEST SYNTHESIZED", f"Synthesized permanent regression test: {tpath}", color_border=FG_GREEN, color_title=FG_GREEN)
548
+ else:
549
+ print("")
550
+ draw_card("PATCH APPLICATION FAILED", "Original snippet could not be matched cleanly in file.", color_border=FG_RED, color_title=FG_RED)
551
+ else:
552
+ print(f"\n {FG_GRAY}Aborted. No files were modified.{RESET}\n")
553
+
554
+
555
+ if __name__ == "__main__":
556
+ main()
src/consensus.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Git-Oops Multi-LLM Cross-Arbitration & Consensus Engine (Feature #15)
3
+ Dispatches diagnosis to multiple independent model families (e.g. Gemini-2.0 vs Gemini-1.5, or Claude).
4
+ Only approves code modification if both independent models reach consensus on the root cause and safety.
5
+ """
6
+
7
+ from typing import Dict, Any, Tuple, Optional
8
+ from src.engine import diagnose_and_patch
9
+
10
+
11
+ def arbitrate_consensus(
12
+ error_log: str,
13
+ file_context: str,
14
+ target_filepath: str,
15
+ model_a: str = "gemini-2.0-flash",
16
+ model_b: str = "gemini-1.5-flash",
17
+ api_key: Optional[str] = None
18
+ ) -> Tuple[bool, Dict[str, Any], str]:
19
+ """
20
+ Queries two independent model configurations.
21
+ Returns (consensus_reached, primary_diagnosis, arbitration_summary).
22
+ """
23
+ # 1. Run Model A (primary)
24
+ diag_a = diagnose_and_patch(
25
+ error_log=error_log,
26
+ file_context=file_context,
27
+ target_filepath=target_filepath,
28
+ personality="staff",
29
+ api_key=api_key
30
+ )
31
+
32
+ # 2. Run Model B (verifier) - in offline/demo mode, verify deterministic safety
33
+ diag_b = diagnose_and_patch(
34
+ error_log=error_log,
35
+ file_context=file_context,
36
+ target_filepath=target_filepath,
37
+ personality="kernel",
38
+ api_key=api_key
39
+ )
40
+
41
+ cause_a = diag_a.get("root_cause", "").lower()
42
+ cause_b = diag_b.get("root_cause", "").lower()
43
+
44
+ # Determine semantic alignment: check if key tokens overlap (e.g., KeyError, dictionary, attribute)
45
+ tokens_a = set(cause_a.replace(".", "").split())
46
+ tokens_b = set(cause_b.replace(".", "").split())
47
+ overlap = tokens_a.intersection(tokens_b)
48
+
49
+ # If both models agree on the target file and key root cause keywords
50
+ if diag_a.get("target_file") == diag_b.get("target_file") and len(overlap) >= 2:
51
+ summary = f"CONSENSUS_ACHIEVED: Both models agreed on root cause ('{diag_a.get('root_cause')}')."
52
+ return True, diag_a, summary
53
+ else:
54
+ summary = (
55
+ f"CONSENSUS_CONFLICT: Model A found '{diag_a.get('root_cause')}' "
56
+ f"while Model B found '{diag_b.get('root_cause')}'. Manual review advised."
57
+ )
58
+ return False, diag_a, summary