# Reglas de equipo — correcciones y cambios en mbinv

> ⚠️ **Estas reglas SOLO las pueden reescribir Andrea o Manuel.** El resto —
> programadores y agentes de Claude— las **leen y las siguen**. Cualquier cambio a
> este archivo requiere la aprobación de Andrea o Manuel (ver `CODEOWNERS`).
>
> Este archivo es la **fuente única** de las reglas del equipo, compartida por todas
> las cuentas de Claude (grupo y personales) porque vive en el repo. No copiar las
> reglas a la memoria personal de nadie: se leen de acá.

---

## 0. Cuándo algo se vuelve regla de equipo — y cuándo NO

> *"la idea es ir generando reglas para todo el equipo, pero me he encontrado que si
> no están bien afinadas causan ruido en el equipo, especialmente gerencial. Lo que
> quiero es que cuando ya tengamos bien sólida una metodología, la podamos volver
> regla para todo el equipo."* — Manuel, 27-ago-2026

**Esta sección va primera a propósito: es la que decide si las demás existen.**

### De dónde sale el ruido

No de que haya muchas reglas. De que **una regla que necesita un humano que la haga
cumplir convierte a alguien en policía** — y ese alguien termina siendo Andrea o
Manuel, recordándosela a gente que ya tiene su trabajo. Eso es el ruido gerencial.

Una regla que se hace cumplir sola no genera ninguno: corrige en el momento, a quien
la rompió, sin testigos y sin reunión.

### Las seis condiciones. Se cumplen TODAS, o no se promueve

| | Condición | Cómo se comprueba |
|---|---|---|
| 1 | **Ya costó algo medible** | hay un número y una fecha: cuántas horas, cuántos tiquetes, cuántos sitios. Una idea buena no basta |
| 2 | **Tiene un síntoma literal** | se puede escribir *"está mal hecho cuando pasa exactamente X"*, sin que haga falta criterio para verlo |
| 3 | **La comprueba un guion** | un hook, una prueba o un vigilante. Si depende de que alguien se acuerde, no está lista |
| 4 | **Se probó contra casos REALES** | contra lo que de verdad pasó, no contra ejemplos inventados |
| 5 | **Le sirve a alguien que no es Claude** | si sólo describe cómo trabaja una ventana, va en las convenciones, no acá |
| 6 | **El que la rompe se entera solo** | sin que un gerente se lo diga. Ésta es la que quita el ruido |
| 7 | **Quien la recibe puede aplicarla sin preguntarle nada a nadie** | si hay que explicársela, todavía no está escrita |

### Qué pasa con la que no las cumple

**No se descarta: se queda esperando.** Vive como nota en su tiquete o como convención
de quien la propuso, y **vuelve cuando junte la evidencia que le falta**. Casi siempre
lo que falta es la 1 (todavía no costó nada) o la 3 (no hay con qué comprobarla).

⚠️ **Una regla a medio afinar promovida es peor que ninguna**, porque gasta la atención
del equipo y enseña que estas reglas se pueden ignorar.

### El síntoma de que se promovió antes de tiempo

Que haya que **repetirla**. Si a la semana alguien la está recordando en un chat, no
falló el equipo: **faltaba la condición 3 o la 6**, y lo que corresponde es construir
el guion que la haga cumplir, no insistir.

### La condición 7 es la más cara de aprender, y ya se pagó

> *"ya nos pasó de tocar los tiquetes de los agentes y no entendieron qué se les pedía
> o para qué se tocaban sus tiquetes, o que les llegan tiquetes y no logran
> descifrarlos, entonces caemos a: **es otra prueba de Manuel, no le pongamos coco**"*
> — Manuel, 27-ago-2026

Eso no es una molestia: es **pérdida de crédito**. El que recibe algo que no entiende
no pide aclaración — deja de enganchar, y a partir de ahí ignora también lo que sí
importaba.

**Medido ese día en Freshdesk, sobre 14 días:**

```
tiquetes tocados                                300
asignados a alguien que no somos nosotros       145
   de esos, con nota nuestra                     27
      contestaron y siguieron                    11
      NUNCA contestaron                          16
      contestaron preguntando qué era             0
```

⚠️ **El cero es el dato.** Nadie pide aclaración. Por eso «no contestó» no se puede
leer como desinterés: es **el síntoma de que no se pudo descifrar**. Y el que está mal
escrito es lo que se mandó, no quien lo recibió.

Vale igual para un tiquete que para una regla: si hay que explicarla después de
mandarla, no estaba lista.

**Y tiene con qué comprobarse**, que es lo que la vuelve promovible: el aviso de
arranque de cada ventana (`tiquetes_sueltos.py`) lista los traspasos que la otra
persona nunca contestó, y se lo dice **a quien lo escribió** — no a Manuel.

### Lo medido que justifica la condición 4

El 27-ago-2026 se escribió un hook para cazar un cierre que dejaba trabajo prometido
sin hacer. **Daba 13 de 13 en verde con casos inventados.** Al probarlo contra los
cierres de verdad de ese mismo día, **no cazaba el único que había motivado el
trabajo** — porque decía «arranco con eso» y «terminé» en el mismo párrafo, y el
«terminé» lo eximía.

Una regla validada sólo contra ejemplos inventados **se ve sólida y no lo es**. Es la
misma familia del `MB_TECHO=0` que estuvo roto una semana: *cuando falla se ve igual
que cuando funciona*.

---

## 1. LEY: nada sin tiquete

**Nunca** se trabaja una corrección, modificación o cambio **sin un tiquete**. Si ya
hay algo en curso sin tiquete, **crear uno** antes de seguir. Pasa muy seguido que
soporte reporta por chat, se arranca sin tiquete, se olvida y queda en el aire — sin
bitácora, sin dueño. El tiquete es lo que hace que el trabajo exista.

## 1b. Un hallazgo NO es un tiquete

La §1 dice que no se trabaja sin tiquete. **No dice que cada cosa que uno encuentra
de paso merezca uno.** Son cosas distintas y confundirlas tiene un costo medible.

**Medido el 11-ago-2026:** la cola visible de Manuel tenía **45 tiquetes**. De esos,
**32 los habían abierto sesiones de Claude esa misma semana** como hallazgos, y solo
**12 eran trabajo de un cliente** (el que falta no quedó clasificado en el conteo).
Ese mismo día, ya con el acuerdo tomado, cuatro ventanas distintas abrieron cuatro
tiquetes más en una tarde.

**La regla:**

- **Trabajo que alguien pidió** → tiquete, siempre. Eso es la §1.
- **Hallazgo encontrado de paso** → **no se abre tiquete solo**. Se junta con los
  demás y se le proponen al final: *"encontré esto, esto y esto; ¿cuál amerita
  tiquete?"*. "Ninguno" es una respuesta válida.
- **Excepción:** si es urgente y le está pegando a un cliente ahora, tiquete de una,
  sin preguntar.
- Los hallazgos internos que igual se abran **nacen en `Parqueado`**, no en la cola de
  trabajo de nadie. Siguen buscables y con su detalle; si ameritan trabajarse, se
  sacan de ahí.

**Por qué va acá y no en la memoria de una sesión:** cada ventana de Claude carga su
memoria **al arrancar**. Un acuerdo tomado a media mañana no llega a las ventanas que
ya estaban corriendo — por eso ese día se siguieron abriendo tiquetes después de
acordar que no. Este archivo, en cambio, lo lee cualquier sesión que trabaje el repo.
Las reglas de coordinación viven acá; la memoria sirve para lo de cada quien.

## 2. Nada se da por probado sin evidencia

- **Antes/después**, probado **en pruebas** (nunca en producción). `php -l` es solo
  sintaxis, **no** es prueba.
- Los **comentarios del bot (Copilot/codex)** los revisa y resuelve **quien hace el
  fix**, antes de pedir review humano — no dejarlos colgando.
- El tiquete lleva una **guía de prueba completa**: qué sitio/URL (el ambiente aislado
  de la rama), paso a paso qué hacer, qué se debe ver (antes vs después), **y qué hacer
  según el resultado** (ver sección 3).
- **Se prueba por el camino que usa la gente, no por el más cómodo.** Si el cambio se
  usa desde una pantalla, la prueba es **clic real en el botón real**, con sesión real.
  Un `curl` que devuelve 200 **no** prueba que el botón funcione: el navegador manda
  cabeceras, preflight de CORS y cookies que `curl` no manda. Lo mismo aplica a probar
  una consulta suelta en vez de la pantalla, o el endpoint en vez del flujo completo.
- **Si solo se pudo probar por el camino cómodo, hay que decirlo.** "Probado por API,
  falta probarlo desde la pantalla" es una entrega honesta; reportarlo como verificado
  a secas es lo que deja bugs vivos con el tiquete cerrado.
- **Sin insumos para reproducir, no se arregla a ciegas.** Cuando el error depende de un
  dato que trae el usuario —**carga masiva / importación**, ante todo— el reporte debe
  **adjuntar el archivo exacto que falla**. Es parte del "antes": sin él no se reproduce
  ni se prueba, y adivinar el fix es cómo se cierran tiquetes con el bug vivo. Si el
  tiquete llega sin el archivo, se pide antes de arrancar (no después de "corregir").

> **De dónde salió esta regla** (ago-2026): el panel de huellas rompió **dos veces** el
> mismo botón, y las dos veces la verificación previa estaba en verde. Primero, CORS sin
> `methods` explícito bloqueaba PUT/DELETE solo en el navegador. Después, el cliente HTTP
> mandaba `Content-Type: application/json` en peticiones sin cuerpo, y el servidor
> respondía **400 "Bad Request"** — con eso, los tres botones de Eliminar del panel nunca
> funcionaron desde el navegador. En los dos casos `curl` pasaba limpio, porque `curl` no
> manda esas cabeceras. No fue falta de pruebas: fue probar por donde no era.

## 3. Flujo de estados

`En Desarrollo` → `Pruebas de QA` → (el dueño **mergea**) → **`Resuelto`** → **vuelve a
Soporte** → Soporte actualiza al cliente **en el horario acordado** → **visto bueno del
cliente** → `Cerrado`.

- **Nunca `Cerrado` sin el OK del cliente.**
- Si en QA **no funciona**: no cerrarlo; documentar qué se vio y en qué paso (captura +
  status), estado a `En Desarrollo`, y reasignar al dueño con esa evidencia.

## 3b. El GRUPO es el área. La ETAPA va en el estado

**El grupo dice de qué área es el tiquete. El estado dice en qué momento va.** Son dos cosas
distintas y hay que dejarlas separadas:

- **El grupo no se mueve para avanzar el flujo.** Un tiquete de Programación sigue siendo de
  Programación mientras está en QA, mientras espera autorización y mientras se publica.
- **Para avanzar, se cambia el ESTADO** (§3), nunca el grupo.

**Por qué, y esto es lo importante:** en Freshdesk el grupo además decide **quién puede
recibir el tiquete**. Asignar a alguien que no está en el grupo **se ignora en silencio** —
el PUT contesta 200, aplica el estado y la fecha, y **deja el responsable vacío sin avisar**
(§12). Así que cada vez que el grupo se usa como etapa, mover el tiquete puede dejar fuera a
la persona que lo va a trabajar. El problema no es la asignación: es usar el grupo para dos
cosas a la vez.

**Grupos que eran etapa disfrazada, y a qué estado corresponden:**

| Grupo (etapa) | Estado que le corresponde |
|---|---|
| Correcciones | `En Desarrollo` |
| Pruebas/QA | `Pruebas de QA` |
| Programación Autorización | `Autorización de versión` |
| Publicación de versión | `Publicación de versión` |
| Cierre | `Resuelto` → `Cerrado` |
| Seguimiento | `Esperando respuesta nuestra` |
| Reportería Nuevos / Análisis | `En Análisis` |
| Reportería Cambios / Correcciones | `En Desarrollo` |
| Reportería Revisión | `Pruebas de QA` |

Los cinco de Reportería son una sola área partida en cinco etapas: quedan como **un solo
grupo Reportería**. Los estados `Autorización de versión` y `Publicación de versión` se
crearon el 14-ago-2026 para esto, así que las once etapas tienen su equivalente.

**Medido el 14-ago-2026** sobre los 500 tiquetes tocados desde el 1-may, para que se vea que
esto no rompe nada en uso:

```
grupos con tiquetes abiertos ...........  9 de 27
grupos sin NI UN tiquete en 3½ meses ... 14 de 27   (los 5 de Reportería entre ellos)
tiquetes SIN grupo ..................... 215        <- "ninguno" es el grupo más usado
Programación ........................... 144 tiquetes (106 abiertos)
```

Los cinco grupos de Reportería son el ejemplo exacto del problema —una sola área partida en
cinco etapas— y **ninguno tiene un tiquete desde mayo**: ya se había abandonado en la práctica.

⚠️ **Los grupos que salen NO se borran.** "Sin actividad desde mayo" no es "sin uso": los 27
tienen historia, y los 11 que eran etapa cargan **~1,980 tiquetes** entre todos (Seguimiento
933, Cierre 438, Reportería Nuevos 281, Pruebas/QA 149...). Borrar uno deja sus tiquetes sin
grupo y sin forma de filtrarlos. Se renombran con un aviso —`ZZ — no usar (ver §3b)`— para que
nadie los elija por error, y el historial queda intacto.

**Al mover un tiquete, entonces:** cambiar el estado, dejar el grupo quieto, y asignar a la
persona. Si por algo hay que cambiar de grupo, **mandar grupo y responsable en el mismo
movimiento** y releer el tiquete después para confirmar que la asignación se aplicó.

## 3c. Cuando el trabajo cruza de área: padre en Soporte, hijo en Programación

Si el grupo no se mueve (§3b), falta contestar qué pasa cuando **un caso de Soporte necesita
programación** — que es el cruce más frecuente que tenemos. La respuesta **no** es meter a
todos los agentes en todos los grupos: eso vacía de sentido al grupo y no resuelve lo de
fondo.

**Se abre un tiquete hijo.** El caso del cliente se queda donde está y el trabajo técnico nace
en su propia área:

| | Padre | Hijo |
|---|---|---|
| Vive en | **Soporte** | **Programación** |
| Dueño | Quien le habla al cliente | Quien programa |
| Plazo | El del cliente (horas) | El del desarrollo (semanas) |
| Cierra cuando | El cliente confirma | Se entrega el código |

Así **nadie necesita pertenecer a dos grupos**, y se arregla algo que un solo tiquete no puede
hacer: sostener **dos relojes distintos**. Hoy, cuando el caso se traspasa entero a
Programación, el tiquete arrastra el plazo del cliente a un trabajo que tarda semanas, y se
pierde de vista quién le responde.

**Tres reglas del modelo:**

- **El padre nunca cierra antes que el hijo.**
- **Al cliente le responde siempre el dueño del padre**, aunque el trabajo lo haga otro.
- **Nada de padre/hijo dentro del mismo equipo.** Si los dos tiquetes son del mismo grupo, no
  es padre/hijo: es un tiquete mal partido, o basta con la miga de "Sale de #NNNNN".

En la API va como `parent_id` en el `POST /tickets`; el padre queda con `association_type: 1` y
el hijo con `2`. El hijo lleva **solicitante interno**, así que el cliente no recibe nada de él.

⚠️ **El costo, que hay que aceptar de entrada:** esto **duplica el conteo de tiquetes**. Queda
acordado que **los hijos no cuentan como carga de la mesa de soporte** sino como trabajo de
programación. Si no se dice antes, en tres meses se discute por qué "subieron los tiquetes".

> **Estado al 14-ago-2026:** el modelo se definió el 30-jul y **no se está usando para esto**.
> De 500 tiquetes tocados desde mayo, sólo 16 tienen asociación — y **los 16 están en
> Programación**, padre e hijo en el mismo grupo, que es justo lo que el modelo prohíbe. Se usa
> para agrupar proyectos internos, no para el cruce con Soporte. Mientras tanto Soporte tiene
> 18 tiquetes abiertos y Programación 106: los casos entran por Soporte y terminan viviendo en
> Programación.

## 4. Marcar EN PROGRESO

Al crear un tiquete y empezar a trabajarlo de una, **indicarlo** (estado + asignación)
para que nadie duplique el esfuerzo.

## 5. Asignación y escalación

- **PT (POSTouch) → Andrea** (única que lo ve). **BO → el programador del módulo** (ver
  el mapa de dominios; se regenera del `git blame` reciente).
- **Escala a Manuel** si es **urgente** (cliente esperando) y no hay respuesta en un
  tiempo razonable. **Trivial** (detectado proactivamente, sin cliente detrás) → espera
  al **día hábil**.
- Programadores de BO y otros sistemas: **lunes a viernes, horario variable (HO)**. Para
  lo no-urgente, el "tiempo razonable" cuenta **solo horas hábiles**.
- **Guía (no ley):** no desplegar/actualizar **viernes ni fin de semana**, salvo
  corrección urgente — para no hacer trabajar a programación en horario inhábil.

## 6. Auto-corrección: cuándo el agente consulta

- **Confianza alta** (reproduce el error, señala la línea exacta, y el fix sigue un
  patrón que el propio código ya usa) → **arregla y presenta el PR**.
- **Confianza baja** → **consulta el diagnóstico al programador ANTES de escribir
  código** (validar el disparador).
- Si el agente **se estanca**, para y consulta con *"esto descarté (A, B, C), esto
  necesito (X)"* — no seguir cavando.

## 7. Dominio: las ventas se hacen solo desde el PT

Las ventas se crean **únicamente desde el PT (POSTouch)**, no desde el Back Office. Al
sugerir pruebas de documentos en el BO, **no usar una venta** de ejemplo — usar un
**traslado** o un **ajuste** (que sí se hacen en el BO y no llevan proveedor).

## 8. Ambiente de prueba aislado por rama

`probar-rama.sh <rama>` (en el repo `oci-dr`) monta un ambiente aislado en pruebas con
URL clicable `<slug>.pruebas.sistemasmb.com` (wildcard DNS ya creado). **Cada rama en
su caja** — dos cambios nunca se pisan. Para diagnosticar, `app_dev.php` muestra la
excepción y el status reales. Borrar con `probar-rama.sh <rama> --borrar`.

## 9. Fechas y horas: siempre Guatemala, anclada explícita

Todo lo que **compare, guarde o dispare por fecha/hora** (agendadores, cron, sellos de
tiempo, "vence el…", "actualizar a las…", reportes con hora) usa **hora de Guatemala
anclada de forma explícita** (UTC−6; Guatemala no tiene horario de verano) — **nunca** la
hora "local" del sistema operativo ni UTC crudo.

El "ahora" sin zona (`now()` / `datetime.now()` / `new Date()`) depende de cómo esté
configurado **ese** servidor. El default de casi todo servidor cloud es **UTC**, así que un
`now()` naive se corre **6 horas** y el reloj dispara a la hora equivocada **sin dar ningún
error** — nadie se entera hasta que un despliegue o un aviso sale a deshora.

- **Al revisar o escribir cualquier cambio que toque tiempo**, verificá de dónde sale el
  "ahora", en qué zona se guarda y en qué zona se compara. Si no está anclado a GT de forma
  explícita, es un **bug latente** aunque hoy funcione.
- En **reportes y mensajes al equipo**, mostrar la hora en **GT** (las fuentes —OCI,
  Freshdesk, logs— suelen devolver UTC; convertir antes de mostrar).
- **Ejemplo real (6-ago-2026, #23102):** el agendador del panel de despliegues comparaba
  con `datetime.now()` naive. Funcionaba solo porque el servidor estaba en
  `America/Guatemala`; pasarlo a UTC habría corrido cada despliegue programado 6 h. Se
  ancló con un offset fijo GT (`datetime.now(GT)`), correcto aunque el servidor esté en UTC.

## 10. Prioridad de tiquetes — qué se atiende primero

Orden para elegir qué trabajar, de más a menos urgente. Cada nivel tiene una **señal
detectable** (estado o `type`), no es criterio de cada quien:

1. **Errores de programación que rompen o frenan la operación** (bug). Señal: `type`
   **Incidente / Problema** → prioridad **Urgente**. Si un bug quedó mal tipeado, corregir el
   tipo para que la regla lo tome. Un detalle cosmético que no frena nada **no** es Urgente:
   baja de nivel.
2. **Tiquetes que esperan respuesta NUESTRA** — la pelota está de nuestro lado y el cliente (o
   un compañero) está **bloqueado esperándonos**. Señal: estado **«Esperando respuesta
   nuestra»** (id 13). Incluye los que esperan la revisión de Manuel (`cf_pendiente_de_mb =
   "Sí"`). No pueden quedar dormidos.
3. **Tiquetes de clientes** activos (que no son bug ni esperan respuesta nuestra): estados
   Open / En Desarrollo con un cliente detrás.
4. **Proyectos** internos y funcionalidades nuevas. Aquí entra también la **seguridad y la
   deuda técnica** que no está rompiendo nada (p.ej. endurecer una consulta, sacar una
   credencial del código).
5. **Parqueados** (estado **Parqueado**): lo último.

**Override:** si un cliente **no puede vender, facturar o inventariar**, salta al frente esté
en el nivel que esté.

**Los que esperan respuesta del CLIENTE** (le pedimos un dato, la pelota es de él) van a
estado **Pending «Esperando su respuesta»** y **salen de esta cola** hasta que conteste — no se
priorizan porque no dependen de nosotros. Es el simétrico del estado 13: uno es «esperamos al
cliente», el otro «el cliente nos espera a nosotros».

## 11. La evidencia del "después" la entrega el programador

Amplía la §2. El reparto de la evidencia es claro:

- El **"antes"** (el error) lo manda **quien reporta** (cliente/soporte) al abrir el tiquete —
  captura, texto exacto del mensaje, datos para reproducir.
- **Antes de corregir, validar que el reporte trae lo suficiente.** Si ya alcanza (p. ej. la captura
  muestra el error), **no pedir más: trabajarlo.** Si de verdad falta algo, pedir lo puntual **y mover
  el tiquete** (enviar al cliente + pasar a `Pendiente`). Nunca dejar una nota de "falta información"
  sin cambiar el estado — el tiquete queda **muerto** en la cola.
- El **"después"** (que se probó y quedó corregido) lo entrega **el programador**: imágenes o video
  del BO mostrando que ya no sale el error / que la funcionalidad queda como se necesita. **QA no
  valida sin esa evidencia.**

## 12. Traza al mover un tiquete

Toda nota que cambie **asignación o estado** cierra con una línea de **traza**: el **antes → después**
(a quién quedó asignado, en qué estado, o si se le regresa a alguien), con **nombre** del agente y
**nombre** del estado (no ids ni números). Que quien la lea sepa de un vistazo qué pasó, sin adivinar.
Verificar siempre que el cambio **realmente se aplicó** releyendo el tiquete: asignar a un agente
fuera del grupo del tiquete **se ignora en silencio** (el PUT da 200 y no aplica). La forma de no
toparse con eso es no usar el grupo como etapa (§3b); si aun así hay que cambiar de grupo, mandar
**grupo y responsable juntos** en el mismo cambio.

## 12b. La última nota EMPIEZA con qué hacer, no con qué se hizo

> *"lo que me gustaría es que en la última nota pongan el resumen de lo que se quiere
> que se haga, para no tener que perder el tiempo revisando las notas anteriores de lo
> que se hizo para comprender lo que quieren que uno haga"*
> — un agente del equipo, 27-ago-2026

**Cuando un tiquete pasa a otra persona, su última nota empieza con un bloque de
acción.** Primero qué hay que hacer, quién, para cuándo y qué evidencia traer. El
diagnóstico, lo medido y la historia van **después**, para el que quiera el contexto —
no para el que tiene que trabajar.

**El síntoma, literal:** si para saber qué hacer hay que leer notas anteriores, la nota
está mal escrita. No falló quien la recibió.

### Lo medido el 27-ago-2026, en nuestras propias notas

Los siete tiquetes que le pasamos a alguien y esa persona nunca contestó:

```
TIQUETE  PALABRAS  cómo empieza la última nota nuestra
#23845      617    contando qué se diagnosticó
#23804      147    contando en qué está esperando
#23754      180    "Criterio de Manuel, 24-ago..."
#23768      132    "Traza de la reasignación..."
#23802      108    "▶ PRÓXIMO PASO — Emilia, para el 27-ago"   <- el ÚNICO bien
#23807       50    contando qué pasó
#23755       22    contando qué falló
```

**Seis de siete empiezan contando qué pasó.** Una tiene 617 palabras. Y en los 14 días
anteriores, de 27 traspasos **16 nunca recibieron respuesta y CERO preguntaron qué
eran** — nadie pide aclaración, dejan de enganchar, y con eso se pierde el crédito del
resto.

### La forma

El #23802 es el molde. Arranca así, y recién después cuenta el resto:

```
▶ QUÉ HAY QUE HACER — <persona>, para el <fecha>
   <la acción, en una o dos líneas: qué, dónde, y qué debe pasar>
   Evidencia: <qué hay que traer para darlo por hecho>
   ⚠️ Trampas: <lo que ya se sabe que va a salir mal>
```

⚠️ **Y por eso esta regla se puede promover y «que se entienda» no:** un formato se
comprueba con un guion; una intención no. Ver §0, condición 3.

---

## 13. La cuota de Claude no se desperdicia

**`/clear` al cambiar de tarea**, y siempre antes de que la ventana pase de **250k** de
contexto **si la tarea ya se puede cortar ahí**. **Sonnet por defecto** (`/model`); Opus
sólo para diseñar, depurar lo difícil o revisar un PR. **Una ventana, un tiquete**: al
cerrar el tiquete, se cierra la ventana.

⚠️ **Si a los 250k la tarea SIGUE viva** —investigación, implementación o corrección que
todavía necesita lo acumulado—, no se corta con `/clear`: se usa `/compact`, que conserva
lo que hace falta. El umbral se cumple con uno de los dos, nunca perdiendo trabajo en
curso a la mitad. (Acordado con Manuel, 29-ago-2026 — antes esta sección sólo mencionaba
`/clear`, y eso chocaba con la regla de no cortar una tarea sin terminar.)

El porqué, medido sobre 49 sesiones reales: el **76%** del gasto es Claude releyendo el
contexto acumulado —no lo que escribe—, y la misma petición cuesta **ocho veces más** a
900k que a 100k. Limpiar antes de los 250k ahorra el **39%**.

**Y toda máquina del equipo lleva instalado el aviso.** Es parte de la puesta a punto,
como el acceso al repo — no es opcional ni queda a criterio de cada quien:

    python3 herramientas/claude-costo/instalar.py

Después, **`/hooks` una vez** en cada ventana ya abierta (se abre y se sale con Esc), o
cerrarlas y abrirlas. Las que ya estaban corriendo no lo cargan solas.

Sin el aviso, las tres reglas de arriba dependen de que cada quien se acuerde — y así es
como se rompieron las reglas anteriores: no por mala fe, sino porque **cada ventana carga
lo que sabe al arrancar** y nadie se entera de lo que se acordó después.

**Si no programás, leé [`GUIA-CLAUDE-PARA-TODOS.md`](GUIA-CLAUDE-PARA-TODOS.md)**: es una
página, sin nada técnico, y trae un texto que pegás en tu chat para que Claude te instale
el aviso solo — sin abrir una terminal ni saber qué es Python.

Las reglas completas, la evidencia y el detalle del aviso:
[`REGLAS-CLAUDE-COSTO.md`](REGLAS-CLAUDE-COSTO.md).

## 14. Un frente se abre y se CIERRA. Las dos mitades.

**Al mergear un PR, en el mismo movimiento:** borrar la rama, quitar el worktree, desmontar
el ambiente de prueba (§8) y liberar el carril. No al final del día ni «cuando junte varios»:
**al mergear**.

**Y si dejás de trabajar una rama sin mergearla: abrí el PR o borrá la rama.** Una rama subida
y sin PR **no la ve nadie** — no aparece en ninguna lista, nadie la revisa y nadie la mergea.
Está muerta y parece viva.

⛔ **Lo único que no se toca es lo de otra ventana viva.** Si el worktree lo tiene alguien que
late, se deja y se le avisa. `claim list` y `quien` dicen quién (las dos, más abajo). Y no se fuerza: git se niega a
borrar una rama que otro tiene sacada, y esa negativa es correcta.

### Lo que se midió el 25-ago-2026

Las dos mitades estaban falladas a la vez.

**Lo que sobraba:**

```
ramas cuyo PR ya habia cerrado ........... 27
worktrees de esas ramas .................. 10
ambientes de prueba de PR ya cerrados .... 16 de 24   (el mas viejo, de 24 dias)
```

Nada de eso rompe nada por sí solo. Lo que hace es **aparentar frentes abiertos**: al mirar en
qué anda el equipo, 27 ramas y 24 ambientes tapaban los **8 frentes reales**.

**Lo que faltaba, que es lo que cuesta plata:**

El [#21906](https://macrobase.freshdesk.com/a/tickets/21906) le llegó **vencido** al cliente.
Su arreglo estaba hecho, probado y aprobado por Soporte desde el 21-ago — pero vivía en una
rama subida **sin pull request**. Nadie la vio. Se desplegó Sally el 24-ago creyendo que eso lo
destrabaría, y no: el arreglo no estaba en `main` porque nunca se había mergeado. Se descubrió
el 25-ago, cuatro días después, buscando por qué el despliegue no lo había resuelto.

Es la segunda vez en el mes: pasó igual con `feat-T23442-ReporteDeSallyServidor`. Y al revisar
quedaban **otras dos ramas iguales**, una de **59 días**.

### ⚠️ Una rama de integración de QA NO es «hecho y nunca subido»

`que-me-espera.py` las mezcla en la misma lista, y para éstas **la respuesta es siempre
borrar**, nunca abrirles un PR. Se reconocen porque su punta es un **merge** y no tienen
ningún commit propio: son ramas armadas para probar dos PR juntos, y cuando ésos se mergean
la rama de integración ya no aporta nada.

El caso real: `qa-2026-08-20-pr25-pr26` apareció en la lista como trabajo sin subir. Su punta
es un merge de `claude/2026-08-20-decir-por-que-no-hay-nada`, cuyo PR #26 se mergeó el 20-ago
y cuya rama fuente ya no existe. Abrirle un PR habría sido pedir que se mergee algo ya
mergeado.

**Antes de abrirle el PR a una rama de esta lista, mirá si tiene commits propios.**

### Con qué se cumple, para que no dependa de acordarse

Las tres viven **fuera de este repo**, así que acá va dónde encontrarlas:

- **`limpiar-lo-terminado.py`** (repo `mb-herramientas`, en `bin/`) borra las ramas cuyo PR ya
  cerró, **salta las que tienen trabajo sin guardar y respeta las ventanas vivas**. Con
  `--hacer` las borra; sin nada, sólo muestra.
- **`que-me-espera.py`** (repo `mb-herramientas`, en `bin/`) lista las ramas con trabajo y sin
  PR bajo **«HECHO Y NUNCA SUBIDO»**. Si algo tuyo aparece ahí, o le abrís el PR o lo borrás.
- **`quien`** (repo `mb-herramientas`, en `bin/`) dice qué ventana es cada id: su título,
  cuándo escribió por última vez, en qué directorio y rama está, y qué carril tiene tomado. Es
  con lo que se averigua si un worktree es de alguien vivo.
- **`probar-rama.sh <rama> --borrar`** (repo `oci-dr`, igual que en la §8) desmonta el
  ambiente.

⚠️ **Y un hueco conocido de la herramienta:** el candado del carril bloquea
`que-me-espera.py` cuando toca una carpeta que otra ventana tiene tomada, **aunque sólo lea**.
Si esta regla se apoya en que todos lo corran, eso los va a frenar seguido. Mientras no se
arregle, quien se tope con el bloqueo puede mirar la lista de otra ventana o pedírsela.

---

## 15. Una etapa hereda del maestro, y lo lleva ESCRITO

> *"andas dejando tiquetes a diestra y siniestra y no te acordás después para qué los
> hiciste"* — Manuel, 25-ago-2026

Cuando un proyecto grande se parte en etapas, **el diseño se decide una vez, en el
maestro** — y las etapas nacen sin él. El que abre la etapa tres semanas después no ve
las decisiones: ve un título y un «qué resolver». Entonces vuelve a preguntar lo que ya
está contestado, o peor, **decide distinto**.

**Al abrir una etapa, arriba va lo que hereda del maestro**, en tres renglones:

1. **Lo que YA está decidido** y no se vuelve a discutir, con el enlace al maestro.
2. **Lo que esta etapa entrega**, y **lo que NO** — sobre todo si algo que el maestro
   promete se completa recién en otra etapa.
3. **Lo que sigue sin decidir**, si queda algo. Si no queda nada, se dice: *«no hay
   decisiones pendientes, esto es ejecución»*.

**El síntoma de que está mal hecho:** alguien abre una etapa y su primer movimiento es
preguntarle a Manuel algo que el maestro ya contesta. Pasó el 25-ago-2026 con la
[etapa 1 de la plataforma](https://macrobase.freshdesk.com/a/tickets/23664): se le
llevaron tres preguntas —dónde vive el host, cuántos, qué se muda primero— y **las dos
primeras estaban escritas en el maestro desde el principio**.

⚠️ **Y el punto 2 no es relleno.** Esa misma etapa parecía coja cada vez que alguien la
abría, porque el maestro promete DR y la etapa 1 no lo puede entregar: el DR es *por
redeploy*, y redesplegar exige el registro de imágenes (etapa 2) y el estado en Object
Storage (etapa 3). **No era un hueco del diseño: era el orden, sin escribir.** Un mes
así y alguien lo "arregla" agregándole a la etapa 1 algo que ya estaba planeado en otra.

**Qué NO cambia:** el maestro sigue siendo el que manda. Esto es una copia para que se
lea, no una segunda fuente — si difieren, gana el maestro, y la copia estaba vieja.

## Mapa de dominios (quién toca qué)

Por actividad reciente (últimos 12 meses), excluyendo a Manuel (backstop):

- **Christian** — la mayor parte del BO activo: Maedoc, Maecli, Fisico, Producto, Dic,
  BackOrder, Procesos, Reparaciones, Tiquetes… **y desde el 25-ago-2026 también Traslado,
  TrasladoEnTransito, Documento, Consultas y Parametros**, que eran de Saraí.
- **Andrea** — PT (POSTouch) siempre + lo que tocó del BO.
- **Manuel** — backstop / módulos maestros dormidos sin autor reciente.

> **Por qué Christian y no otro** (decisión de Manuel, 25-ago-2026, con esto medido enfrente):
> en los **últimos 3 meses**, sacando a Saraí, sólo Manuel y Christian tocaron esos cinco
> módulos, y **Christian es el segundo en los cinco** — 8 commits en Traslado, 7 en
> TrasladoEnTransito, 21 en Documento, 7 en Consultas y 18 en Parametros. No es una
> preferencia: es lo que dice el `git blame` reciente, que es de donde sale este mapa.
>
> ⚠️ Lo que **no** se midió: si Christian tiene capacidad, y cuántos tiquetes al mes generan
> esos cinco dominios.

**Ex-empleados — no asignarles nada:** JC (JC04mb), jmorales17, **Saraí** (`sgarciamacro` /
`sgarcia@macrobase.com.gt`, desde el 25-ago-2026).

> El mapa es **vivo**: se regenera del `git blame` reciente porque la actividad se
> mueve. No tomarlo como fijo.

## Coordinación entre ventanas — registro de trabajo en curso

Trabajamos con varias ventanas de Claude en paralelo que **no se ven entre sí**. Para no
competir por lo mismo (el incidente de oci-dr), antes de empezar una modificación se **avisa** en
un registro central que todas las ventanas consultan.

**Registro:** tabla `claude_en_curso` en `mbinvmacrobase` (la BD de pruebas por el bastión, puerto
3310 — credenciales en `app/config/parameters.yml`) — la misma BD
que todas las ventanas ya alcanzan. Una fila por **recurso** (repo, servidor o módulo):
`recurso, alcance, ventana, tiquete, inicio, ultimo_latido, estado`. Latido viejo (>30 min) =
candado abandonado (se puede tomar).

**Protocolo — toda ventana lo sigue:**
1. **Antes de tocar** un proyecto/servidor: `claim check <recurso>`. Si sale **TOMADO** por otra
   ventana con latido reciente → **no competir**, avisar a MB.
2. **Al empezar:** `claim take <recurso> <ventana> <tiquete> "<alcance>"`.
3. **Mientras trabajás:** `claim beat <recurso> <ventana>` de vez en cuando.
4. **Al terminar:** `claim release <recurso> <ventana>`.
5. `claim list` = todo lo activo.

- **`recurso`**: nombre estable — el repo (`mbinv`, `oci-dr`, `mb-deploy-panel`), el servidor
  (`app-4`, `oci-mbinv`) o `repo:modulo`.
- **`ventana`**: un id estable de tu sesión (p.ej. el UUID de la sesión).
- **Helper (Mac de MB):** `~/bin/claim` (envuelve `~/bin/claim.php`; lee la clave de
  `parameters.yml`). Cuentas sin ese helper usan el mismo SQL sobre la tabla.

**Complementa, no reemplaza:** sigue vigente el aislamiento por **worktree** (§8) y marcar el
tiquete **En Progreso** (§4). El registro es el aviso ENTRE ventanas a nivel *recurso*; el tiquete
es la señal para el equipo humano.

> Ejemplo real (8-ago-2026): dos ventanas iban a tocar "disco" — una en `mb-deploy-panel` (#23090,
> aviso de disco con proyección) y otra en `oci-dr` (#23221, alarmas OCI). El check detectó el
> solape y se paró #23221 antes de duplicar trabajo.

---

## UN proyecto activo, UN frente por proyecto

La regla de una ventana por proyecto (arriba) evita que dos sesiones se pisen el mismo
archivo, y **eso sí se está cumpliendo**. Lo que nadie limitaba es cuántos frentes abre
esa ventana adentro, que es donde se pierde el tiempo de verdad.

**Medido el 14-ago-2026**, leyendo los repos en disco:

```
ramas vivas de una sola ventana        51        59 directorios de trabajo
   mbinv                               28        mbinv            34
   Panel de rescate (DR)               17        mb-deploy-panel  19
   Panel de despliegues                 6        oci-dr            6

ramas que YA están en la base          56        su trabajo está mergeado: sobran
```

**Las dos reglas:**

1. **Un proyecto activo a la vez, hasta cerrarlo.** Que existan las otras ventanas está
   bien; lo que no debe pasar es que tres proyectos tengan trabajo el mismo día. El
   trabajo en curso es capital inmovilizado: nada rinde hasta que sale.
2. **⚠️ ORDENANZA: una rama se abre y se cierra el MISMO DÍA. Al segundo día es deuda.**

   No es una recomendación. Una rama que envejece contra la base **vuelve convertida en
   conflictos**: de las 51 vivas el 14-ago-2026, varias estaban **265 commits atrás**. Eso
   no es un bug nuevo cada día — es la misma rama desincronizándose, y se siente
   exactamente como "arreglo uno y sale otro".

   **Si el trabajo no cabe en un día, el tiquete es demasiado grande: se parte.** No se
   deja la rama esperando.

   **Cómo se verifica** — nadie tiene que declarar nada:

   ```bash
   que-me-espera.py            lista las ramas vivas con su edad y cuánto están atrás
   limpiar-lo-terminado.py     borra las que ya están mergeadas, con su worktree
   ```

   El informe de las 7:00 marca toda rama de **más de un día**. Una rama de tres días es
   una excepción que hay que poder explicar, no el estado normal.

**La excepción es la cola de tiquetes de clientes.** Va en su propia ventana, siempre
activa: son ciclos cortos, interrumpibles, y es lo que el cliente ve. Esa no cuenta como
"proyecto activo".

### Y por qué esto no es otra regla más

⚠️ **El 14-ago-2026 se encontraron TRES mecanismos de coordinación ya definidos y sin
usar:**

- el **modelo padre/hijo** (§3c): 16 asociaciones en 500 tiquetes, y las 16 mal —
  padre e hijo en el mismo grupo, que es lo que el modelo prohíbe;
- los archivos **`EN-USO-*.md`**: uno quedó colgado desde el 13-ago sin borrarse;
- la tabla **`claude_en_curso`** del `claim`, arriba: **vacía**, el mismo día en que
  cinco ventanas trabajaban cinco proyectos.

**Todo lo que hay que llenar a mano queda vacío.** Por eso esta regla viene con algo que
la mide sola y no le pide nada a nadie: `~/bin/que-me-espera.py` (repo `mb-herramientas`,
corre a las 7:00 y a las 14:00) lee los repos en disco y contesta qué se puede cerrar ya,
qué está en riesgo, cuántos frentes hay de verdad y qué tiquetes llevan una semana
quietos. Deja el informe en `~/Documents/que-me-espera.txt`.

Si una regla no se puede medir sin que alguien la declare, no se va a cumplir.

---

## Consultar las bases de PRUEBAS sin pegar la clave en el chat

Cuando le pidas a Claude que revise datos de una base de pruebas, **no le pegues la
contraseña en la conversación**: queda en el historial del chat para siempre. Usá el guion
del repo, que lee la clave de un archivo tuyo y nunca la imprime.

**Una sola vez, en tu máquina:**

```bash
mkdir -p ~/.config/mb
printf "BD_USUARIO=tu_usuario\nBD_CLAVE='tu_clave'\n" > ~/.config/mb/pruebas.env
chmod 600 ~/.config/mb/pruebas.env
```

Ese archivo no va a ningún repo. Si no sabés tu usuario o tu clave, pedíselos a Manuel —
**nunca los pongas en el chat ni en un tiquete**.

**Después, desde el repo:**

```bash
scripts/consultar-pruebas.sh --quien-soy              comprobar el acceso
scripts/consultar-pruebas.sh --bases                  qué bases hay
scripts/consultar-pruebas.sh --tablas <base>          qué tablas tiene
scripts/consultar-pruebas.sh --columnas <base> <tabla>
scripts/consultar-pruebas.sh "SELECT ..."             una consulta
```

**Deja hacer todo lo que necesites** — consultar, insertar, actualizar, borrar filas, crear
y alterar tablas. Es el servidor de pruebas y ahí se trabaja.

⚠️ **Lo único que pide confirmación son las tres cosas que no se deshacen:** borrar una base
entera (`DROP DATABASE`), y un `DELETE` o un `UPDATE` **sin `WHERE`**, que se llevan la tabla
completa. No es que no se pueda — hay que agregar `--se-lo-que-hago`:

```bash
scripts/consultar-pruebas.sh --se-lo-que-hago "DELETE FROM ..."
```

El motivo es que en el servidor de pruebas viven **copias de bases de clientes reales**, y las
cuentas del equipo tienen permisos de administrador sobre todo el servidor. El freno no está
para limitarte: está para que un descuido no borre la base de un cliente. Para todo lo demás
el guion no se mete.

**Y comprobá siempre contra qué servidor estás:** `--quien-soy` imprime la huella. La de
PRUEBAS es `wemuedtigeloq3nx`. Si sale otra, pará y avisá — el puerto 3309 es PRODUCCIÓN y
está a un dígito de distancia.

## Al crear un usuario de MySQL, nombrale el plugin viejo

**Todo usuario de base de datos que crees —a mano, en un guion o en código nuevo— lleva el
plugin escrito:**

```sql
CREATE USER 'nombre'@'%' IDENTIFIED WITH mysql_native_password BY 'la clave';
```

**No lo dejes en `IDENTIFIED BY` a secas.** Sin nombrarlo, MySQL usa el que el servidor tenga
por omisión, y eso deja de servirnos: en **MySQL 8.4 esa variable se ignora** y el servidor
siempre usa `caching_sha2_password`, **que el PHP 5.6 de los sitios no sabe hablar**. El
usuario nace y el `CREATE USER` no falla; lo que falla es el sitio del cliente cuando intenta
conectarse, semanas después y sin nada que lo relacione con el alta.

Medido el 15-ago-2026 con el PHP 5.6 real de `oci-mbinv`: un usuario con
`caching_sha2_password` responde *"The server requested authentication method unknown to the
client"*. Uno con `mysql_native_password` entra sin problema.

**Comprobá siempre con qué plugin quedó**, que es una consulta:

```sql
SELECT user, plugin FROM mysql.user WHERE user = 'nombre';
```

**Ya está cubierto** el alta por las dos vías normales —el panel de altas y la pantalla de
administración de mbinv—, así que esta regla es para todo lo demás: los usuarios que se crean
a mano en el bastión, los guiones sueltos y cualquier código nuevo que dé de alta cuentas.

⚠️ **Y esto es una prórroga, no la solución.** En **MySQL 9.x el plugin viejo desaparece del
todo**: ahí no hay parámetro que valga y los usuarios que sigan en `mysql_native_password`
quedan afuera, sin vuelta atrás. La salida de fondo es que los sitios corran PHP 7.2 o más;
recién ahí un usuario puede nacer con `caching_sha2_password`. Mientras tanto, el plugin viejo
es lo único que funciona en todo el parque. Ver el tiquete #23486.

## ⛔ Los avisos salen por Telegram. NTFY ya no se usa

Ordenanza de Manuel, **24-ago-2026**: *"tenés que anotar en todos lados que no usamos ya
NTFY, sólo Telegram"*.

**Cualquier aviso que salga sólo por ntfy es un aviso que nadie va a leer.** Y es peor que no
tener aviso, por cómo falla: ntfy contesta **HTTP 200**, el guion escribe *"(aviso enviado)"*
en su registro, y de este lado todo se ve sano. **El silencio se lee como "todo bien".**

**Por dónde sí, para una ALARMA:**

- `avisar_a_manuel()` del panel de rescate — manda por Telegram y ya trae los reintentos.
  **`entregado` es si Telegram lo aceptó**; ntfy, si sigue configurado, no cuenta.
- O directo a `api.telegram.org` desde el bastión, que lo alcanza (verificado el
  24-ago-2026), leyendo `TELEGRAM_BOT_TOKEN` y `TELEGRAM_CHAT_IDS` del `config.php` del
  panel. Así no hay que tocar `/etc/cron.d` ni pedir credenciales nuevas.

⚠️ **`bitacora_borrador.enviar` NO es un canal de alarma: manda por CORREO** (SendGrid).
Sirve para un informe periódico, no para reemplazar una alarma muda — cambiaría un canal
que nadie mira por uno que se lee tarde. Ahí es donde salen `16-mapa-infra.py` y
`monitor-discos.py`, que son informes, no alarmas.

**Un HTTP 200 no prueba entrega.** Si el aviso importa, se comprueba que **llegó**, no que
salió.

⚠️ **Y comprobarlo de verdad quiere decir mandar uno.** Medido el 24-ago-2026 cerrando esto:
`TELEGRAM_CHAT_IDS` se declara como **arreglo de PHP**, no como cadena —
`define('TELEGRAM_CHAT_IDS', [243565544]);`— y una lectura que esperaba comillas devolvía
vacío: el guion se quedaba sin destinatarios. La comprobación parcial *«token: encontrado, 46
caracteres»* había dado **luz verde falsa**, porque el token sí es cadena. Y al arreglarlo
apareció un segundo defecto en la misma línea: un `sed` se comía el primer dígito del
chat_id — **un identificador con un dígito de menos es un aviso que no llega y que no da
error**. Los dos sólo se destaparon forzando un envío real.

⚠️ **Esto es para las ALARMAS.** Un informe periódico que ya sale por correo y se comprobó
que llega —como el resumen del panel de salud del inventario, por SendGrid— **no hay que
migrarlo**: no es ntfy y no está roto. La regla prohíbe el canal muerto, no obliga a un único
camino.

⚠️ **Y no confundir con el otro Telegram.** `mbinv` y `mbinv_sf34` lo mencionan muchas veces
y **eso no es el canal de alarmas**: es funcionalidad del producto. El de las alarmas es el
del panel de rescate, con `TELEGRAM_BOT_TOKEN` y `TELEGRAM_CHAT_IDS`.

**Lo que falta, medido el 24-ago-2026** — cinco guiones de `oci-dr` que no pasan por el panel
y hacen `curl` directo a ntfy, sin otro camino:

```
VIVOS Y MUDOS (programados y corriendo en el bastion de Phoenix)
  21-vigilar-certificados.sh      avisa el 12-sep-2026 y nadie lo va a leer
  18-revisar-recursos-ociosos.py  su propio comentario dice "ntfy, que es el
                                  canal que llega" -- eso ya es falso

EN EL REPO, SIN PROGRAMAR (mudos hoy, nacen mudos si alguien los programa)
  13-detectar-drift.sh
  17-vigilar-errores.sh
  18-validar-backups-clientes.sh
```

En los servidores queda **un solo cron** pasando un topic de ntfy: el de los certificados, en
el bastión de Phoenix. `app-4`, `oci-mbinv` y `Pruebas` están limpios, y `mb-herramientas` no
tiene una sola referencia.

**El panel de rescate ya está bien** y no hay que tocarlo: `avisar_a_manuel()` es
Telegram-primero desde antes, y la insistencia de las alarmas críticas
(`reintentar-push-alarmas.php`) sale por Telegram **desde el 22-ago-2026** — commit `4ee2dac`.

⚠️ **Y al buscar un guion, cuidado con los duplicados.** `18-revisar-recursos-ociosos.py`
existe **dos veces** en `oci-dr` —en la raíz y en `webapp/scripts/`— y **la que corre es la de
`webapp/scripts/`**, que se despliega a `/var/www/panel-dr/scripts/`. El panel del DR vive en
`/var/www/panel-dr`, no en `/var/www/html`. Arreglar la copia equivocada deja el problema
intacto y el trabajo hecho: pasó el mismo día en `chatbot-mbinv`, con un cambio entero a parar
al archivo que nadie ejecuta.

> ⚠️ Ese último dato hay que decirlo porque circula al revés: *"el cron de reintentos manda
> sólo por ntfy"* fue cierto **hasta el 22-ago** y hoy no lo es. Verificado en el código:
> `reintentar-push-alarmas.php` llama a `telegram_reintento_critica()`. **Antes de repetir un
> dato de alarmas, mirá el código** — este cambió dos días antes de que dos ventanas lo
> siguieran repitiendo.

Ver el tiquete #23469.

## Tras mergear a main, refrescar el ambiente de QA

**Al mergear un PR, refrescá el ambiente donde QA va a probar. Si no, QA prueba código viejo
y rebota el tiquete** — que es de lo que se queja el #23430.

| Qué se mergeó | Qué se corre |
|---|---|
| PR de `mbinv` | `refrescar-tras-merge.sh <rama>` |
| PR de `mbinv-catalogo` | `deploy.sh` del contenedor |

Los dos guiones están versionados en `oci-dr`, junto a `probar-rama.sh` (PR #22). Y hay un
chequeo que dice qué ambiente está viejo: compara el **contenido** de `src/` contra la rama en
`origin`, no el commit —que engaña por el overlay—, y cubre también los contenedores docker
del catálogo, donde la señal es que **la imagen se creó ANTES del último commit**.

⚠️ **Esto no es teórico y el número es feo.** Medido el 13-ago-2026: **8 de 16 ambientes de
prueba corrían `src/` viejo**. Y el 17-ago, el panel de QA estaba **4 días atrás de
producción** — o sea *producción más adelante que pruebas*, al revés de lo esperable. Ahí es
donde nacen los tiquetes que QA rebota sin que nadie entienda por qué.

**Por qué va en el flujo de merge y no en un CI:** el servidor de pruebas no tiene git contra
GitHub, y los merges los hace Claude, no una automatización. Así que el único lugar donde el
refresco se puede enganchar de verdad es el paso de mergear.

⚠️ **Dos ambientes no se refrescan: se desmontan.** Si la rama ya se mergeó y se borró,
`probar-rama.sh` no sirve para refrescarlo — ese ambiente sobra y hay que darlo de baja. Es
otra decisión, no un refresco.

Propuesto el 13-ago-2026 en el #23430 y pendiente desde entonces; agregado el 24-ago con la
autorización de Manuel.
