9365def3dd
Primitivas (python/functions/infra): - drain_fleet_events: consume la cola del watcher (~/.claude/fleet/ events.jsonl) desde un cursor, agrupa por clasificacion, marca urgentes. 7 tests. - set_dod_contract: escribe el DoD-contrato fijo (dod_contract/dod_status) en el goal.json de un agente sin pisar el resto (escritura atomica). 5 tests. Skill /orquestador evolucionado (sin romper lo existente): vigila la flota por su DoD (no por 'esta vivo'). Nueva seccion 'Consumo de la cola de la flota': DoD-contrato obligatorio al lanzar, drenar la cola, politicas por clasificacion (RECLAMA escala / DICE_TERMINADO verifica / ESTANCADO nudge / MAL_LANZADO re-DoD), verificador independiente del ejecutor (lee el report vs dod_contract), splitter con tope de fan-out, y cadencia (drain al actuar + heartbeat). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""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 `<goals_dir>/<session_id>.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
|
|
`<session_id>.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,
|
|
}
|