"""Escribe un DoD-contrato fijo y su estado en el sidecar goal.json de una sesion Claude.""" import json import os _VALID_STATUS = ("pending", "met", "failed") def set_dod_contract( session_id: str, contract: str, status: str = "pending", goals_dir: str | None = None, ) -> dict: """Escribe el DoD-contrato y su estado en el goal.json de una sesion Claude. Lee el sidecar `/.json` si existe, preservando TODOS sus campos (goal, phase, dod, history, prompts, etc.), y actualiza solo las claves `dod_contract` y `dod_status`. La escritura es atomica (tmp + os.replace) para no corromper el archivo si el proceso muere a mitad. Args: session_id: ID de la sesion Claude. Da nombre al archivo `.json` dentro de goals_dir. contract: Criterio de aceptacion estable contra el que se evalua la terminacion del agente. No puede ser vacio. status: Estado del contrato. Uno de "pending", "met" o "failed". goals_dir: Directorio de sidecars de goal. Por defecto `~/.claude/goals`. Returns: dict con session_id, path, dod_contract, dod_status y written. Raises: ValueError: si contract es vacio o status no es valido. No escribe nada. """ if not contract or not contract.strip(): raise ValueError("contract no puede ser vacio: un DoD-contrato vacio no tiene sentido") if status not in _VALID_STATUS: raise ValueError( f"status invalido: {status!r}. Debe ser uno de {_VALID_STATUS}" ) if goals_dir is None: goals_dir = os.path.join(os.path.expanduser("~"), ".claude", "goals") os.makedirs(goals_dir, exist_ok=True) path = os.path.join(goals_dir, f"{session_id}.json") data: dict = {} if os.path.exists(path): try: with open(path, "r", encoding="utf-8") as f: loaded = json.load(f) if isinstance(loaded, dict): data = loaded except (json.JSONDecodeError, OSError): # Archivo corrupto o ilegible: no se pierde lo nuevo, se parte limpio. data = {} # Solo se tocan estas dos claves; el resto del dict se preserva intacto. data["dod_contract"] = contract data["dod_status"] = status tmp_path = f"{path}.tmp" with open(tmp_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) f.write("\n") f.flush() os.fsync(f.fileno()) os.replace(tmp_path, path) return { "session_id": session_id, "path": path, "dod_contract": contract, "dod_status": status, "written": True, }