Skip to main content

#!/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