feat(infra): modelo de datos del meta-orquestador de flota (flow 0012)
Fase 1, piezas 1+2: - ClaudeFleet + list_claude_fleet ganan DodContract/DodStatus/Role, leidos de goals/<sessionId>.json (.dod_contract/.dod_status/.role). Aditivo: fleetview sigue compilando. - classify_fleet_termination (pura): clasifica el estado de terminacion de un agente (RECLAMA/MAL_LANZADO/DICE_TERMINADO/ESTANCADO/TRABAJANDO) con precedencia fija, para que un watcher sin LLM decida. 34 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,19 +10,22 @@ package infra
|
|||||||
// from a single ~/.claude/sessions/<PID>.json entry plus its optional
|
// from a single ~/.claude/sessions/<PID>.json entry plus its optional
|
||||||
// ~/.claude/goals/<sessionId>.json sidecar and the process' own /proc entry.
|
// ~/.claude/goals/<sessionId>.json sidecar and the process' own /proc entry.
|
||||||
type ClaudeFleet struct {
|
type ClaudeFleet struct {
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
KittyPID int `json:"kitty_pid"` // KITTY_PID from the process environ; 0 if not applicable (e.g. remote tmux)
|
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)
|
SessionID string `json:"session_id"` // Claude Code sessionId (UUID)
|
||||||
Rename string `json:"rename"` // display name: short goal if present, else basename(cwd)
|
Rename string `json:"rename"` // display name: short goal if present, else basename(cwd)
|
||||||
Target string `json:"target"` // sessionId[:8] + "@" + basename(cwd)
|
Target string `json:"target"` // sessionId[:8] + "@" + basename(cwd)
|
||||||
Goal string `json:"goal"` // from goals/<sessionId>.json .goal ("" if absent)
|
Goal string `json:"goal"` // from goals/<sessionId>.json .goal ("" if absent)
|
||||||
Phase string `json:"phase"` // from goals/<sessionId>.json .phase ("" if absent)
|
Phase string `json:"phase"` // from goals/<sessionId>.json .phase ("" if absent)
|
||||||
Emojis string `json:"emojis"` // 3 emojis representing the task (from goals .emojis; "" if absent)
|
DodContract string `json:"dod_contract"` // from goals .dod_contract: fixed acceptance criterion ("" if absent)
|
||||||
Name string `json:"name"` // manual rename of the terminal (from goals .rename; "" if none)
|
DodStatus string `json:"dod_status"` // from goals .dod_status: pending|met|failed ("" if absent)
|
||||||
Status string `json:"status"` // idle|busy|waiting (from sessions/<pid>.json)
|
Role string `json:"role"` // from goals .role: orchestrator|executor ("" if absent; defaults to executor in consumers)
|
||||||
Cwd string `json:"cwd"` // working directory of the session
|
Emojis string `json:"emojis"` // 3 emojis representing the task (from goals .emojis; "" if absent)
|
||||||
TmuxWindow string `json:"tmux_window"` // "" for now (populated in a later phase)
|
Name string `json:"name"` // manual rename of the terminal (from goals .rename; "" if none)
|
||||||
Alive bool `json:"alive"` // process alive AND procStart matches (guards against PID recycling)
|
Status string `json:"status"` // idle|busy|waiting (from sessions/<pid>.json)
|
||||||
UpdatedAt int64 `json:"updated_at"` // from sessions/<pid>.json .updatedAt (epoch millis)
|
Cwd string `json:"cwd"` // working directory of the session
|
||||||
CtxPct int `json:"ctx_pct"` // context window used %, from runtime/<sessionId>.json; -1 if unknown
|
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/<pid>.json .updatedAt (epoch millis)
|
||||||
|
CtxPct int `json:"ctx_pct"` // context window used %, from runtime/<sessionId>.json; -1 if unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ type sessionFile struct {
|
|||||||
|
|
||||||
// goalFile mirrors the on-disk shape of ~/.claude/goals/<sessionId>.json.
|
// goalFile mirrors the on-disk shape of ~/.claude/goals/<sessionId>.json.
|
||||||
type goalFile struct {
|
type goalFile struct {
|
||||||
Goal string `json:"goal"`
|
Goal string `json:"goal"`
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
Emojis string `json:"emojis"`
|
Emojis string `json:"emojis"`
|
||||||
Rename string `json:"rename"`
|
Rename string `json:"rename"`
|
||||||
|
DodContract string `json:"dod_contract"`
|
||||||
|
DodStatus string `json:"dod_status"`
|
||||||
|
Role string `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// runtimeFile mirrors ~/.claude/runtime/<sessionId>.json written by statusline.sh
|
// runtimeFile mirrors ~/.claude/runtime/<sessionId>.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).
|
// KITTY_PID from the process environ (0 if unreadable / absent).
|
||||||
f.KittyPID = readKittyPID(sess.PID)
|
f.KittyPID = readKittyPID(sess.PID)
|
||||||
|
|
||||||
// Join goal/phase/emojis/name from goals/<sessionId>.json (optional).
|
// Join goal/phase/emojis/name + DoD contract/status from
|
||||||
f.Goal, f.Phase, f.Emojis, f.Name = readGoal(goalsDir, sess.SessionID)
|
// goals/<sessionId>.json (optional).
|
||||||
|
f.Goal, f.Phase, f.Emojis, f.Name, f.DodContract, f.DodStatus, f.Role = readGoal(goalsDir, sess.SessionID)
|
||||||
|
|
||||||
// Context usage from runtime/<sessionId>.json (written by statusline).
|
// Context usage from runtime/<sessionId>.json (written by statusline).
|
||||||
f.CtxPct = readCtxPct(runtimeDir, sess.SessionID)
|
f.CtxPct = readCtxPct(runtimeDir, sess.SessionID)
|
||||||
@@ -179,18 +183,19 @@ func readKittyPID(pid int) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// readGoal reads goals/<sessionID>.json and returns its goal, phase, emojis and
|
// readGoal reads goals/<sessionID>.json and returns its goal, phase, emojis,
|
||||||
// manual rename. If the file is absent or unparseable, all are "".
|
// manual rename, DoD contract, DoD status and role. If the file is absent or
|
||||||
func readGoal(goalsDir, sessionID string) (goal, phase, emojis, rename string) {
|
// 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"))
|
raw, err := os.ReadFile(filepath.Join(goalsDir, sessionID+".json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", ""
|
return "", "", "", "", "", "", ""
|
||||||
}
|
}
|
||||||
var g goalFile
|
var g goalFile
|
||||||
if json.Unmarshal(raw, &g) != nil {
|
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/<sessionID>.json and returns the context-window used
|
// readCtxPct reads runtime/<sessionID>.json and returns the context-window used
|
||||||
|
|||||||
@@ -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}`,
|
fmt.Sprintf(`{"pid":%d,"sessionId":"aaaaaaaa-1111-2222-3333-444444444444","cwd":"/home/enmanuel/fn_registry","procStart":%q,"status":"idle","updatedAt":1000}`,
|
||||||
livePID, liveProcStart))
|
livePID, liveProcStart))
|
||||||
writeFile(t, filepath.Join(goals, "aaaaaaaa-1111-2222-3333-444444444444.json"),
|
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
|
// Session B: alive (own PID again — same process, valid procStart), no
|
||||||
// goal sidecar -> rename = basename(cwd) = projectx, status busy.
|
// goal sidecar -> rename = basename(cwd) = projectx, status busy.
|
||||||
@@ -100,6 +100,12 @@ func TestListClaudeFleetFrom(t *testing.T) {
|
|||||||
if a.Phase != "investigando" {
|
if a.Phase != "investigando" {
|
||||||
t.Errorf("session A: phase join failed, got %q", a.Phase)
|
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.
|
// Rename = goal truncated to 48 runes.
|
||||||
wantRename := string([]rune(a.Goal)[:48])
|
wantRename := string([]rune(a.Goal)[:48])
|
||||||
if a.Rename != wantRename {
|
if a.Rename != wantRename {
|
||||||
@@ -117,6 +123,9 @@ func TestListClaudeFleetFrom(t *testing.T) {
|
|||||||
if b.Goal != "" || b.Phase != "" {
|
if b.Goal != "" || b.Phase != "" {
|
||||||
t.Errorf("session B: expected empty goal/phase, got goal=%q phase=%q", 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" {
|
if b.Rename != "projectx" {
|
||||||
t.Errorf("session B: rename = %q, want basename(cwd) %q", b.Rename, "projectx")
|
t.Errorf("session B: rename = %q, want basename(cwd) %q", b.Rename, "projectx")
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-12
@@ -6,18 +6,21 @@ version: "1.0.0"
|
|||||||
algebraic: product
|
algebraic: product
|
||||||
definition: |
|
definition: |
|
||||||
type ClaudeFleet struct {
|
type ClaudeFleet struct {
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
KittyPID int `json:"kitty_pid"`
|
KittyPID int `json:"kitty_pid"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
Rename string `json:"rename"`
|
Rename string `json:"rename"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
Goal string `json:"goal"`
|
Goal string `json:"goal"`
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
Status string `json:"status"`
|
DodContract string `json:"dod_contract"`
|
||||||
Cwd string `json:"cwd"`
|
DodStatus string `json:"dod_status"`
|
||||||
TmuxWindow string `json:"tmux_window"`
|
Role string `json:"role"`
|
||||||
Alive bool `json:"alive"`
|
Status string `json:"status"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
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/<PID>.json + goals/<sessionId>.json). Pieza de datos de la app TUI fleetview, producida por list_claude_fleet_go_infra."
|
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/<PID>.json + goals/<sessionId>.json). Pieza de datos de la app TUI fleetview, producida por list_claude_fleet_go_infra."
|
||||||
tags: [claude-fleet, infra, claude, session, process]
|
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). |
|
| `Target` | string | derivado | sessionId[:8] + "@" + basename(cwd). |
|
||||||
| `Goal` | string | goals/<sessionId>.json .goal | "" si no hay sidecar. |
|
| `Goal` | string | goals/<sessionId>.json .goal | "" si no hay sidecar. |
|
||||||
| `Phase` | string | goals .phase | "" 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. |
|
| `Status` | string | sessions .status | idle\|busy\|waiting. |
|
||||||
| `Cwd` | string | sessions .cwd | Working directory. |
|
| `Cwd` | string | sessions .cwd | Working directory. |
|
||||||
| `TmuxWindow` | string | (reservado) | "" por ahora; se rellena en fase posterior. |
|
| `TmuxWindow` | string | (reservado) | "" por ahora; se rellena en fase posterior. |
|
||||||
|
|||||||
Reference in New Issue
Block a user