package infra // Termination labels returned by ClassifyFleetTermination. They describe the // mechanical termination state of a Claude fleet agent so a cheap (LLM-free) // watcher can decide what to do with it. const ( // TerminationReclama means the agent is asking for human input and must be // attended first, above any other consideration. TerminationReclama = "RECLAMA" // TerminationMalLanzado means the agent was launched without a DoD contract // — no agent should run without an acceptance criterion. TerminationMalLanzado = "MAL_LANZADO" // TerminationDiceTerminado means the agent claims it is finished (idle and // either phase "hecho" or its DoD status is "met"). TerminationDiceTerminado = "DICE_TERMINADO" // TerminationEstancado means the agent is idle, not finished, and has been // inactive at or beyond the stall threshold. TerminationEstancado = "ESTANCADO" // TerminationTrabajando means the agent is still working (busy, or idle // recently below the stall threshold). TerminationTrabajando = "TRABAJANDO" ) // ClassifyFleetTermination mechanically classifies the termination state of a // fleet agent. It is pure and deterministic: no I/O, no clock, no global state. // // Inputs: // - status: process state from sessions.json — "idle" | "busy" | "waiting". // - phase: work phase from goal.json — "investigando", "planificando", // "haciendo", "testeando", "puliendo", "pendiente_revision", "preguntando", // "bloqueado", "en_pausa", "hecho", "iterando" or "". // - dodContract: fixed acceptance criterion ("" means none was defined). // - dodStatus: "pending" | "met" | "failed" | "". // - idleSeconds: seconds since the session's last activity. // - stallThresholdSeconds: threshold to consider the agent stalled. // // Precedence (evaluated top to bottom; the first match wins): // 1. RECLAMA — the agent is asking for human input (status "waiting" OR phase // "preguntando"/"bloqueado"). This dominates everything, even a missing DoD // contract: if it asks for input, that is the first thing to handle. // 2. MAL_LANZADO — it does not reclaim input but has no DoD contract; no agent // should run without one. // 3. DICE_TERMINADO — idle AND (phase "hecho" OR dodStatus "met"). // 4. ESTANCADO — idle AND phase not "hecho" AND idleSeconds >= stall threshold. // 5. TRABAJANDO — everything else (busy, or idle recently below the threshold). func ClassifyFleetTermination(status, phase, dodContract, dodStatus string, idleSeconds, stallThresholdSeconds int) string { // 1. RECLAMA dominates: a request for human input is handled first. if status == "waiting" || phase == "preguntando" || phase == "bloqueado" { return TerminationReclama } // 2. MAL_LANZADO: running without a DoD contract is invalid. if dodContract == "" { return TerminationMalLanzado } // 3. DICE_TERMINADO: idle and self-reporting completion. if status == "idle" && (phase == "hecho" || dodStatus == "met") { return TerminationDiceTerminado } // 4. ESTANCADO: idle, not finished, inactive at/beyond the stall threshold. if status == "idle" && phase != "hecho" && idleSeconds >= stallThresholdSeconds { return TerminationEstancado } // 5. TRABAJANDO: busy, or idle recently below the threshold. return TerminationTrabajando }