diff --git a/functions/infra/classify_fleet_termination.go b/functions/infra/classify_fleet_termination.go new file mode 100644 index 00000000..fec86ef6 --- /dev/null +++ b/functions/infra/classify_fleet_termination.go @@ -0,0 +1,69 @@ +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 +} diff --git a/functions/infra/classify_fleet_termination.md b/functions/infra/classify_fleet_termination.md new file mode 100644 index 00000000..e0ca8ec9 --- /dev/null +++ b/functions/infra/classify_fleet_termination.md @@ -0,0 +1,109 @@ +--- +name: classify_fleet_termination +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: pure +signature: "func ClassifyFleetTermination(status, phase, dodContract, dodStatus string, idleSeconds, stallThresholdSeconds int) string" +description: "Clasifica MECANICAMENTE el estado de terminacion de un agente Claude de la flota para que un watcher barato sin LLM decida que hacer. Pura y determinista. Devuelve una de RECLAMA, MAL_LANZADO, DICE_TERMINADO, ESTANCADO o TRABAJANDO segun precedencia fija: RECLAMA (pide input humano) manda sobre todo, luego MAL_LANZADO (sin DoD-contrato), luego DICE_TERMINADO, ESTANCADO y TRABAJANDO." +tags: [fleet, claude-fleet, classification, watcher, termination, orchestrator, pure, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: status + desc: "estado del proceso Claude leido de sessions.json: idle | busy | waiting" + - name: phase + desc: "fase de trabajo del goal.json: investigando | planificando | haciendo | testeando | puliendo | pendiente_revision | preguntando | bloqueado | en_pausa | hecho | iterando | (vacio)" + - name: dodContract + desc: "criterio de aceptacion fijo del agente; cadena vacia significa que no se definio DoD-contrato (agente mal lanzado)" + - name: dodStatus + desc: "estado de cumplimiento del DoD: pending | met | failed | (vacio)" + - name: idleSeconds + desc: "segundos transcurridos desde la ultima actividad de la sesion" + - name: stallThresholdSeconds + desc: "umbral en segundos a partir del cual un agente idle no terminado se considera ESTANCADO (comparacion >=, inclusiva)" +output: "una etiqueta string: RECLAMA | MAL_LANZADO | DICE_TERMINADO | ESTANCADO | TRABAJANDO (constantes exportadas TerminationReclama, TerminationMalLanzado, TerminationDiceTerminado, TerminationEstancado, TerminationTrabajando)" +tested: true +tests: + - "waiting reclama input" + - "phase preguntando reclama" + - "phase bloqueado reclama" + - "waiting manda aunque sin dodContract" + - "preguntando manda aunque sin dodContract" + - "bloqueado manda aunque idle estancado" + - "sin dodContract busy" + - "sin dodContract idle reciente" + - "sin dodContract idle estancado" + - "sin dodContract phase hecho" + - "sin dodContract dodStatus met" + - "idle phase hecho" + - "idle dodStatus met" + - "idle hecho y met" + - "idle met aunque estancado por tiempo" + - "idle hecho aunque estancado por tiempo" + - "idle no hecho en umbral exacto" + - "idle no hecho por encima del umbral" + - "idle iterando estancado" + - "idle dodStatus failed estancado" + - "idle en_pausa estancado" + - "busy trabajando" + - "busy aunque idleSeconds alto" + - "idle reciente bajo umbral" + - "idle reciente cero segundos" + - "idle no hecho justo bajo umbral" + - "busy phase vacia con dodContract" + - "umbral cero idle no hecho => estancado" + - "idle hecho con umbral cero => dice terminado" + - "dodStatus met con status busy NO termina" + - "phase hecho con status busy NO termina" + - "idle pendiente_revision bajo umbral trabajando" + - "idle pendiente_revision sobre umbral estancado" + - "waiting con dodStatus met sigue reclamando" +test_file_path: "functions/infra/classify_fleet_termination_test.go" +file_path: "functions/infra/classify_fleet_termination.go" +--- + +## Ejemplo + +```go +// Agente idle que dice estar terminado. +label := ClassifyFleetTermination("idle", "hecho", "tests verdes + indexado", "met", 30, 600) +// label == "DICE_TERMINADO" (== TerminationDiceTerminado) + +// Agente idle, no terminado, parado 12 minutos con umbral de 10 minutos. +label = ClassifyFleetTermination("idle", "haciendo", "tests verdes", "pending", 720, 600) +// label == "ESTANCADO" + +// Agente que reclama input humano: manda sobre todo, aunque le falte DoD. +label = ClassifyFleetTermination("waiting", "haciendo", "", "", 5, 600) +// label == "RECLAMA" + +switch label { +case TerminationReclama: + // notificar al humano +case TerminationMalLanzado: + // matar y relanzar con DoD-contrato +case TerminationDiceTerminado: + // pasar a verificacion de DoD +case TerminationEstancado: + // empujar / reiniciar / escalar +case TerminationTrabajando: + // dejar trabajar +} +``` + +## Cuando usarla + +Usala en el watcher del meta-orquestador de flota (dev/flows/0012-fleet-orchestrator-dod.md) cuando barras cada agente Claude y necesites decidir mecanicamente, sin gastar LLM, en que cubo cae (reclama input / mal lanzado / dice terminado / estancado / trabajando) a partir de sessions.json + goal.json. Es el paso de triaje barato antes de cualquier accion costosa. + +## Gotchas + +- **Precedencia estricta de arriba a abajo.** RECLAMA gana siempre, incluso si el agente no tiene `dodContract` o ya esta idle y estancado. Si tu logica espera que MAL_LANZADO domine, esta funcion no hace eso a proposito: un agente que pide input se atiende primero. +- **El umbral es inclusivo (`>=`).** `idleSeconds == stallThresholdSeconds` ya cuenta como ESTANCADO. Con `stallThresholdSeconds == 0` cualquier agente idle no terminado es ESTANCADO al instante. +- **`status == "busy"` nunca termina ni se estanca.** Aunque `dodStatus == "met"` o `phase == "hecho"`, si el proceso esta busy se clasifica como TRABAJANDO (la condicion de terminacion/estancamiento exige idle). +- **Strings exactos, case-sensitive.** Valores fuera de los esperados (ej. "Idle", "WAITING", una phase desconocida) caen a TRABAJANDO salvo que disparen otra rama. Normaliza las entradas antes de llamar si tus fuentes no garantizan el casing. diff --git a/functions/infra/classify_fleet_termination_test.go b/functions/infra/classify_fleet_termination_test.go new file mode 100644 index 00000000..99dc0404 --- /dev/null +++ b/functions/infra/classify_fleet_termination_test.go @@ -0,0 +1,74 @@ +package infra + +import "testing" + +func TestClassifyFleetTermination(t *testing.T) { + const stall = 600 + + cases := []struct { + name string + status string + phase string + dodC string + dodS string + idle int + thresh int + want string + }{ + // --- RECLAMA (precedencia 1) --- + {"waiting reclama input", "waiting", "haciendo", "criterio", "pending", 10, stall, TerminationReclama}, + {"phase preguntando reclama", "busy", "preguntando", "criterio", "pending", 10, stall, TerminationReclama}, + {"phase bloqueado reclama", "idle", "bloqueado", "criterio", "pending", 9999, stall, TerminationReclama}, + {"waiting manda aunque sin dodContract", "waiting", "haciendo", "", "", 10, stall, TerminationReclama}, + {"preguntando manda aunque sin dodContract", "idle", "preguntando", "", "", 10, stall, TerminationReclama}, + {"bloqueado manda aunque idle estancado", "idle", "bloqueado", "criterio", "pending", 10000, stall, TerminationReclama}, + + // --- MAL_LANZADO (precedencia 2) --- + {"sin dodContract busy", "busy", "haciendo", "", "pending", 10, stall, TerminationMalLanzado}, + {"sin dodContract idle reciente", "idle", "haciendo", "", "", 5, stall, TerminationMalLanzado}, + {"sin dodContract idle estancado", "idle", "haciendo", "", "pending", 10000, stall, TerminationMalLanzado}, + {"sin dodContract phase hecho", "idle", "hecho", "", "met", 10, stall, TerminationMalLanzado}, + {"sin dodContract dodStatus met", "idle", "testeando", "", "met", 10, stall, TerminationMalLanzado}, + + // --- DICE_TERMINADO (precedencia 3) --- + {"idle phase hecho", "idle", "hecho", "criterio", "pending", 10, stall, TerminationDiceTerminado}, + {"idle dodStatus met", "idle", "testeando", "criterio", "met", 10, stall, TerminationDiceTerminado}, + {"idle hecho y met", "idle", "hecho", "criterio", "met", 10, stall, TerminationDiceTerminado}, + {"idle met aunque estancado por tiempo", "idle", "puliendo", "criterio", "met", 10000, stall, TerminationDiceTerminado}, + {"idle hecho aunque estancado por tiempo", "idle", "hecho", "criterio", "pending", 10000, stall, TerminationDiceTerminado}, + + // --- ESTANCADO (precedencia 4) --- + {"idle no hecho en umbral exacto", "idle", "haciendo", "criterio", "pending", stall, stall, TerminationEstancado}, + {"idle no hecho por encima del umbral", "idle", "investigando", "criterio", "pending", stall + 1, stall, TerminationEstancado}, + {"idle iterando estancado", "idle", "iterando", "criterio", "failed", 5000, stall, TerminationEstancado}, + {"idle dodStatus failed estancado", "idle", "testeando", "criterio", "failed", 700, stall, TerminationEstancado}, + {"idle en_pausa estancado", "idle", "en_pausa", "criterio", "pending", 601, stall, TerminationEstancado}, + + // --- TRABAJANDO (precedencia 5, todo lo demas) --- + {"busy trabajando", "busy", "haciendo", "criterio", "pending", 0, stall, TerminationTrabajando}, + {"busy aunque idleSeconds alto", "busy", "investigando", "criterio", "pending", 99999, stall, TerminationTrabajando}, + {"idle reciente bajo umbral", "idle", "haciendo", "criterio", "pending", stall - 1, stall, TerminationTrabajando}, + {"idle reciente cero segundos", "idle", "planificando", "criterio", "pending", 0, stall, TerminationTrabajando}, + {"idle no hecho justo bajo umbral", "idle", "testeando", "criterio", "failed", 599, stall, TerminationTrabajando}, + {"busy phase vacia con dodContract", "busy", "", "criterio", "", 10, stall, TerminationTrabajando}, + + // --- Bordes y combinaciones --- + {"umbral cero idle no hecho => estancado", "idle", "haciendo", "criterio", "pending", 0, 0, TerminationEstancado}, + {"idle hecho con umbral cero => dice terminado", "idle", "hecho", "criterio", "pending", 0, 0, TerminationDiceTerminado}, + {"dodStatus met con status busy NO termina", "busy", "haciendo", "criterio", "met", 10, stall, TerminationTrabajando}, + {"phase hecho con status busy NO termina", "busy", "hecho", "criterio", "pending", 10, stall, TerminationTrabajando}, + {"idle pendiente_revision bajo umbral trabajando", "idle", "pendiente_revision", "criterio", "pending", 100, stall, TerminationTrabajando}, + {"idle pendiente_revision sobre umbral estancado", "idle", "pendiente_revision", "criterio", "pending", 1000, stall, TerminationEstancado}, + {"waiting con dodStatus met sigue reclamando", "waiting", "hecho", "criterio", "met", 10, stall, TerminationReclama}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := ClassifyFleetTermination(c.status, c.phase, c.dodC, c.dodS, c.idle, c.thresh) + if got != c.want { + t.Errorf("ClassifyFleetTermination(%q,%q,%q,%q,%d,%d) = %q, want %q", + c.status, c.phase, c.dodC, c.dodS, c.idle, c.thresh, got, c.want) + } + }) + } +} diff --git a/functions/infra/claude_fleet.go b/functions/infra/claude_fleet.go index 0913b95b..42f13d5b 100644 --- a/functions/infra/claude_fleet.go +++ b/functions/infra/claude_fleet.go @@ -10,19 +10,22 @@ package infra // from a single ~/.claude/sessions/.json entry plus its optional // ~/.claude/goals/.json sidecar and the process' own /proc entry. type ClaudeFleet struct { - PID int `json:"pid"` - KittyPID int `json:"kitty_pid"` // KITTY_PID from the process environ; 0 if not applicable (e.g. remote tmux) - SessionID string `json:"session_id"` // Claude Code sessionId (UUID) - Rename string `json:"rename"` // display name: short goal if present, else basename(cwd) - Target string `json:"target"` // sessionId[:8] + "@" + basename(cwd) - Goal string `json:"goal"` // from goals/.json .goal ("" if absent) - Phase string `json:"phase"` // from goals/.json .phase ("" if absent) - Emojis string `json:"emojis"` // 3 emojis representing the task (from goals .emojis; "" if absent) - Name string `json:"name"` // manual rename of the terminal (from goals .rename; "" if none) - Status string `json:"status"` // idle|busy|waiting (from sessions/.json) - Cwd string `json:"cwd"` // working directory of the session - TmuxWindow string `json:"tmux_window"` // "" for now (populated in a later phase) - Alive bool `json:"alive"` // process alive AND procStart matches (guards against PID recycling) - UpdatedAt int64 `json:"updated_at"` // from sessions/.json .updatedAt (epoch millis) - CtxPct int `json:"ctx_pct"` // context window used %, from runtime/.json; -1 if unknown + PID int `json:"pid"` + KittyPID int `json:"kitty_pid"` // KITTY_PID from the process environ; 0 if not applicable (e.g. remote tmux) + SessionID string `json:"session_id"` // Claude Code sessionId (UUID) + Rename string `json:"rename"` // display name: short goal if present, else basename(cwd) + Target string `json:"target"` // sessionId[:8] + "@" + basename(cwd) + Goal string `json:"goal"` // from goals/.json .goal ("" if absent) + Phase string `json:"phase"` // from goals/.json .phase ("" if absent) + DodContract string `json:"dod_contract"` // from goals .dod_contract: fixed acceptance criterion ("" if absent) + DodStatus string `json:"dod_status"` // from goals .dod_status: pending|met|failed ("" if absent) + Role string `json:"role"` // from goals .role: orchestrator|executor ("" if absent; defaults to executor in consumers) + Emojis string `json:"emojis"` // 3 emojis representing the task (from goals .emojis; "" if absent) + Name string `json:"name"` // manual rename of the terminal (from goals .rename; "" if none) + Status string `json:"status"` // idle|busy|waiting (from sessions/.json) + Cwd string `json:"cwd"` // working directory of the session + TmuxWindow string `json:"tmux_window"` // "" for now (populated in a later phase) + Alive bool `json:"alive"` // process alive AND procStart matches (guards against PID recycling) + UpdatedAt int64 `json:"updated_at"` // from sessions/.json .updatedAt (epoch millis) + CtxPct int `json:"ctx_pct"` // context window used %, from runtime/.json; -1 if unknown } diff --git a/functions/infra/list_claude_fleet.go b/functions/infra/list_claude_fleet.go index 1daacda7..6ba313e5 100644 --- a/functions/infra/list_claude_fleet.go +++ b/functions/infra/list_claude_fleet.go @@ -25,10 +25,13 @@ type sessionFile struct { // goalFile mirrors the on-disk shape of ~/.claude/goals/.json. type goalFile struct { - Goal string `json:"goal"` - Phase string `json:"phase"` - Emojis string `json:"emojis"` - Rename string `json:"rename"` + Goal string `json:"goal"` + Phase string `json:"phase"` + Emojis string `json:"emojis"` + Rename string `json:"rename"` + DodContract string `json:"dod_contract"` + DodStatus string `json:"dod_status"` + Role string `json:"role"` } // runtimeFile mirrors ~/.claude/runtime/.json written by statusline.sh @@ -104,8 +107,9 @@ func ListClaudeFleetFrom(claudeDir string) ([]ClaudeFleet, error) { // KITTY_PID from the process environ (0 if unreadable / absent). f.KittyPID = readKittyPID(sess.PID) - // Join goal/phase/emojis/name from goals/.json (optional). - f.Goal, f.Phase, f.Emojis, f.Name = readGoal(goalsDir, sess.SessionID) + // Join goal/phase/emojis/name + DoD contract/status from + // goals/.json (optional). + f.Goal, f.Phase, f.Emojis, f.Name, f.DodContract, f.DodStatus, f.Role = readGoal(goalsDir, sess.SessionID) // Context usage from runtime/.json (written by statusline). f.CtxPct = readCtxPct(runtimeDir, sess.SessionID) @@ -179,18 +183,19 @@ func readKittyPID(pid int) int { return 0 } -// readGoal reads goals/.json and returns its goal, phase, emojis and -// manual rename. If the file is absent or unparseable, all are "". -func readGoal(goalsDir, sessionID string) (goal, phase, emojis, rename string) { +// readGoal reads goals/.json and returns its goal, phase, emojis, +// manual rename, DoD contract, DoD status and role. If the file is absent or +// unparseable, all are "". +func readGoal(goalsDir, sessionID string) (goal, phase, emojis, rename, dodContract, dodStatus, role string) { raw, err := os.ReadFile(filepath.Join(goalsDir, sessionID+".json")) if err != nil { - return "", "", "", "" + return "", "", "", "", "", "", "" } var g goalFile if json.Unmarshal(raw, &g) != nil { - return "", "", "", "" + return "", "", "", "", "", "", "" } - return g.Goal, g.Phase, g.Emojis, g.Rename + return g.Goal, g.Phase, g.Emojis, g.Rename, g.DodContract, g.DodStatus, g.Role } // readCtxPct reads runtime/.json and returns the context-window used diff --git a/functions/infra/list_claude_fleet_test.go b/functions/infra/list_claude_fleet_test.go index fbb21beb..a27e759e 100644 --- a/functions/infra/list_claude_fleet_test.go +++ b/functions/infra/list_claude_fleet_test.go @@ -57,7 +57,7 @@ func TestListClaudeFleetFrom(t *testing.T) { fmt.Sprintf(`{"pid":%d,"sessionId":"aaaaaaaa-1111-2222-3333-444444444444","cwd":"/home/enmanuel/fn_registry","procStart":%q,"status":"idle","updatedAt":1000}`, livePID, liveProcStart)) writeFile(t, filepath.Join(goals, "aaaaaaaa-1111-2222-3333-444444444444.json"), - `{"goal":"Recomendar stack tecnologico para la nueva app de inventario y validar dependencias","phase":"investigando","history":["haciendo","investigando"]}`) + `{"goal":"Recomendar stack tecnologico para la nueva app de inventario y validar dependencias","phase":"investigando","history":["haciendo","investigando"],"dod_contract":"build verde + tests pasan + tipo expuesto","dod_status":"pending"}`) // Session B: alive (own PID again — same process, valid procStart), no // goal sidecar -> rename = basename(cwd) = projectx, status busy. @@ -100,6 +100,12 @@ func TestListClaudeFleetFrom(t *testing.T) { if a.Phase != "investigando" { t.Errorf("session A: phase join failed, got %q", a.Phase) } + if a.DodContract != "build verde + tests pasan + tipo expuesto" { + t.Errorf("session A: dod_contract join failed, got %q", a.DodContract) + } + if a.DodStatus != "pending" { + t.Errorf("session A: dod_status join failed, got %q", a.DodStatus) + } // Rename = goal truncated to 48 runes. wantRename := string([]rune(a.Goal)[:48]) if a.Rename != wantRename { @@ -117,6 +123,9 @@ func TestListClaudeFleetFrom(t *testing.T) { if b.Goal != "" || b.Phase != "" { t.Errorf("session B: expected empty goal/phase, got goal=%q phase=%q", b.Goal, b.Phase) } + if b.DodContract != "" || b.DodStatus != "" { + t.Errorf("session B: expected empty dod fields (no sidecar), got contract=%q status=%q", b.DodContract, b.DodStatus) + } if b.Rename != "projectx" { t.Errorf("session B: rename = %q, want basename(cwd) %q", b.Rename, "projectx") } diff --git a/types/infra/claude_fleet.md b/types/infra/claude_fleet.md index 8f27b142..5962bfe6 100644 --- a/types/infra/claude_fleet.md +++ b/types/infra/claude_fleet.md @@ -6,18 +6,21 @@ version: "1.0.0" algebraic: product definition: | type ClaudeFleet struct { - PID int `json:"pid"` - KittyPID int `json:"kitty_pid"` - SessionID string `json:"session_id"` - Rename string `json:"rename"` - Target string `json:"target"` - Goal string `json:"goal"` - Phase string `json:"phase"` - Status string `json:"status"` - Cwd string `json:"cwd"` - TmuxWindow string `json:"tmux_window"` - Alive bool `json:"alive"` - UpdatedAt int64 `json:"updated_at"` + PID int `json:"pid"` + KittyPID int `json:"kitty_pid"` + SessionID string `json:"session_id"` + Rename string `json:"rename"` + Target string `json:"target"` + Goal string `json:"goal"` + Phase string `json:"phase"` + DodContract string `json:"dod_contract"` + DodStatus string `json:"dod_status"` + Role string `json:"role"` + Status string `json:"status"` + Cwd string `json:"cwd"` + TmuxWindow string `json:"tmux_window"` + Alive bool `json:"alive"` + UpdatedAt int64 `json:"updated_at"` } description: "Registro de una sesion de Claude Code en la maquina local. Cruza el estado del proceso (/proc) con la metadata que Claude Code persiste en ~/.claude (sessions/.json + goals/.json). Pieza de datos de la app TUI fleetview, producida por list_claude_fleet_go_infra." tags: [claude-fleet, infra, claude, session, process] @@ -36,6 +39,9 @@ file_path: "functions/infra/claude_fleet.go" | `Target` | string | derivado | sessionId[:8] + "@" + basename(cwd). | | `Goal` | string | goals/.json .goal | "" si no hay sidecar. | | `Phase` | string | goals .phase | "" si no hay sidecar. | +| `DodContract` | string | goals .dod_contract | Criterio de aceptacion FIJO escrito al lanzar el agente; "" si ausente o vacio. | +| `DodStatus` | string | goals .dod_status | Estado del DoD: `pending`\|`met`\|`failed`; "" si ausente o vacio. | +| `Role` | string | goals .role | Rol en la flota: `orchestrator`\|`executor`; "" si ausente (los consumidores asumen executor). El orquestador se pinea arriba en la TUI. | | `Status` | string | sessions .status | idle\|busy\|waiting. | | `Cwd` | string | sessions .cwd | Working directory. | | `TmuxWindow` | string | (reservado) | "" por ahora; se rellena en fase posterior. |