#!/usr/bin/env python3 """ Loop Guard Utility
Provides a safety mechanism for automated loops to prevent infinite execution and ensure traceable exit conditions.
Standards Compliance:
- ADR-056: Action-Level Task Tracking (Task ID required)
- ADR-125: Centralized Logging (Logs to ~/.coditect-data/logs/)
Usage: from scripts.utils.loop_guard import LoopGuard
guard = LoopGuard(task_id="A.9.1.3", max_iterations=50)
while True:
if guard.should_break(context="Waiting for pod"):
break
if guard.check_condition(lambda: check_pod_status() == "Running"):
break
time.sleep(1)
"""
import logging import os import time from pathlib import Path from typing import Callable, Optional
--- Configuration ---
Per ADR-125, logs must live in user data, not the repo.
LOG_DIR = Path(os.path.expanduser("~/PROJECTS/.coditect-data/logs")) LOG_FILE = LOG_DIR / "loop_guard.log"
Ensure log directory exists
LOG_DIR.mkdir(parents=True, exist_ok=True)
Configure Logging
logging.basicConfig( filename=str(LOG_FILE), level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(name)s] - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )
logger = logging.getLogger("LoopGuard")
class LoopGuard: """ Monitors loop execution to enforce safety limits and logging. """
def __init__(
self,
task_id: str,
max_iterations: int = 100,
timeout_sec: float = 60.0,
name: str = "GenericLoop"
):
"""
Initialize the LoopGuard.
Args:
task_id (str): The specific Task ID (e.g., 'A.9.1') for audit trails (ADR-056).
max_iterations (int): Safety limit for iteration count.
timeout_sec (float): Safety limit for execution time.
name (str): Human-readable name for the loop context.
"""
if not task_id or not task_id[0].isupper():
# Basic validation for Task ID format (e.g. A.1...)
logger.warning(f"LoopGuard initialized with invalid Task ID: '{task_id}'. See ADR-056.")
self.task_id = task_id
self.max_iterations = max_iterations
self.timeout_sec = timeout_sec
self.name = name
self.start_time = time.time()
self.iteration_count = 0
self._log(f"STARTED: {name} (Limit: {max_iterations} iter, {timeout_sec}s)")
def _log(self, message: str, level: int = logging.INFO):
"""Internal helper to log with Task ID prefix."""
# Enforcing ADR-056: "{Task ID}: {Message}"
formatted_msg = f"{self.task_id}: {message}"
logger.log(level, formatted_msg)
def should_break(self, context: str = "") -> bool:
"""
Check if the loop should break due to safety limits.
Must be called once per iteration.
Returns:
bool: True if the loop should exit.
"""
self.iteration_count += 1
elapsed = time.time() - self.start_time
# Check Iteration Limit
if self.iteration_count > self.max_iterations:
self._log(f"BREAK: {self.name} exceeded max iterations ({self.max_iterations}). Context: {context}", level=logging.WARNING)
return True
# Check Timeout
if elapsed > self.timeout_sec:
self._log(f"BREAK: {self.name} exceeded timeout ({self.timeout_sec}s). Context: {context}", level=logging.WARNING)
return True
return False
def check_condition(self, predicate: Callable[[], bool], description: str) -> bool:
"""
Check a custom exit condition. If true, logs the success and signals break.
Args:
predicate (Callable): Function returning True if loop should exit.
description (str): Description of the success condition.
Returns:
bool: True if condition met OR safety limits exceeded.
"""
# First, enforce limits
if self.should_break(context=f"Checking: {description}"):
return True
# Check actual condition
try:
if predicate():
self._log(f"BREAK: Condition met - {description}")
return True
except Exception as e:
self._log(f"ERROR: Exception while checking condition '{description}': {str(e)}", level=logging.ERROR)
# Fail safe: break on error to prevent infinite error loops
return True
return False