tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1182Shell 87Rust 63Markdown 541592 Definiciones

atuq: el umbral son 4 — y mostrar el diálogo NO limpia el contador, como este documento decía

Medido en los dos lados, partiendo del perfil en 2 y con tres fases más en un arranque: el que
encuentra 2 guardado PINTA (y queda en 3); el que encuentra 3 lo sube a 4, compara 4 > 3 y abre el
diálogo (y queda en 4); el siguiente queda en 5 y vuelve a abrir el diálogo. O sea que la
comparación va DESPUÉS del incremento del propio arranque: contando desde la última corrida que
terminó bien, son cuatro arranques interrumpidos seguidos y el quinto lanzamiento ya no abre el
navegador.

`toolkit.startup.last_success` vale lo mismo en las tres fases (1789494795, el arranque de la última
corrida sana): el contador sube mientras esa marca no se mueve. Con las dos cifras juntas, un
contador que no cambió deja de ser ambiguo entre «no subió» y «subió y se limpió».

⚠ Y eso refuta una atribución que yo había escrito ayer y que estaba en tres sitios: mostrar el
diálogo NO limpia el contador (k2 lo dejó en 4, k3 en 5). Queda retirada del SDD y del comentario
del arnés. La desaparición del 16 entre las 17:12 y las 17:20 pasa a ser un hecho SIN explicación
medida, con dos candidatas escritas y ninguna dada por buena — y una de ellas es mía y fea: todas
las corridas que pintaron llevaban `--crashes 0` pinneado, que es el valor por DEFECTO, y un pref
igual al default no se persiste. El «ausente» puede ser eso y no una limpieza. Queda dicho cómo se
cierra: dejar el contador en 2 SIN pinnearlo, correr hasta que pinte y leerlo después.

Un discriminador que salió gratis: `ATUQ-EXIT=137` es «la matamos» (128+9) y `ATUQ-EXIT=0` es «se
fue sola» por el diálogo. El código de salida contesta lo que una captura no podía.

Medido por la otra sesión del frente (work/umbral-1.txt), con la predicción escrita antes de mirar.

79974643375aba67653163cbf79a6c8f9151c713 · por Sergio · (hace 1 mes) · padre cd678efd0 · ver el árbol en este commit

Delta semántico · 3

scripts/cosmic/atuq-en-imagen.py

3 archivos · +212 −133

diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 8f155c6e..4a5b7d05 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -2127,9 +2127,9 @@ sola cosa: `toolkit.startup.recent_crashes`, fijado en el `user.js` del perfil (
 | **B** 17:44 | fijado en **16** | ✗ nada en 300 s · `set_title("Open atuq in Troubleshoot Mode?")` · `ATUQ-EXIT=0` |
 
 ⚠ **Y una honestidad que cambia la fuerza del argumento:** en la corrida A el `--crashes 0` fue un
-**no-op** —el contador ya no estaba en `prefs.js`, porque Gecko lo BORRA después de mostrar el
-diálogo—, así que A sola no prueba nada: prueba tanto «lo arregló el 0» como «el perfil ya estaba
-limpio». **La que prueba la causa es B**, que lo pone de vuelta y rompe de vuelta. Una sola dirección
+**no-op** —el contador ya no estaba en `prefs.js`, y **por qué no estaba sigue sin explicación
+medida** (ver más abajo: mostrar el diálogo NO lo limpia)—, así que A sola no prueba nada: prueba
+tanto «lo arregló el 0» como «el perfil ya estaba limpio». **La que prueba la causa es B**, que lo pone de vuelta y rompe de vuelta. Una sola dirección
 habría sido una correlación con suerte.
 
 ##### ⚠⚠ La serie de `primera-pintura.json` está CONTAMINADA, y por el propio andamiaje
@@ -2160,20 +2160,51 @@ distinguía:
   incrementa» de «sólo después de uno interrumpido», que es la diferencia entre culpar al kill y
   culpar al arranque que no llegó a terminar.
 
-⚠ **Lo que sigue sin medir es el BORDE**, y se dice para que no se lea como medido: el diálogo aparece
-cuando el contador vale más que `max_resumed_crashes`, pero no está comprobado si la comparación se
-hace **antes o después** del incremento de ese mismo arranque — o sea si el diálogo sale en el
-arranque que EMPIEZA valiendo 3 (compara después de sumar) o en el que empieza valiendo 4 (compara lo
-guardado). Las dos dan «cuatro arranques interrumpidos seguidos» como respuesta práctica, y
-distinguirlas importa sólo para poder decir el número sin inventarlo. Se mide cruzando el umbral en
-vivo, que es lo que está en curso.
+##### El borde, medido en los dos lados: **3 guardado no alcanza, 4 sí**
+
+Partiendo del perfil en 2, tres fases más en un arranque (las dos primeras matadas antes de pintar):
+
+| fase | antes | después | qué ventana abrió | `ATUQ-EXIT` |
+|---|---|---|---|---|
+| k1 | 2 | 3 | **el navegador** — `set_title("atuq")`, `set_min_size(638, 120)` | 137 (la matamos) |
+| k2 | 3 | **4** | **el diálogo** — `set_title("Open atuq in Troubleshoot Mode?")`, `min_size(117, 37)` | 0 |
+| k3 | 4 | **5** | el diálogo otra vez | 0 |
+
+⇒ **la comparación es DESPUÉS de sumar**: el arranque que encuentra 3 guardado lo sube a 4, compara
+4 > 3 y abre el diálogo; el que encuentra 2 pinta. Contando desde la última corrida que terminó bien,
+eso son **cuatro arranques interrumpidos seguidos, y el quinto lanzamiento ya no abre el navegador**.
+
+Y la otra mitad del cálculo lo confirma sin ambigüedad: `toolkit.startup.last_success` vale
+**1789494795 en las tres fases** —congelado en el arranque de la última corrida sana—. El contador
+sube mientras `last_success` no se mueve; con las dos cifras juntas, un contador que no cambió deja
+de ser ambiguo entre «no subió» y «subió y se limpió».
+
+⚠⚠ **Y eso REFUTA algo que este documento afirmaba dos secciones más arriba: mostrar el diálogo NO
+limpia el contador.** k2 lo dejó en 4 y k3 en 5 — dos corridas consecutivas con diálogo y ninguna
+limpieza. Entonces la desaparición del 16 entre las 17:12 y las 17:20 **no tiene explicación
+medida**, y la atribución «Gecko lo borró al mostrarlo» se retira. Dos candidatas, las dos sin
+comprobar: que el `prefs.js` se perdiera en el kill (cuando se lo leyó con `debugfs` figuraba de 0
+bytes, y ese volcado no es fiable — es la lección del párrafo de `debugfs`), o que un arranque que
+TERMINA sí lo limpie, que es lo que hace upstream pero que acá no se puede atribuir: **todas las
+corridas que pintaron llevaban `--crashes 0` pinneado, o sea el valor por defecto, y un pref igual al
+default no se persiste**. El «ausente» de esas corridas puede ser eso.
+
+La medición que lo cierra, y que sale gratis de la serie sana: dejar el contador en 1 o 2 **sin
+pinnearlo**, correr hasta que pinte y leerlo después. Si pasa a ausente, limpia el arranque que
+termina; si sigue en 2, no limpia nada y lo del 16 fue el fichero.
+
+**Un discriminador que salió gratis y sirve para siempre:** el `ATUQ-EXIT` de una fase que matamos es
+**137** (128+9) y el de una corrida que se fue sola por el diálogo es **0**. El código de salida
+distingue «la mataron» de «se fue sola» sin mirar nada más — que es justo lo que el §6.10.terdecies
+no podía distinguir de una captura.
 
 Qué se hizo con eso, en vez de borrar el fichero:
 
 - **el contador viaja con cada medición**: `recent_crashes_antes`, `recent_crashes_despues` y
   `recent_crashes_fijado` son campos de cada corrida de `docs/state/primera-pintura.json`. Antes y
-  después porque Gecko lo limpia al mostrar el diálogo: sin el «después», la corrida siguiente parece
-  arreglarse sola y la anterior parece intermitente;
+  después porque el número cambia DENTRO de la corrida —lo escribe el arranque, no el kill— y porque
+  sin el «después» no se puede encadenar una corrida con la siguiente, que es lo que convirtió esto
+  en una serie legible;
 - **el vigía las APARTA y lo dice**: `scripts/vigia-imagen.py` saca de la tasa las corridas con el
   contador > 3 y, para las 21 anteriores al campo, informa que **no se pueden clasificar** en vez de
   contarlas como buenas. Un denominador que se marca, no que se borra.
@@ -2237,10 +2268,11 @@ con el árbol de fuentes, pero con la mitigación ya puesta. Dos consecuencias:
    `set_max_size(16332, 16332)`; el diálogo pide `set_min_size(117, 37)` y `set_max_size(348, 16332)`.
    Con mirar el `min_size` se sabe cuál abrió, sin leer el título.
 
-   Dos cosas más de esa corrida: `recent_crashes_despues` salió **ausente** —la corrida que pinta
-   LIMPIA el contador, que es el mecanismo visto desde el otro lado y explica el «se arregla sola»
-   sin ninguna hipótesis— y el `xulstore` quedó igual: **una corrida sana no ensucia lo que el perfil
-   recuerda**.
+   Dos cosas más de esa corrida: el `xulstore` quedó igual ⇒ **una corrida sana no ensucia lo que el
+   perfil recuerda**; y `recent_crashes_despues` salió **ausente**, que en su momento se leyó como
+   «la corrida que pinta limpia el contador» y **hay que no leerlo así** — esa corrida llevaba
+   `--crashes 0` pinneado, o sea el valor por DEFECTO, y un pref igual al default no se persiste. El
+   «ausente» puede ser eso y no una limpieza.
 
 ### 6.11 Y ahora la pregunta que faltaba: ¿REPRODUCE? (2026-09-07)
 
diff --git a/docs/state/primera-pintura.json b/docs/state/primera-pintura.json
index 7f53c290..bd299302 100644
--- a/docs/state/primera-pintura.json
+++ b/docs/state/primera-pintura.json
@@ -1,112 +1,5 @@
 {
   "corridas": [
-    {
-      "medido": "2026-09-14T16:17:13Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": null,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1
-    },
-    {
-      "medido": "2026-09-14T16:26:43Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": 184,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1,
-      "pantalla": "desconocida (una sola captura)"
-    },
-    {
-      "medido": "2026-09-14T16:39:48Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": null,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1
-    },
-    {
-      "medido": "2026-09-14T16:48:45Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": 199,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1,
-      "pantalla": "desconocida (una sola captura)"
-    },
-    {
-      "medido": "2026-09-14T17:03:19Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": null,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1
-    },
-    {
-      "medido": "2026-09-14T17:18:52Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": null,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1
-    },
-    {
-      "medido": "2026-09-14T17:34:16Z",
-      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
-      "segundos_primera_pintura": null,
-      "presupuesto_s": 420,
-      "ventana_s": 480,
-      "cadencia_s": 30,
-      "lanzamiento": "perfil del usuario",
-      "video": "vga+virtio-gpu (dos DRM)",
-      "mem_mb": 2560,
-      "vcpus": 2,
-      "acel": "tcg",
-      "guion": "scripts/cosmic/atuq-en-imagen.py",
-      "pantallas_fotografiadas": 1
-    },
     {
       "medido": "2026-09-14T17:51:50Z",
       "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
@@ -475,6 +368,139 @@
       "recent_crashes_fijado": 16,
       "recent_crashes_antes": null,
       "recent_crashes_despues": 16
+    },
+    {
+      "medido": "2026-09-15T17:56:31Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": 195,
+      "pantalla": "gpu0",
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 300,
+      "cadencia_s": 60,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": 0,
+      "recent_crashes_antes": 16,
+      "recent_crashes_despues": null
+    },
+    {
+      "medido": "2026-09-15T18:04:44Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": null,
+      "pantalla": null,
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 60,
+      "cadencia_s": 30,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": null,
+      "recent_crashes_antes": null,
+      "recent_crashes_despues": null
+    },
+    {
+      "medido": "2026-09-15T18:06:05Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": null,
+      "pantalla": null,
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 60,
+      "cadencia_s": 30,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": null,
+      "recent_crashes_antes": null,
+      "recent_crashes_despues": 1
+    },
+    {
+      "medido": "2026-09-15T18:07:24Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": null,
+      "pantalla": null,
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 60,
+      "cadencia_s": 30,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": null,
+      "recent_crashes_antes": 1,
+      "recent_crashes_despues": 2
+    },
+    {
+      "medido": "2026-09-15T18:13:42Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": null,
+      "pantalla": null,
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 60,
+      "cadencia_s": 30,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": null,
+      "recent_crashes_antes": 2,
+      "recent_crashes_despues": 3
+    },
+    {
+      "medido": "2026-09-15T18:15:02Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": null,
+      "pantalla": null,
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 60,
+      "cadencia_s": 30,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": null,
+      "recent_crashes_antes": 3,
+      "recent_crashes_despues": 4
+    },
+    {
+      "medido": "2026-09-15T18:16:18Z",
+      "imagen": "/mnt/cosecha/takana-cosmic-qemu.img",
+      "segundos_primera_pintura": null,
+      "pantalla": null,
+      "pantallas_fotografiadas": 1,
+      "presupuesto_s": 420,
+      "ventana_s": 60,
+      "cadencia_s": 30,
+      "lanzamiento": "perfil del usuario",
+      "video": "virtio-gpu solo (-vga none)",
+      "mem_mb": 2560,
+      "vcpus": 2,
+      "acel": "tcg",
+      "guion": "scripts/cosmic/atuq-en-imagen.py",
+      "recent_crashes_fijado": null,
+      "recent_crashes_antes": 4,
+      "recent_crashes_despues": 5
     }
   ]
 }
diff --git a/scripts/cosmic/atuq-en-imagen.py b/scripts/cosmic/atuq-en-imagen.py
index 07c46191..b176d9cd 100755
--- a/scripts/cosmic/atuq-en-imagen.py
+++ b/scripts/cosmic/atuq-en-imagen.py
@@ -255,13 +255,21 @@ def main():
                          "que es la única forma de subirle el nivel de log sin recompilar")
     ap.add_argument("--crashes", type=int, metavar="N",
                     help="fija `toolkit.startup.recent_crashes` del perfil del usuario antes de "
-                         "lanzar: 0 lo limpia, >3 reproduce el diálogo de Modo de resolución de "
-                         "problemas. Sólo tiene efecto con --as-user")
+                         "lanzar: 0 lo limpia, 4 o más reproduce el diálogo de Modo de resolución "
+                         "de problemas (medido: con 3 guardado todavía pinta, porque el arranque "
+                         "suma ANTES de comparar). Sólo tiene efecto con --as-user. ⚠ 0 es el valor "
+                         "por DEFECTO y un pref igual al default no se persiste: si lo que querés "
+                         "medir es quién limpia el contador, NO lo pinnees")
     ap.add_argument("--xulstore", metavar="ANCHOxALTO",
                     help="agrega una SEGUNDA fase en el mismo arranque con `xulstore.json` sembrado "
                          "(p. ej. 1152x720): la ventana arranca con un tamaño YA PERSISTIDO. Es el "
                          "control del 117x70 — si con semilla sale grande, lo que falla es que nadie "
                          "le da tamaño en el primer arranque")
+    ap.add_argument("--repeat", type=int, metavar="N",
+                    help="corre N fases IDÉNTICAS en el mismo arranque. Con `--wait` corto (60 s, o "
+                         "sea antes de que pinte) mide el contador de caídas: cada fase lee "
+                         "`recent_crashes` ANTES de lanzar, así que la serie 0,1,2… dice cuánto suma "
+                         "un arranque interrumpido — y si no sube, el envenenamiento vino de otro lado")
     ap.add_argument("--out", default=os.path.join(RAIZ, "work/atuq-imagen"))
     a = ap.parse_args()
 
@@ -400,8 +408,12 @@ def main():
                     # que CADA corrida fallida puede sumar uno: el instrumento degrada el sujeto y
                     # la tasa medida deja de ser del producto. Sin este número impreso, una corrida
                     # no se puede interpretar.
-                    volcado = ser.cmd("grep -E 'toolkit.startup.(recent_crashes|max_resumed)|"
-                                      "browser.sessionstore.max_resumed' %s/prefs.js" % d, 60)
+                    # ⚠ `last_success` VA EN EL MISMO VOLCADO. Es la otra mitad del cálculo: sin
+                    # él, un `recent_crashes` que no se movió es ambiguo entre «no subió» y «subió y
+                    # lo limpió el gancho de cierre», que son conclusiones opuestas sobre la misma
+                    # cifra.
+                    volcado = ser.cmd("grep -E 'toolkit.startup.(recent_crashes|max_resumed|"
+                                      "last_success)|browser.sessionstore.max_resumed' %s/prefs.js" % d, 60)
                     print(volcado.strip())
                     estado["recent_crashes_antes"] = leer_contador_de_caidas(volcado)
                 else:
@@ -455,8 +467,12 @@ def main():
                 línea de orden del shell del serial y ahí se mata la sesión entera (medido en el hub,
                 sale 144 y lo que sigue no corre).
                 """
+                # ⚠ `-9` A PROPÓSITO. Un `kill` a secas le da a Gecko su apagado ordenado, y un
+                # apagado ordenado es EXACTAMENTE la variable bajo prueba: lo que hace este arnés al
+                # terminar es matar la VM entera, que no avisa. Con `-KILL` la fase siguiente
+                # encuentra el mismo estado que encontraría tras una corrida matada de verdad.
                 ser.cmd("for p in /proc/[0-9]*; do grep -q atuq $p/comm 2>/dev/null && "
-                        "kill ${p#/proc/}; done; true", 60)
+                        "kill -9 ${p#/proc/}; done; true", 60)
                 time.sleep(8)
                 salida = ser.cmd("for c in /proc/[0-9]*/comm; do cat $c 2>/dev/null; done | grep -c atuq")
                 # El serial hace ECO de la orden, así que la respuesta es la ÚLTIMA línea que sea un
@@ -477,10 +493,14 @@ def main():
             fases = [("base", None)]
             if a.xulstore:
                 fases.append(("semilla-%s" % a.xulstore, a.xulstore))
+            if a.repeat:
+                # N fases iguales: lo que varía entre ellas no es la configuración sino el ESTADO que
+                # deja la anterior en el perfil, que es justo lo que se quiere medir.
+                fases = [("k%d" % i, None) for i in range(1, a.repeat + 1)]
 
             for fase, semilla in fases:
                 print("\n===== FASE «%s» =====" % fase, flush=True)
-                if semilla:
+                if fase != fases[0][0]:
                     matar_al_navegador()
                 perfil_d = perfil_de_la_fase(semilla)
                 log_atuq = "/var/log/cosmic/atuq-%s.log" % fase
@@ -545,10 +565,11 @@ def main():
                         rc[0] = 1
 
                 if perfil_d and a.as_user:
-                    # ⚠ EL ESTADO FINAL ES PARTE DE LA PRUEBA. Gecko BORRA el contador después de
-                    # mostrar el diálogo, así que un perfil envenenado se ve limpio en cuanto se
-                    # lo mira DESPUÉS: sin este segundo volcado, la corrida siguiente parece
-                    # «arreglada sola» y la anterior parece intermitente. Se lee con el navegador
+                    # ⚠ EL ESTADO FINAL ES PARTE DE LA PRUEBA: el número cambia DENTRO de la
+                    # corrida —lo escribe el ARRANQUE al encontrar el anterior sin terminar, no el
+                    # kill— así que sin el «después» no se puede encadenar una corrida con la
+                    # siguiente, que es lo que convierte esto en una serie legible (medido:
+                    # 2 → 3 → 4 en tres fases de un mismo arranque). Se lee con el navegador
                     # todavía vivo, o sea que refleja el último volcado de prefs de Gecko.
                     volcado = ser.cmd("grep -E 'toolkit.startup.recent_crashes' %s/prefs.js || "
                                       "echo '(el pref no está: arranque sin caídas pendientes)'"