agents-city 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "city",
11
11
  "source": "./plugin",
12
- "version": "0.3.0",
12
+ "version": "0.4.0",
13
13
  "description": "Operate one autonomous city seat, its domain, role, repo support agents, goal, recognised skills and explicit roads to other cities."
14
14
  }
15
15
  ]
package/README.es.md CHANGED
@@ -556,6 +556,27 @@ se escriben en el registro de actividad. El token de observador rota con el hub,
556
556
  sólo acepta un origen de este ordenador y es de sólo lectura: el navegador no
557
557
  puede dirigir el comité. `Ctrl-c` detiene el Hall.
558
558
 
559
+ **Demos**, en el rail, reproduce un comité entero sin montar nada: una historia
560
+ por dominio de trabajo — un estudio, una clínica, un despacho — con play, pausa,
561
+ repetir y velocidad. Lo que reproduce son *grabaciones*: `demo/graba.py` ejecuta
562
+ cada historia sobre el bus local real, a través de la máquina de estados real del
563
+ comité, y guarda el flujo exacto de eventos que vio un espectador; el Hall los
564
+ reproduce con el mismo renderizador que usa el rail en vivo. Lo dice en pantalla,
565
+ porque una demo que finge estar en vivo es justo la que este producto no debe
566
+ publicar. Para lanzar una en vivo desde terminal: `agents-city demo --domain
567
+ software`.
568
+
569
+ Regenera las grabaciones tras editar `demo/stories.py`:
570
+
571
+ ```bash
572
+ demo/graba.py # todas las historias
573
+ demo/graba.py medicina # sólo una
574
+ ```
575
+
576
+ La suite de demo falla cuando una grabación deja de corresponderse con la
577
+ historia que dice ser, así que una obsoleta es un build en rojo y no un navegador
578
+ reproduciendo en silencio el comité del mes pasado.
579
+
559
580
  Bajo la marca hay dos botones: **día/noche** e **ES/EN**. El Hall habla español e
560
581
  inglés, arranca en el idioma del navegador y recuerda la elección explícita. Las
561
582
  traducciones se indexan por la frase en inglés, así que lo que aún no esté
@@ -1843,9 +1864,17 @@ CITY_SETTLE=0 CITY_STAGGER=0 agents-city seat --city producto
1843
1864
  Lee `.git/config` y `HEAD` directamente en vez de lanzar `git` una vez por
1844
1865
  repositorio, así que un escaneo completo termina mientras alguien mira la
1845
1866
  pantalla, y funciona allí donde funcione Python: macOS, Linux y Windows por
1846
- igual. La guía y el Hall buscan sobre este índice; `plugin/scripts/find-repos.sh`
1847
- es un envoltorio fino para quien lo llama desde shell, y sólo imprime la mitad
1848
- git.
1867
+ igual. Este índice es el que usa la **terminal**: `seat --repos`, y el lanzador
1868
+ resolviendo el `repo@rama` de una ficha a una ruta de esta máquina.
1869
+ `plugin/scripts/find-repos.sh` es un envoltorio fino para quien lo llama desde
1870
+ shell, y sólo imprime la mitad git.
1871
+
1872
+ El **Hall** no lo usa, y a propósito: allí, elegir sobre qué trabaja un agente es
1873
+ un explorador de carpetas. Recorres tu disco y coges lo que quieras — un
1874
+ repositorio, un worktree, una carpeta de documentos, un fichero exacto — y no se
1875
+ te adelanta nada ni se filtra nada. Una lista adivinada sólo puede ofrecerte lo
1876
+ que sabía buscar, y es una lista más que leer antes de hacer aquello a lo que
1877
+ venías.
1849
1878
 
1850
1879
  El índice se cachea un día en `$XDG_CACHE_HOME/agents-city/lugares.tsv` o
1851
1880
  `~/.cache/agents-city/lugares.tsv`. Tanto el formulario de casas del Hall como el
@@ -2099,10 +2128,30 @@ agents-city seat --repos
2099
2128
  ```
2100
2129
 
2101
2130
  La detección exige `.git` (directorio o fichero de worktree) y un remote `origin`.
2102
- Si acabas de cambiar las raíces, usa **Buscar otra vez** en el formulario de
2103
- casas, o ejecuta `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` puede estar filtrando el repo;
2131
+ Si acabas de cambiar las raíces, ejecuta `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` puede estar filtrando el repo;
2104
2132
  déjala vacía para indexar todos los remotes.
2105
2133
 
2134
+ ### Desinstalarlo del todo
2135
+
2136
+ ```bash
2137
+ agents-city uninstall # dice exactamente qué se iría; no borra nada
2138
+ agents-city uninstall --yes # lo hace
2139
+ agents-city uninstall --keep-cities --yes # desconecta la máquina, conserva las ciudades
2140
+ agents-city uninstall --npm --yes # y quita también el paquete global
2141
+ ```
2142
+
2143
+ Cierra todas las sesiones, halls y mapas que el producto arrancó, quita los
2144
+ accesos directos del escritorio y el registro del plugin de Claude, y borra
2145
+ `~/.agents-city` (tus ciudades, su estado y sus copias), `~/.config/agents-city`,
2146
+ `~/.cache/agents-city` y `~/.claude/channels/city-bus` — más el token del bus en
2147
+ el Keychain de macOS.
2148
+
2149
+ Nunca toca tus repositorios, tus worktrees ni tus carpetas de documentos. La casa
2150
+ de un agente guarda *enlaces* a eso, y lo único que se va es el enlace.
2151
+
2152
+ `reset` responde a la otra pregunta: vacía una ciudad, deja una copia y conserva
2153
+ la instalación, para cuando piensas seguir usándolo.
2154
+
2106
2155
  ### GitHub no muestra repos privados u organizaciones
2107
2156
 
2108
2157
  ```bash
package/README.md CHANGED
@@ -550,6 +550,26 @@ spectator token rotates with the hub, accepts only an origin on this computer,
550
550
  and is read-only: the browser cannot direct the committee. `Ctrl-c` stops the
551
551
  Hall.
552
552
 
553
+ **Demos** in the rail plays a whole committee without setting anything up: one
554
+ story per work domain — a studio, a clinic, a law firm — with play, pause,
555
+ replay and speed. What it plays are *recordings*: `demo/graba.py` runs each story
556
+ over the real local bus, through the real committee state machine, and keeps the
557
+ exact event stream a spectator saw; the Hall replays those events through the
558
+ same renderer the live rail uses. It says so on screen, because a demo that
559
+ pretends to be live is the one kind this product must not ship. To run one live
560
+ in a terminal instead: `agents-city demo --domain software`.
561
+
562
+ Regenerate the recordings after editing `demo/stories.py`:
563
+
564
+ ```bash
565
+ demo/graba.py # every story
566
+ demo/graba.py medicina # just one
567
+ ```
568
+
569
+ The demo suite fails when a recording no longer matches the story it claims to
570
+ be, so a stale one is a red build rather than a browser quietly playing last
571
+ month's committee.
572
+
553
573
  Two buttons sit under the brand: **day/night**, and **ES/EN**. The Hall speaks
554
574
  Spanish and English, starting in the browser's own language and remembering an
555
575
  explicit choice. Translations are keyed by the English sentence, so anything not
@@ -1819,9 +1839,17 @@ CITY_SETTLE=0 CITY_STAGGER=0 agents-city seat --city product
1819
1839
 
1820
1840
  It reads `.git/config` and `HEAD` directly rather than running `git` once per
1821
1841
  repository, so a full scan finishes while somebody is looking at the screen, and
1822
- it runs anywhere Python does — macOS, Linux and Windows alike. The guide and the
1823
- Hall search this index; `plugin/scripts/find-repos.sh` is a thin shim over it for
1824
- shell callers, printing the git half only.
1842
+ it runs anywhere Python does — macOS, Linux and Windows alike. This index is what
1843
+ the **terminal** uses: `seat --repos`, and the launcher resolving a card's
1844
+ `repo@branch` to a path on this machine. `plugin/scripts/find-repos.sh` is a thin
1845
+ shim over it for shell callers, printing the git half only.
1846
+
1847
+ The **Hall** does not use it, and deliberately: choosing what an agent works on
1848
+ is a folder picker there. You walk your disk and take what you want — a
1849
+ repository, a worktree, a folder of documents, one exact file — and nothing is
1850
+ offered in advance or filtered out. A guessed list can only ever offer what it
1851
+ knew how to look for, and it is one more list to read before you can do the thing
1852
+ you came to do.
1825
1853
 
1826
1854
  The index is cached for one day at `$XDG_CACHE_HOME/agents-city/lugares.tsv` or
1827
1855
  `~/.cache/agents-city/lugares.tsv`. Both the Hall's house form and the seat offer
@@ -2065,10 +2093,30 @@ agents-city seat --repos
2065
2093
  ```
2066
2094
 
2067
2095
  Discovery requires `.git` (a directory or worktree file) and an `origin` remote.
2068
- If roots just changed, use the house form's **Search again**, or run
2069
- `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` may be filtering the repo;
2096
+ If roots just changed, run `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` may be filtering the repo;
2070
2097
  leave it empty to index every remote.
2071
2098
 
2099
+ ### Removing it completely
2100
+
2101
+ ```bash
2102
+ agents-city uninstall # says exactly what would go; removes nothing
2103
+ agents-city uninstall --yes # goes through with it
2104
+ agents-city uninstall --keep-cities --yes # unwire the machine, keep the cities
2105
+ agents-city uninstall --npm --yes # and remove the global package too
2106
+ ```
2107
+
2108
+ It closes every session, hall and map the product started, removes the desktop
2109
+ shortcuts and the Claude plugin registration, and deletes `~/.agents-city` (your
2110
+ cities, their state and their backups), `~/.config/agents-city`,
2111
+ `~/.cache/agents-city` and `~/.claude/channels/city-bus` — plus the bus token in
2112
+ the macOS Keychain.
2113
+
2114
+ It never touches your repositories, your worktrees or your document folders. An
2115
+ agent's home holds *links* to those, and a link is all that goes.
2116
+
2117
+ `reset` is the other question: it empties one city, keeps a backup and leaves the
2118
+ install in place, for when you mean to keep using it.
2119
+
2072
2120
  ### GitHub does not show private repos or organisations
2073
2121
 
2074
2122
  ```bash
@@ -30,6 +30,10 @@ const ORDENES = {
30
30
  benchmark: { que: ['bin/benchmark'], di: 'offline stress/governance or opt-in live runtime latency' },
31
31
  logs: { que: ['bin/logs'], di: 'read or follow visible activity and operational diagnostics' },
32
32
  reset: { que: ['bin/reset'], di: 'reset one city to onboarding, recoverably' },
33
+ uninstall: {
34
+ que: ['bin/uninstall'],
35
+ di: 'remove everything this wrote on this machine (previews first)',
36
+ },
33
37
  skills: { que: ['bin/skills'], di: 'recognise the skills installed in each repo' },
34
38
  city: { que: ['bin/city'], di: 'draw your city' },
35
39
  shortcut: { que: ['bin/shortcut'], di: 'put a city on your desktop: icon, name, double-click' },
package/bin/hall.html CHANGED
@@ -31,6 +31,7 @@
31
31
  --hud:"Chakra Petch",system-ui,sans-serif; --texto:"Newsreader",Georgia,serif;
32
32
  --mono:"IBM Plex Mono",ui-monospace,monospace;
33
33
  --sombra:0 18px 40px -30px #1d222945;
34
+ --velo:#1d222959; --sombra-alta:0 24px 60px -20px #1d222966;
34
35
 
35
36
  /* Night, declared once: the city's own light, an isometric map at 3am. The
36
37
  two rules below only MAP these; a palette written twice drifts twice. */
@@ -41,6 +42,7 @@
41
42
  --noche-panel:#101720; --noche-burbuja:#171f2a; --noche-burbuja-mia:#27291f;
42
43
  --noche-borde-acento:#65552d; --noche-sobre-lampara:#1a1408;
43
44
  --noche-lampara-viva:#ffd469;
45
+ --noche-velo:#05080d99; --noche-sombra-alta:0 24px 60px -18px #000000aa;
44
46
  --noche-chip-luz:36% 30%; --noche-chip-tinta:#f3eee4;
45
47
  --noche-tono-palabra:#aa93ff; --noche-tono-evidencia:#61ace9;
46
48
  --noche-tono-verificacion:#50ccb1;
@@ -60,6 +62,7 @@
60
62
  --tono-palabra:var(--noche-tono-palabra); --tono-evidencia:var(--noche-tono-evidencia);
61
63
  --tono-verificacion:var(--noche-tono-verificacion);
62
64
  --sombra:var(--noche-sombra);
65
+ --velo:var(--noche-velo); --sombra-alta:var(--noche-sombra-alta);
63
66
  }
64
67
  /* No `prefers-color-scheme` rule on purpose: daylight is THE default, not a
65
68
  default that a machine set to dark quietly overrides. Night is a choice made
@@ -349,15 +352,97 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
349
352
  font:11px var(--mono);padding:5px 10px;border-radius:999px;cursor:pointer}
350
353
  .bvChip:hover{border-color:var(--lampara)}
351
354
  .bvChip.elegida{border-color:var(--lampara);background:var(--burbuja-mia);color:var(--tinta)}
355
+ .dlgFondo{position:fixed;inset:0;z-index:60;display:flex;align-items:center;
356
+ justify-content:center;padding:24px;background:var(--velo);backdrop-filter:blur(2px)}
357
+ .dlg{background:var(--suelo2);border:1px solid var(--linea2);border-radius:8px;
358
+ box-shadow:var(--sombra-alta);padding:22px 24px;width:100%;max-width:460px;
359
+ max-height:86vh;overflow:auto}
360
+ .dlg h2{font:600 17px var(--hud);color:var(--tinta);margin:0 0 10px}
361
+ .dlgProsa{font:13px/1.55 var(--hud);color:var(--tinta2);margin:0 0 10px}
362
+ .dlg .campo{margin-bottom:12px}
363
+ .dlg .campo label{display:block;font:600 10px var(--hud);letter-spacing:.14em;
364
+ text-transform:uppercase;color:var(--tinta3);margin-bottom:5px}
365
+ .dlg .campo input,.dlg .campo select{width:100%;background:var(--suelo);
366
+ border:1px solid var(--linea2);border-radius:4px;padding:8px 10px;
367
+ font-family:var(--hud);font-size:13px;color:var(--tinta)}
368
+ .dlg .campo input:focus,.dlg .campo select:focus{outline:none;border-color:var(--lampara)}
369
+ .dlgBotones{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
370
+ .dlgBotones .bt[disabled]{opacity:.45;cursor:not-allowed}
352
371
  .bvClase{font:600 9px var(--mono);letter-spacing:.08em;text-transform:uppercase;
353
372
  opacity:.55;font-style:normal;margin-right:6px}
354
373
  .bvChip.cl-docs .bvClase{color:var(--verde);opacity:.85}
355
374
  .bvChip.cl-worktree .bvClase{color:var(--lampara);opacity:.85}
356
- .bvBuscador{display:flex;gap:8px;align-items:center;margin:4px 0 10px}
357
- .bvBuscador input{flex:1;background:var(--suelo);border:1px solid var(--linea2);
358
- border-radius:4px;padding:8px 10px;font-family:var(--hud);font-size:13px;color:var(--tinta)}
359
- .bvBuscador input:focus{outline:none;border-color:var(--lampara)}
360
375
  .bt.bvMini{padding:7px 12px;font-size:11px;white-space:nowrap}
376
+ .exp{border:1px solid var(--linea2);border-radius:5px;background:var(--suelo);
377
+ padding:8px;margin-bottom:10px}
378
+ .expAtajos{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:7px}
379
+ .expAtajo{border:1px solid var(--linea);background:var(--suelo3);color:var(--tinta2);
380
+ font:11px var(--hud);padding:3px 9px;border-radius:999px;cursor:pointer}
381
+ .expAtajo:hover,.expAtajo.aqui{border-color:var(--lampara);color:var(--lampara)}
382
+ .expBarra{display:flex;gap:7px;align-items:center;margin-bottom:7px}
383
+ .expSubir{border:1px solid var(--linea2);background:var(--suelo3);color:var(--tinta2);
384
+ font:12px var(--mono);width:26px;height:26px;border-radius:4px;cursor:pointer;flex:none}
385
+ .expSubir:hover:not([disabled]){border-color:var(--lampara);color:var(--lampara)}
386
+ .expSubir[disabled]{opacity:.35;cursor:default}
387
+ .expMigas{display:flex;flex-wrap:wrap;align-items:center;gap:1px;overflow:hidden;
388
+ font:11px var(--mono);color:var(--tinta3)}
389
+ .expMigas i{opacity:.4;font-style:normal}
390
+ .expMiga{border:0;background:none;color:var(--tinta2);font:11px var(--mono);
391
+ padding:1px 3px;cursor:pointer;border-radius:3px}
392
+ .expMiga:hover{color:var(--lampara);background:var(--suelo3)}
393
+ .expLista{max-height:240px;overflow:auto;border-top:1px solid var(--linea);
394
+ border-bottom:1px solid var(--linea)}
395
+ .expFila{display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--linea)}
396
+ .expFila:last-child{border-bottom:0}
397
+ .expFila.puesta{background:var(--burbuja-mia)}
398
+ .expNombre{flex:1;display:flex;align-items:center;gap:7px;border:0;background:none;
399
+ text-align:left;font:12px var(--hud);color:var(--tinta);padding:6px 8px;cursor:pointer;
400
+ overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
401
+ .expNombre[disabled]{cursor:default;color:var(--tinta2)}
402
+ .expNombre:hover:not([disabled]){color:var(--lampara)}
403
+ .expIcono{font-style:normal;color:var(--tinta3);font-size:10px;width:8px;flex:none}
404
+ .expFila.fich .expNombre{font-family:var(--mono);font-size:11px}
405
+ .expMarca{font:600 9px var(--mono);letter-spacing:.06em;text-transform:uppercase;
406
+ color:var(--lampara);opacity:.8;font-style:normal;flex:none}
407
+ .expMas{border:0;background:none;color:var(--tinta3);font:13px var(--mono);
408
+ width:30px;height:28px;cursor:pointer;flex:none}
409
+ .expMas:hover{color:var(--lampara)}
410
+ .expFila.puesta .expMas{color:var(--lampara)}
411
+ .expTomaAqui{margin-top:8px;width:100%}
412
+ .expElegidas{margin:8px 0 4px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
413
+ .expElegidas .bvEtiqueta{width:100%;margin:0 0 2px}
414
+ .expQuita{font-style:normal;margin-left:7px;opacity:.6}
415
+ .pista.malo{color:var(--carta)}
416
+ .demoRejilla{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
417
+ gap:12px;margin:18px 0 14px}
418
+ .demoCarta{display:flex;flex-direction:column;gap:5px;text-align:left;cursor:pointer;
419
+ background:var(--suelo2);border:1px solid var(--linea2);border-radius:7px;
420
+ padding:15px 16px;box-shadow:var(--sombra)}
421
+ .demoCarta:hover{border-color:var(--lampara)}
422
+ .demoDominio{font:600 10px var(--hud);letter-spacing:.14em;text-transform:uppercase;
423
+ color:var(--lampara)}
424
+ .demoCarta b{font:600 15px/1.35 var(--hud);color:var(--tinta)}
425
+ .demoCiudad{font:12px var(--texto);color:var(--tinta2)}
426
+ .demoReparto{font:10px var(--mono);color:var(--tinta3);overflow:hidden;
427
+ white-space:nowrap;text-overflow:ellipsis}
428
+ .demoPlay{margin-top:6px;font:600 11px var(--mono);color:var(--tinta2)}
429
+ .demoCarta:hover .demoPlay{color:var(--lampara)}
430
+ .demoCab{display:flex;gap:14px;align-items:flex-start;margin-bottom:12px}
431
+ .demoCab h2{font:600 17px var(--hud);color:var(--tinta);margin:2px 0 0}
432
+ .demoMandos{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:9px}
433
+ .demoVel{display:inline-flex;border:1px solid var(--linea2);border-radius:999px;
434
+ overflow:hidden;background:var(--suelo2)}
435
+ .demoVelBoton{border:0;background:none;color:var(--tinta3);font:11px var(--mono);
436
+ padding:5px 10px;cursor:pointer}
437
+ .demoVelBoton:hover{color:var(--tinta)}
438
+ .demoVelBoton.aqui{background:var(--burbuja-mia);color:var(--lampara)}
439
+ .demoCuenta{font:11px var(--mono);color:var(--tinta3);margin-left:auto}
440
+ .demoBarra{height:3px;background:var(--linea);border-radius:2px;overflow:hidden;
441
+ margin-bottom:12px}
442
+ .demoBarra i{display:block;height:100%;background:var(--lampara);
443
+ transition:width .35s ease}
444
+ .demoPlayer .liveLista{max-height:calc(100vh - 300px);border:1px solid var(--linea);
445
+ border-radius:7px;background:var(--panel);padding:16px}
361
446
  .bvMotor{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:6px}
362
447
  .bvMotor .campo{margin:0}
363
448
  @media (max-width:640px){.bvMotor{grid-template-columns:1fr}}
@@ -456,7 +541,9 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
456
541
  stroke-linecap:round;stroke-linejoin:round}
457
542
  .liveContextText{min-width:0;color:var(--tinta3);font:10px/1.35 var(--hud)}
458
543
  .liveContextText b{color:var(--tinta2);font-weight:600}
459
- #liveEvents{list-style:none;margin:0;padding:16px 16px 26px;overflow-y:auto;flex:1;
544
+ /* The demo player replays into a list of its own, and must look identical:
545
+ it is the same renderer, so it gets the same rules rather than a copy. */
546
+ #liveEvents,.liveLista{list-style:none;margin:0;padding:16px 16px 26px;overflow-y:auto;flex:1;
460
547
  display:flex;flex-direction:column;gap:13px;scrollbar-color:var(--linea2) transparent}
461
548
  .liveEmpty{margin:auto;padding:30px 18px;text-align:center;color:var(--tinta3);font-size:13px;line-height:1.55}
462
549
  .liveTurn{--tono:var(--tinta3);display:grid;grid-template-columns:31px minmax(0,1fr);
package/bin/navegador.mjs CHANGED
@@ -292,23 +292,32 @@ async function main() {
292
292
  await new Promise(r => setTimeout(r, 900));
293
293
  (await espera('[data-bv="nuevo"]'))?.click();
294
294
  await new Promise(r => setTimeout(r, 400));
295
- const busca = await espera('#bvBusca');
296
295
  const motores = ['#bvRuntime', '#bvModelo', '#bvEsfuerzo'].map(s => !!document.querySelector(s));
297
296
  const opcionesMotor = [...(document.querySelector('#bvModelo')?.options ?? [])].map(o => o.value);
298
- const antes = document.querySelectorAll('.bvChip').length;
299
- let despues = antes;
300
- if (busca) {
301
- busca.value = 'zzz-no-existe-nada';
302
- busca.dispatchEvent(new Event('input', { bubbles: true }));
303
- await new Promise(r => setTimeout(r, 400));
304
- despues = document.querySelectorAll('.bvChip').length;
297
+ // The picker walks: it must list a folder, and going into one must change
298
+ // where it says you are. Nothing is offered before you ask for it.
299
+ await espera('.exp .expLista .expFila');
300
+ const filas = document.querySelectorAll('.expFila').length;
301
+ // Picking a row records it — no scan happened, nothing was suggested.
302
+ document.querySelector('.expFila .expMas')?.click();
303
+ await new Promise(r => setTimeout(r, 300));
304
+ const elegidas = document.querySelectorAll('.expElegidas .bvChip').length;
305
+ // And going into a folder must change where it says you are.
306
+ const dondeAntes = document.querySelector('.expMigas')?.textContent ?? '';
307
+ [...document.querySelectorAll('.expFila.dir .expNombre')][0]?.click();
308
+ for (let i = 0; i < 40; i++) {
309
+ if ((document.querySelector('.expMigas')?.textContent ?? '') !== dondeAntes) break;
310
+ await new Promise(r => setTimeout(r, 250));
305
311
  }
306
- return { hayBusca: !!busca, motores, opcionesMotor, antes, despues,
307
- rebusca: !!document.querySelector('[data-bv="rebusca"]') };
312
+ const dondeDespues = document.querySelector('.expMigas')?.textContent ?? '';
313
+ return { motores, opcionesMotor, filas, dondeAntes, dondeDespues, elegidas,
314
+ sube: !!document.querySelector('.expSubir'),
315
+ tomaAqui: !!document.querySelector('.expTomaAqui') };
308
316
  })()`);
309
317
  comprueba(
310
- 'the house form searches the whole disk instead of offering a dozen repos',
311
- !!casa?.hayBusca && !!casa?.rebusca && casa.antes > 0 && casa.despues === 0,
318
+ 'the house form walks the disk nothing offered, you pick what you want',
319
+ (casa?.filas ?? 0) > 0 && !!casa?.sube && !!casa?.tomaAqui &&
320
+ casa.dondeDespues !== casa.dondeAntes && (casa?.elegidas ?? 0) > 0,
312
321
  JSON.stringify(casa),
313
322
  );
314
323
  comprueba(
@@ -363,6 +372,126 @@ async function main() {
363
372
  JSON.stringify(ciudades),
364
373
  );
365
374
 
375
+ // The app's own dialogs. Every one of these was a `window.prompt` box, and
376
+ // a headless browser answers those with null — which is how three dead
377
+ // buttons could have passed a browser suite that only counted clicks.
378
+ const dlg = await cdp.evalua(`(async () => {
379
+ const espera = async (sel, n = 40) => {
380
+ for (let i = 0; i < n; i++) {
381
+ const el = document.querySelector(sel);
382
+ if (el) return el;
383
+ await new Promise(r => setTimeout(r, 250));
384
+ }
385
+ return null;
386
+ };
387
+ const nueva = document.getElementById('alWizard');
388
+ nueva?.click();
389
+ const caja = await espera('.dlgFondo .dlg');
390
+ if (!caja) return { falta: true };
391
+ const acepta = caja.querySelector('[data-dlg="si"]');
392
+ const campo = caja.querySelector('input[data-campo="name"]');
393
+ const bloqueado = !!acepta?.disabled;
394
+ if (campo) {
395
+ campo.value = 'x';
396
+ campo.dispatchEvent(new Event('input', { bubbles: true }));
397
+ await new Promise(r => setTimeout(r, 100));
398
+ }
399
+ const libre = !acepta?.disabled;
400
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
401
+ await new Promise(r => setTimeout(r, 200));
402
+ return {
403
+ titulo: caja.querySelector('h2')?.textContent?.trim() ?? '',
404
+ campo: !!campo, bloqueado, libre,
405
+ cerrado: !document.querySelector('.dlgFondo'),
406
+ };
407
+ })()`);
408
+ comprueba(
409
+ 'creating a city asks in the app, not through a browser prompt box',
410
+ !!dlg && !dlg.falta && dlg.campo && dlg.bloqueado && dlg.libre && dlg.cerrado,
411
+ JSON.stringify(dlg),
412
+ );
413
+
414
+ // Building a house from the houses view: the guide's form, in a dialog.
415
+ const alta = await cdp.evalua(`(async () => {
416
+ const ir = [...document.querySelectorAll('nav li')].find(l => /houses|casas/i.test(l.textContent));
417
+ ir?.click();
418
+ await new Promise(r => setTimeout(r, 700));
419
+ document.getElementById('altaAgente')?.click();
420
+ for (let i = 0; i < 40; i++) {
421
+ if (document.querySelector('.dlgFondo .exp .expFila')) break;
422
+ await new Promise(r => setTimeout(r, 250));
423
+ }
424
+ const caja = document.querySelector('.dlgFondo .dlg');
425
+ const hay = s => !!caja?.querySelector(s);
426
+ const fuera = {
427
+ nombre: hay('#bvNombre'), clase: hay('[data-bv="clase"]'), rol: hay('#bvRol'),
428
+ motor: hay('#bvRuntime') && hay('#bvModelo') && hay('#bvEsfuerzo'),
429
+ explora: hay('.exp .expLista'),
430
+ };
431
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
432
+ await new Promise(r => setTimeout(r, 200));
433
+ return fuera;
434
+ })()`);
435
+ comprueba(
436
+ 'and building a house opens the same form the guide uses, not three prompts',
437
+ !!alta && alta.nombre && alta.clase && alta.rol && alta.motor && alta.explora,
438
+ JSON.stringify(alta),
439
+ );
440
+
441
+ // The demo shelf: three cards, and a player that actually advances.
442
+ const demo = await cdp.evalua(`(async () => {
443
+ const espera = async (sel, n = 40) => {
444
+ for (let i = 0; i < n; i++) {
445
+ const el = document.querySelector(sel);
446
+ if (el) return el;
447
+ await new Promise(r => setTimeout(r, 250));
448
+ }
449
+ return null;
450
+ };
451
+ const ir = [...document.querySelectorAll('nav li')].find(l => /demos/i.test(l.textContent));
452
+ if (!ir) return { falta: true };
453
+ ir.click();
454
+ const carta = await espera('.demoCarta');
455
+ const cartas = document.querySelectorAll('.demoCarta').length;
456
+ carta?.click();
457
+ await espera('.demoPlayer');
458
+ // Fastest speed, so the check does not sit here for half a minute.
459
+ document.querySelector('[data-demo="vel"][data-ms="350"]')?.click();
460
+ await new Promise(r => setTimeout(r, 200));
461
+ const alPrincipio = document.querySelectorAll('#demoLista .liveTurn').length;
462
+ for (let i = 0; i < 60; i++) {
463
+ if (document.querySelectorAll('#demoLista .liveTurn').length > alPrincipio + 1) break;
464
+ await new Promise(r => setTimeout(r, 250));
465
+ }
466
+ const jugando = document.querySelectorAll('#demoLista .liveTurn').length;
467
+ document.querySelector('[data-demo="pausa"]')?.click();
468
+ await new Promise(r => setTimeout(r, 700));
469
+ const enPausa = document.querySelectorAll('#demoLista .liveTurn').length;
470
+ await new Promise(r => setTimeout(r, 700));
471
+ const sigueEnPausa = document.querySelectorAll('#demoLista .liveTurn').length;
472
+ document.querySelector('[data-demo="replay"]')?.click();
473
+ await new Promise(r => setTimeout(r, 150));
474
+ const trasReplay = document.querySelectorAll('#demoLista .liveTurn').length;
475
+ document.querySelector('[data-demo="pausa"]')?.click();
476
+ return { cartas, alPrincipio, jugando, enPausa, sigueEnPausa, trasReplay,
477
+ dice: (document.querySelector('.demoPlayer .pista')?.textContent ?? '') };
478
+ })()`);
479
+ comprueba(
480
+ 'the demo shelf offers one story per domain and plays it turn by turn',
481
+ (demo?.cartas ?? 0) === 3 && demo.jugando > demo.alPrincipio,
482
+ JSON.stringify(demo),
483
+ );
484
+ comprueba(
485
+ 'and pause holds, and replay starts it over',
486
+ demo?.sigueEnPausa === demo?.enPausa && (demo?.trasReplay ?? 99) < (demo?.enPausa ?? 0),
487
+ JSON.stringify(demo),
488
+ );
489
+ comprueba(
490
+ 'and it says out loud that it is a recording, not a live committee',
491
+ /recording|grabaci/i.test(demo?.dice ?? ''),
492
+ JSON.stringify(demo?.dice),
493
+ );
494
+
366
495
  // Spanish and English, on a switch. The READMEs shipped bilingual and the
367
496
  // product did not; a language button that does not actually change the page
368
497
  // is worse than none at all.