feat(orquestador): feed reactivo FLEET-STATE + fix peek de drain_fleet_events
El orquestador no se enteraba de los cambios de estado de su flota: el drenado
era manual y el peek documentado `./fn run drain_fleet_events --advance false`
devolvia un falso `{total_new:0, cursor:0}` porque `fn run` mapea los argumentos
posicionalmente y no parsea flags `--nombre valor` (events_path acababa valiendo
"--advance", una ruta inexistente).
- drain_fleet_events: nuevo helper _normalize_fn_run_flags que renormaliza el
patron `--advance <bool>` aplanado por `fn run`, de modo que el peek funciona
directo desde la CLI sin tocar el runner de Go. Bump 1.1.0 + growth log + tests
del normalizador (unit y end-to-end por HOME).
- summarize_fleet_transitions (nueva, pure, grupo claude-fleet): resume el dict
by_classification de drain en un bloque de una linea con las tres categorias
accionables (terminados / reclaman / estancados), dedup por session_id y
truncado de objetivo.
- hook_fleet_state_inject.sh (UserPromptSubmit): si la sesion es role=orchestrator
(leido de ~/.claude/goals/<session_id>.json), hace peek de la cola sin mover el
cursor y emite el bloque FLEET-STATE cada turno. Degrada limpio si el watcher
esta caido, la cola no existe o la sesion no es orquestador.
El registro del hook va en .claude/settings.local.json (gitignored, fuera de este
commit). Pendiente, lo integra otro agente: documentar el bloque FLEET-STATE en
.claude/commands/orquestador.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: summarize_fleet_transitions
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def summarize_fleet_transitions(by_classification: dict, max_items: int = 6) -> str"
|
||||
description: "Resume en una sola linea las transiciones accionables de la flota de Claudes para inyectar por un hook UserPromptSubmit al orquestador. Toma el dict by_classification de drain_fleet_events (agrupado por campo `to`) y condensa SOLO las tres categorias accionables: DICE_TERMINADO (terminados), RECLAMA (reclaman) y ESTANCADO (estancados); ignora TRABAJANDO, GONE y MAL_LANZADO. Funcion pura, determinista y defensiva: deduplica por session_id, trunca el goal y limita el total con overflow `(+N mas)`."
|
||||
tags: [orchestration, claude-fleet, fleet, summary, hook, orchestrator]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: by_classification
|
||||
desc: "dict agrupado por campo `to` tal como lo devuelve drain_fleet_events. Solo se leen las claves accionables DICE_TERMINADO, RECLAMA y ESTANCADO; cada valor es una lista de eventos (dicts) con al menos session_id (str UUID), goal (str) y to (str). Puede ser None o {} (devuelve 'sin cambios')."
|
||||
- name: max_items
|
||||
desc: "limite total de eventos mostrados sumando las tres categorias (default 6). Al superarse se trunca y se añade ' (+N mas)'. El cupo se llena en orden de categorias terminados -> reclaman -> estancados."
|
||||
output: "una cadena de UNA sola linea. 'FLEET-STATE: sin cambios' si no hay nada accionable; en caso contrario 'FLEET-STATE: terminados=[<sid8>:<goal>] reclaman=[...] estancados=[...] (+N mas) (drain con ./fn run drain_fleet_events para consumir)' omitiendo las categorias vacias."
|
||||
tested: true
|
||||
tests:
|
||||
- "test_golden_tres_categorias_con_datos"
|
||||
- "test_categorias_vacias_sin_cambios"
|
||||
- "test_solo_no_accionables_sin_cambios"
|
||||
- "test_dedup_por_session_id_se_queda_ultima"
|
||||
- "test_truncado_de_goal_mayor_de_40"
|
||||
- "test_max_items_con_overflow"
|
||||
- "test_entrada_none_sin_cambios"
|
||||
- "test_entrada_dict_vacio_sin_cambios"
|
||||
- "test_valor_no_lista_se_salta"
|
||||
- "test_eventos_no_dict_se_saltan"
|
||||
- "test_session_id_ausente_usa_placeholder"
|
||||
- "test_goal_ausente_usa_placeholder"
|
||||
- "test_omite_categorias_vacias_solo_renderiza_con_datos"
|
||||
- "test_varios_eventos_misma_categoria_separados_por_coma"
|
||||
test_file_path: "python/functions/infra/summarize_fleet_transitions_test.py"
|
||||
file_path: "python/functions/infra/summarize_fleet_transitions.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from summarize_fleet_transitions import summarize_fleet_transitions
|
||||
|
||||
by_classification = {
|
||||
"DICE_TERMINADO": [
|
||||
{"session_id": "a1b2c3d4-aaaa-bbbb", "goal": "Migrar tabla salesforce a europe-west1", "to": "DICE_TERMINADO"},
|
||||
],
|
||||
"RECLAMA": [
|
||||
{"session_id": "e5f6g7h8-cccc-dddd", "goal": "Necesito clave API de Metabase", "to": "RECLAMA", "urgent": True},
|
||||
],
|
||||
"ESTANCADO": [],
|
||||
"TRABAJANDO": [
|
||||
{"session_id": "99887766-eeee-ffff", "goal": "Indexando registry", "to": "TRABAJANDO"},
|
||||
],
|
||||
}
|
||||
|
||||
linea = summarize_fleet_transitions(by_classification)
|
||||
# FLEET-STATE: terminados=[a1b2c3d4:Migrar tabla salesforce a europe-w…] reclaman=[e5f6g7h8:Necesito clave API de Metabase] (drain con ./fn run drain_fleet_events para consumir)
|
||||
print(linea)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un hook (tipico `UserPromptSubmit`) necesita resumir en una sola linea las
|
||||
transiciones pendientes de la flota para inyectarlas al orquestador-Claude. Se
|
||||
encadena despues de `drain_fleet_events` (que produce el `by_classification`) para
|
||||
darle al orquestador, sin coste de lectura extra, el estado accionable de un
|
||||
vistazo: quien dice haber terminado, quien reclama atencion y quien esta estancado.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Dedup por session_id**: dentro de cada categoria, si un mismo `session_id`
|
||||
aparece en varios eventos se conserva solo la ultima aparicion (la transicion
|
||||
mas reciente en la lista). Eventos sin `session_id` valido no se deduplican
|
||||
entre si y cada uno cuenta como entrada propia.
|
||||
- **Truncado de goal**: el `goal` se recorta a 40 caracteres con elipsis `…`; un
|
||||
`goal` ausente o vacio se muestra como `(sin objetivo)` y un `session_id`
|
||||
ausente/vacio como `????????`.
|
||||
- **Solo categorias accionables**: TRABAJANDO, GONE y MAL_LANZADO se ignoran a
|
||||
proposito. Si las tres categorias accionables estan vacias o no existen,
|
||||
devuelve `FLEET-STATE: sin cambios`.
|
||||
- **Reparto de max_items**: el cupo total se llena recorriendo las categorias en
|
||||
orden (terminados -> reclaman -> estancados); los sobrantes se cuentan en
|
||||
`(+N mas)`. Es defensiva ante datos mal formados (valores no-lista o eventos
|
||||
no-dict se omiten sin lanzar excepcion).
|
||||
Reference in New Issue
Block a user