agents-city 0.3.0 → 0.5.1

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.
Files changed (107) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +294 -16
  3. package/README.md +291 -16
  4. package/benchmarks/latency/fake-claude-cli.mjs +2 -1
  5. package/benchmarks/reception/README.md +16 -0
  6. package/benchmarks/reception/run.mjs +84 -0
  7. package/bin/agents-city.js +5 -0
  8. package/bin/connect +5 -0
  9. package/bin/hall.html +208 -22
  10. package/bin/navegador.mjs +203 -13
  11. package/bin/serve.py +176 -111
  12. package/bin/test +18 -4
  13. package/bin/test-arnes.py +192 -0
  14. package/bin/test-cage.py +9 -0
  15. package/bin/test-channel.py +178 -12
  16. package/bin/test-claude-runtime.py +132 -4
  17. package/bin/test-connect-client.mjs +609 -0
  18. package/bin/test-connect.py +52 -0
  19. package/bin/test-contracts.py +214 -0
  20. package/bin/test-demo.py +67 -0
  21. package/bin/test-desinstala.py +170 -0
  22. package/bin/test-doctor.py +31 -0
  23. package/bin/test-i18n.py +189 -0
  24. package/bin/test-navegador.py +54 -0
  25. package/bin/test-seat.py +78 -20
  26. package/bin/test-security.py +2 -0
  27. package/bin/test-serve.py +414 -2
  28. package/bin/uninstall +5 -0
  29. package/city/web/dist/city.js +65 -65
  30. package/city/web/dist/index.html +18 -5
  31. package/city/web/dist-hall/hall.js +1566 -486
  32. package/city/web/index.html +17 -4
  33. package/city/web/src/bienvenida.ts +48 -341
  34. package/city/web/src/casa.ts +324 -0
  35. package/city/web/src/demo.ts +277 -0
  36. package/city/web/src/dialogo.ts +178 -0
  37. package/city/web/src/es.ts +281 -25
  38. package/city/web/src/explorador.ts +216 -0
  39. package/city/web/src/hall.ts +612 -139
  40. package/city/web/src/idioma.ts +21 -1
  41. package/city/web/src/main.ts +28 -5
  42. package/city/web/src/motores.ts +48 -1
  43. package/city/web/src/vista.ts +58 -0
  44. package/demo/graba.py +127 -0
  45. package/demo/grabaciones/legal.jsonl +22 -0
  46. package/demo/grabaciones/medicina.jsonl +22 -0
  47. package/demo/grabaciones/software.jsonl +22 -0
  48. package/docs/managed-connect.md +237 -0
  49. package/docs/security.md +41 -2
  50. package/package.json +8 -4
  51. package/plugin/.claude-plugin/plugin.json +1 -1
  52. package/plugin/channel/adapter-prompts.ts +10 -0
  53. package/plugin/channel/adapter.js +78 -31
  54. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  55. package/plugin/channel/bus.js +109 -65
  56. package/plugin/channel/bus.ts +11 -1
  57. package/plugin/channel/client.js +67 -30
  58. package/plugin/channel/delivery-queue.ts +160 -21
  59. package/plugin/channel/hub/remote-roads.ts +32 -1
  60. package/plugin/channel/hub/road-controller.ts +116 -16
  61. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  62. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  63. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  64. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  65. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  66. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  67. package/plugin/channel/local-hub.js +2080 -97
  68. package/plugin/channel/local-hub.ts +5 -4
  69. package/plugin/channel/managed-connect/bridge.ts +208 -0
  70. package/plugin/channel/managed-connect/cli.ts +416 -0
  71. package/plugin/channel/managed-connect/device.ts +15 -0
  72. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  73. package/plugin/channel/managed-connect/person-message.ts +74 -0
  74. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  75. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  76. package/plugin/channel/managed-connect/storage.ts +653 -0
  77. package/plugin/channel/managed-connect/transport.ts +87 -0
  78. package/plugin/channel/managed-connect-cli.js +4777 -0
  79. package/plugin/channel/managed-connect-cli.ts +7 -0
  80. package/plugin/channel/managed-connect-client.d.ts +261 -0
  81. package/plugin/channel/managed-connect-client.js +6572 -0
  82. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  83. package/plugin/channel/package-lock.json +123 -105
  84. package/plugin/channel/package.json +4 -4
  85. package/plugin/channel/protocol.ts +4 -0
  86. package/plugin/channel/reception.ts +896 -0
  87. package/plugin/channel/road-cli.ts +1 -1
  88. package/plugin/channel/runtime/arnes.json +189 -0
  89. package/plugin/channel/runtime/arnes.ts +47 -0
  90. package/plugin/channel/runtime/claude.ts +18 -0
  91. package/plugin/channel/runtime/codex-config.ts +51 -1
  92. package/plugin/channel/runtime/codex.ts +31 -11
  93. package/plugin/channel/runtime/kimi.ts +6 -4
  94. package/plugin/channel/runtime-files.ts +39 -5
  95. package/plugin/channel/runtime-gateway.js +426 -100
  96. package/plugin/channel/runtime-gateway.ts +12 -0
  97. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  98. package/plugin/scripts/arnes.py +271 -0
  99. package/plugin/scripts/busca.py +46 -10
  100. package/plugin/scripts/cage.py +5 -0
  101. package/plugin/scripts/city-session.sh +144 -28
  102. package/plugin/scripts/crecimiento.py +4 -1
  103. package/plugin/scripts/demos.py +122 -0
  104. package/plugin/scripts/desinstala.py +237 -0
  105. package/plugin/scripts/doctor.py +37 -15
  106. package/plugin/scripts/read-card.py +48 -8
  107. package/plugin/scripts/reception.py +665 -0
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
@@ -76,11 +79,6 @@ button,input,select{font:inherit;color:inherit}
76
79
  fallback keeps today's behaviour until somebody grabs it. */
77
80
  #app{display:grid;grid-template-columns:250px minmax(420px,1fr) var(--live-ancho,minmax(420px,500px));height:100%}
78
81
  @media(max-width:1180px){#app{grid-template-columns:210px minmax(360px,1fr) var(--live-ancho,390px)}}
79
- @media(max-width:900px){#app{grid-template-columns:minmax(0,1fr) var(--live-ancho,320px);grid-template-rows:auto 1fr}
80
- nav{grid-column:1/-1;border-right:none;border-bottom:1px solid var(--linea);padding:10px 0}
81
- nav .marca,nav .pie{display:none}nav ol{display:flex;overflow-x:auto;gap:4px}}
82
- @media(max-width:680px){#app{grid-template-columns:1fr;grid-template-rows:auto 1fr}
83
- #livePanel{display:none}}
84
82
 
85
83
  /* ── rail ───────────────────────────────────────────────────────────────── */
86
84
  nav{background:var(--suelo2);border-right:1px solid var(--linea);padding:20px 0 16px;
@@ -238,6 +236,79 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
238
236
  .dos{display:grid;grid-template-columns:1fr 1fr;gap:13px}
239
237
  @media(max-width:640px){.dos{grid-template-columns:1fr}}
240
238
 
239
+ /* owner reception: remote text is displayed, never interpreted by the page */
240
+ .recModo{display:flex;align-items:center;justify-content:space-between;gap:18px;
241
+ padding:13px 15px;background:var(--suelo2);border:1px solid var(--linea);border-radius:5px}
242
+ .recModo>div{display:grid;grid-template-columns:auto 1fr;align-items:baseline;gap:4px 10px}
243
+ .recModo .et2{font:600 9px var(--hud);letter-spacing:.12em;text-transform:uppercase;
244
+ color:var(--tinta3)}
245
+ .recModo b{font:600 13px var(--hud);color:var(--tinta)}
246
+ .recModo p{grid-column:1/-1;margin:0;color:var(--tinta3);font-size:12px}
247
+ .recAutoConfig{margin:8px 0 18px;padding:14px;background:var(--suelo2);border:1px solid var(--linea);
248
+ border-radius:6px;display:grid;gap:13px}.recAutoConfig fieldset{display:flex;gap:16px;border:0;
249
+ padding:0;margin:0}.recAutoConfig legend{font:600 9px var(--hud);letter-spacing:.1em;
250
+ text-transform:uppercase;color:var(--tinta3);margin-bottom:7px}.recAutoConfig fieldset label{
251
+ font:11px var(--hud)}.recAutoConfig input[type=radio]{accent-color:var(--lampara)}
252
+ .recReglas{display:grid;gap:7px}.recReglas>p,.recAutoAviso{margin:0;color:var(--tinta3);
253
+ font-size:11px}.recReglas label{display:grid;grid-template-columns:minmax(150px,.4fr) minmax(0,1fr);
254
+ gap:10px;align-items:center}.recReglas label span b,.recReglas label span small{display:block}
255
+ .recReglas label span b{font:600 11px var(--hud)}.recReglas label span small{font:9px var(--mono);
256
+ color:var(--tinta3)}.recAutoConfig>button{justify-self:start}
257
+ .recConexiones{margin:18px 0;padding:14px;background:var(--suelo2);border:1px solid var(--linea);
258
+ border-radius:7px}.recSeccion{display:flex;align-items:baseline;justify-content:space-between;
259
+ gap:12px;margin-bottom:10px}.recSeccion b{font:600 13px var(--hud)}
260
+ .recSeccion span{font:10px var(--mono);color:var(--tinta3)}
261
+ .recCompose{display:grid;grid-template-columns:1fr auto;gap:8px;padding:10px;
262
+ background:var(--suelo3);border:1px solid var(--linea);border-radius:5px}
263
+ .recCompose label{display:grid;gap:5px}.recCompose label span{font:600 9.5px var(--hud);
264
+ letter-spacing:.09em;text-transform:uppercase;color:var(--tinta3)}
265
+ .recComposePersona{grid-column:1/-1}.recComposePersona select{width:100%;font:12px var(--hud)}
266
+ .recComposeTexto{grid-column:1/-1}.recCompose textarea{width:100%;
267
+ min-height:66px;resize:vertical}.recCompose button{grid-column:2;justify-self:end}
268
+ .recResumen{display:flex;align-items:baseline;gap:9px;padding-bottom:10px;
269
+ border-bottom:1px solid var(--linea)}
270
+ .recResumen b{font:500 25px var(--mono)}
271
+ .recResumen span{font:600 11px var(--hud);text-transform:uppercase;letter-spacing:.08em}
272
+ .recResumen small{margin-left:auto;font:10px var(--mono);color:var(--tinta3)}
273
+ .recMensajes{display:flex;flex-direction:column;gap:12px;margin-top:12px}
274
+ .recMensaje{background:var(--suelo2);border:1px solid var(--linea);border-radius:7px;
275
+ overflow:hidden;box-shadow:var(--sombra)}
276
+ .recMensaje>header{display:flex;align-items:center;justify-content:space-between;gap:12px;
277
+ padding:11px 14px;border-bottom:1px solid var(--linea);font:10px var(--mono);color:var(--tinta3)}
278
+ .recMensaje>header>div{display:flex;align-items:center;gap:9px;min-width:0}
279
+ .recOrigen{color:var(--tinta);font-weight:600;overflow:hidden;text-overflow:ellipsis}
280
+ .recSeguro{color:var(--verde);font:600 9px var(--hud);letter-spacing:.06em;text-transform:uppercase;
281
+ white-space:nowrap}
282
+ .recTexto{margin:0;padding:17px 16px;white-space:pre-wrap;overflow-wrap:anywhere;
283
+ background:var(--suelo3);color:var(--tinta);border:0;font:13px/1.55 var(--mono);
284
+ max-height:280px;overflow-y:auto}
285
+ .recAcciones{display:grid;grid-template-columns:minmax(0,1fr) minmax(190px,.42fr);gap:14px;
286
+ padding:14px;border-top:1px solid var(--linea)}
287
+ .recAcciones form{display:flex;flex-direction:column;align-items:flex-start;gap:10px}
288
+ .recAcciones fieldset{border:0;padding:0;margin:0;width:100%;min-width:0}
289
+ .recAcciones legend,.recAcciones details>summary,.recAcciones form>label{font:600 9.5px var(--hud);
290
+ letter-spacing:.09em;text-transform:uppercase;color:var(--tinta3)}
291
+ .recDestinos{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:6px;
292
+ margin-top:7px}
293
+ .recDestino{display:flex;align-items:center;gap:8px;padding:7px 8px;border:1px solid var(--linea);
294
+ border-radius:4px;background:var(--suelo3);cursor:pointer;min-width:0}
295
+ .recDestino:has(input:checked){border-color:var(--lampara);background:var(--burbuja-mia)}
296
+ .recDestino input{accent-color:var(--lampara);flex:none}
297
+ .recDestino span{min-width:0}.recDestino b,.recDestino small{display:block;overflow:hidden;
298
+ text-overflow:ellipsis;white-space:nowrap}
299
+ .recDestino b{font:600 11px var(--hud)}.recDestino small{font:9px var(--mono);color:var(--tinta3)}
300
+ .recAcciones details{padding-top:2px}.recAcciones details>summary{cursor:pointer;color:var(--carta)}
301
+ .recAcciones details form{margin-top:10px}.recAcciones details input{width:100%}
302
+ .recAvisoAccion{display:flex;justify-content:flex-end;padding:12px 14px;border-top:1px solid var(--linea)}
303
+ .recVacia,.recError{padding:24px;border:1px dashed var(--linea2);border-radius:6px;
304
+ color:var(--tinta3);text-align:center}
305
+ .recVacia b{font:600 14px var(--hud);color:var(--tinta)}.recVacia p{margin:5px 0 0}
306
+ .recError{color:var(--carta);text-align:left;font:12px var(--mono)}
307
+ @media(max-width:760px){.recAcciones{grid-template-columns:1fr}.recMensaje>header{align-items:flex-start;
308
+ flex-direction:column}.recResumen{flex-wrap:wrap}.recResumen small{margin-left:0;width:100%}
309
+ .recSeccion,.recCompose label{align-items:flex-start;flex-direction:column}
310
+ .recReglas label{grid-template-columns:1fr}}
311
+
241
312
  /* districts + parcels */
242
313
  .tr{display:flex;align-items:center;gap:10px;background:var(--suelo2);
243
314
  border-bottom:1px solid var(--linea);padding:9px 13px}
@@ -276,7 +347,7 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
276
347
  padding:2px 7px;border-radius:8px;color:#0f172a;font-weight:600}
277
348
  .kindChip.code{background:#3b82f6}.kindChip.knowledge{background:#8b5cf6;color:#f4efe5}
278
349
  .kindChip.coordinator{background:#f59e0b}
279
- .rpgRol,.rpgRun,.rpgHogar{font:10.5px var(--mono);color:var(--tinta3)}
350
+ .rpgRol,.rpgHogar{font:10.5px var(--mono);color:var(--tinta3)}
280
351
  .rpgRun{color:var(--tinta2)}
281
352
  .rpgFila{display:grid;grid-template-columns:52px minmax(0,1fr) auto;gap:8px;align-items:center}
282
353
  .rpgFila label,.rpgSkills label{font:600 9px var(--hud);letter-spacing:.09em;
@@ -344,20 +415,99 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
344
415
  .bvOpcion:hover{border-color:var(--lampara)}
345
416
  .bvOpcion.elegida{border-color:var(--lampara);background:var(--burbuja-mia);
346
417
  box-shadow:inset 3px 0 0 var(--lampara)}
347
- .bvChips{display:flex;flex-wrap:wrap;gap:6px}
348
418
  .bvChip{border:1px solid var(--linea2);background:var(--suelo2);color:var(--tinta2);
349
419
  font:11px var(--mono);padding:5px 10px;border-radius:999px;cursor:pointer}
350
420
  .bvChip:hover{border-color:var(--lampara)}
351
421
  .bvChip.elegida{border-color:var(--lampara);background:var(--burbuja-mia);color:var(--tinta)}
352
- .bvClase{font:600 9px var(--mono);letter-spacing:.08em;text-transform:uppercase;
353
- opacity:.55;font-style:normal;margin-right:6px}
354
- .bvChip.cl-docs .bvClase{color:var(--verde);opacity:.85}
355
- .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)}
422
+ .dlgFondo{position:fixed;inset:0;z-index:60;display:flex;align-items:center;
423
+ justify-content:center;padding:24px;background:var(--velo);backdrop-filter:blur(2px)}
424
+ .dlg{background:var(--suelo2);border:1px solid var(--linea2);border-radius:8px;
425
+ box-shadow:var(--sombra-alta);padding:22px 24px;width:100%;max-width:460px;
426
+ max-height:86vh;overflow:auto}
427
+ .dlg h2{font:600 17px var(--hud);color:var(--tinta);margin:0 0 10px}
428
+ .dlgProsa{font:13px/1.55 var(--hud);color:var(--tinta2);margin:0 0 10px}
429
+ .dlg .campo{margin-bottom:12px}
430
+ .dlg .campo label{display:block;font:600 10px var(--hud);letter-spacing:.14em;
431
+ text-transform:uppercase;color:var(--tinta3);margin-bottom:5px}
432
+ .dlg .campo input,.dlg .campo select{width:100%;background:var(--suelo);
433
+ border:1px solid var(--linea2);border-radius:4px;padding:8px 10px;
434
+ font-family:var(--hud);font-size:13px;color:var(--tinta)}
435
+ .dlg .campo input:focus,.dlg .campo select:focus{outline:none;border-color:var(--lampara)}
436
+ .dlgBotones{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
437
+ .dlgBotones .bt[disabled]{opacity:.45;cursor:not-allowed}
360
438
  .bt.bvMini{padding:7px 12px;font-size:11px;white-space:nowrap}
439
+ .exp{border:1px solid var(--linea2);border-radius:5px;background:var(--suelo);
440
+ padding:8px;margin-bottom:10px}
441
+ .expAtajos{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:7px}
442
+ .expAtajo{border:1px solid var(--linea);background:var(--suelo3);color:var(--tinta2);
443
+ font:11px var(--hud);padding:3px 9px;border-radius:999px;cursor:pointer}
444
+ .expAtajo:hover,.expAtajo.aqui{border-color:var(--lampara);color:var(--lampara)}
445
+ .expBarra{display:flex;gap:7px;align-items:center;margin-bottom:7px}
446
+ .expSubir{border:1px solid var(--linea2);background:var(--suelo3);color:var(--tinta2);
447
+ font:12px var(--mono);width:26px;height:26px;border-radius:4px;cursor:pointer;flex:none}
448
+ .expSubir:hover:not([disabled]){border-color:var(--lampara);color:var(--lampara)}
449
+ .expSubir[disabled]{opacity:.35;cursor:default}
450
+ .expMigas{display:flex;flex-wrap:wrap;align-items:center;gap:1px;overflow:hidden;
451
+ font:11px var(--mono);color:var(--tinta3)}
452
+ .expMigas i{opacity:.4;font-style:normal}
453
+ .expMiga{border:0;background:none;color:var(--tinta2);font:11px var(--mono);
454
+ padding:1px 3px;cursor:pointer;border-radius:3px}
455
+ .expMiga:hover{color:var(--lampara);background:var(--suelo3)}
456
+ .expLista{max-height:min(46vh,420px);overflow:auto;border-top:1px solid var(--linea);
457
+ border-bottom:1px solid var(--linea)}
458
+ .expCuenta{margin:0 0 6px;font:10px var(--mono);color:var(--tinta3)}
459
+ .expFila{display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--linea)}
460
+ .expFila:last-child{border-bottom:0}
461
+ .expFila.puesta{background:var(--burbuja-mia)}
462
+ .expNombre{flex:1;display:flex;align-items:center;gap:7px;border:0;background:none;
463
+ text-align:left;font:12px var(--hud);color:var(--tinta);padding:6px 8px;cursor:pointer;
464
+ overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
465
+ .expNombre[disabled]{cursor:default;color:var(--tinta2)}
466
+ .expNombre:hover:not([disabled]){color:var(--lampara)}
467
+ .expIcono{font-style:normal;color:var(--tinta3);font-size:10px;width:8px;flex:none}
468
+ .expFila.fich .expNombre{font-family:var(--mono);font-size:11px}
469
+ .expMarca{font:600 9px var(--mono);letter-spacing:.06em;text-transform:uppercase;
470
+ color:var(--lampara);opacity:.8;font-style:normal;flex:none}
471
+ .expMas{border:0;background:none;color:var(--tinta3);font:13px var(--mono);
472
+ width:30px;height:28px;cursor:pointer;flex:none}
473
+ .expMas:hover{color:var(--lampara)}
474
+ .expFila.puesta .expMas{color:var(--lampara)}
475
+ .expTomaAqui{margin-top:8px;width:100%}
476
+ .expElegidas{margin:8px 0 4px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
477
+ .expElegidas .bvEtiqueta{width:100%;margin:0 0 2px}
478
+ .expQuita{font-style:normal;margin-left:7px;opacity:.6}
479
+ .pista.malo{color:var(--carta)}
480
+ .demoRejilla{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
481
+ gap:12px;margin:18px 0 14px}
482
+ .demoCarta{display:flex;flex-direction:column;gap:5px;text-align:left;cursor:pointer;
483
+ background:var(--suelo2);border:1px solid var(--linea2);border-radius:7px;
484
+ padding:15px 16px;box-shadow:var(--sombra)}
485
+ .demoCarta:hover{border-color:var(--lampara)}
486
+ .demoDominio{font:600 10px var(--hud);letter-spacing:.14em;text-transform:uppercase;
487
+ color:var(--lampara)}
488
+ .demoCarta b{font:600 15px/1.35 var(--hud);color:var(--tinta)}
489
+ .demoCiudad{font:12px var(--texto);color:var(--tinta2)}
490
+ .demoReparto{font:10px var(--mono);color:var(--tinta3);overflow:hidden;
491
+ white-space:nowrap;text-overflow:ellipsis}
492
+ .demoPlay{margin-top:6px;font:600 11px var(--mono);color:var(--tinta2)}
493
+ .demoCarta:hover .demoPlay{color:var(--lampara)}
494
+ .demoCab{display:flex;gap:14px;align-items:flex-start;margin-bottom:12px}
495
+ .demoCab h2{font:600 17px var(--hud);color:var(--tinta);margin:2px 0 0}
496
+ .demoMandos{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:9px}
497
+ .demoVel{display:inline-flex;border:1px solid var(--linea2);border-radius:999px;
498
+ overflow:hidden;background:var(--suelo2)}
499
+ .demoVelBoton{border:0;background:none;color:var(--tinta3);font:11px var(--mono);
500
+ padding:5px 10px;cursor:pointer}
501
+ .demoVelBoton:hover{color:var(--tinta)}
502
+ .demoVelBoton.aqui{background:var(--burbuja-mia);color:var(--lampara)}
503
+ .demoCuenta{font:11px var(--mono);color:var(--tinta3);margin-left:auto}
504
+ .demoBarra{height:3px;background:var(--linea);border-radius:2px;overflow:hidden;
505
+ margin-bottom:12px}
506
+ .demoBarra i{display:block;height:100%;background:var(--lampara);
507
+ transition:width .35s ease}
508
+ .dlg .expLista{max-height:min(38vh,320px)}
509
+ .demoPlayer .liveLista{max-height:calc(100vh - 300px);border:1px solid var(--linea);
510
+ border-radius:7px;background:var(--panel);padding:16px}
361
511
  .bvMotor{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:6px}
362
512
  .bvMotor .campo{margin:0}
363
513
  @media (max-width:640px){.bvMotor{grid-template-columns:1fr}}
@@ -456,7 +606,9 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
456
606
  stroke-linecap:round;stroke-linejoin:round}
457
607
  .liveContextText{min-width:0;color:var(--tinta3);font:10px/1.35 var(--hud)}
458
608
  .liveContextText b{color:var(--tinta2);font-weight:600}
459
- #liveEvents{list-style:none;margin:0;padding:16px 16px 26px;overflow-y:auto;flex:1;
609
+ /* The demo player replays into a list of its own, and must look identical:
610
+ it is the same renderer, so it gets the same rules rather than a copy. */
611
+ #liveEvents,.liveLista{list-style:none;margin:0;padding:16px 16px 26px;overflow-y:auto;flex:1;
460
612
  display:flex;flex-direction:column;gap:13px;scrollbar-color:var(--linea2) transparent}
461
613
  .liveEmpty{margin:auto;padding:30px 18px;text-align:center;color:var(--tinta3);font-size:13px;line-height:1.55}
462
614
  .liveTurn{--tono:var(--tinta3);display:grid;grid-template-columns:31px minmax(0,1fr);
@@ -496,6 +648,39 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
496
648
  letter-spacing:.03em}.liveEvidence[open] summary,.liveFull[open] summary{color:var(--tono)}
497
649
  .liveEvidence ul{margin:7px 0 0;padding-left:16px;color:var(--tinta3);font-size:11.5px;line-height:1.45}
498
650
  .liveEvidence li+li{margin-top:4px}.liveFull p{margin-top:7px;color:var(--tinta3);font-size:11.5px}
651
+
652
+ /* Keep responsive overrides after the base rail and live-panel rules. Placing
653
+ them before those rules made the desktop declarations win in the cascade,
654
+ leaving the actual Hall content at zero height on a phone. */
655
+ @media(max-width:900px){
656
+ #app{grid-template-columns:minmax(0,1fr) var(--live-ancho,320px);
657
+ grid-template-rows:auto minmax(0,1fr)}
658
+ body.enGuia #app{grid-template-columns:minmax(0,1fr);
659
+ grid-template-rows:auto minmax(0,1fr)}
660
+ nav{grid-column:1/-1;grid-row:1;border-right:0;border-bottom:1px solid var(--linea);
661
+ padding:8px 10px;display:flex;flex-direction:row;align-items:center;gap:8px;
662
+ overflow-x:auto;overflow-y:hidden}
663
+ nav .marca,nav .pie,nav #ciudades,nav #railOtras{display:none}
664
+ .railCiudad{margin:0;min-width:154px;flex:0 0 auto;padding:7px 10px}
665
+ .railCiudad .railEtiqueta,.railCiudad .railDe{display:none}
666
+ nav ol{padding:0;display:flex;flex:1 0 auto;flex-direction:row;gap:4px}
667
+ nav li{padding:8px 9px}
668
+ main{grid-column:1;grid-row:2}
669
+ #livePanel{grid-column:2;grid-row:2}
670
+ .cuerpo{padding:28px 30px 52px}
671
+ }
672
+ @media(max-width:680px){
673
+ #app{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
674
+ #livePanel{display:none;grid-column:1;grid-row:2}
675
+ main{grid-column:1;grid-row:2}
676
+ .cuerpo{padding:24px 18px 48px}
677
+ .cuerpo>.dentro{gap:20px}
678
+ .recModo{align-items:stretch;flex-direction:column;gap:10px}
679
+ .recModo>button{align-self:flex-start}
680
+ .recAutoConfig fieldset{flex-direction:column;gap:8px}
681
+ .recCompose{grid-template-columns:1fr}
682
+ .recCompose button{grid-column:1;justify-self:stretch}
683
+ }
499
684
  </style>
500
685
  </head>
501
686
  <body>
@@ -530,19 +715,19 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
530
715
  aria-label="Resize the live panel" title="Drag to resize · double-click to reset"></div>
531
716
  <header>
532
717
  <div class="liveTop"><span class="liveDot" id="liveDot"></span>
533
- <h2>City live</h2><span id="liveState">waiting</span></div>
534
- <p>A moderated conversation over the WebSocket bus. The seat routes each turn;
718
+ <h2 data-i18n>City live</h2><span id="liveState" data-i18n>waiting</span></div>
719
+ <p data-i18n>A moderated conversation over the WebSocket bus. The seat routes each turn;
535
720
  private model reasoning is never shown or stored.</p>
536
721
  <div class="liveControls">
537
722
  <select id="liveFilter" aria-label="Filter live activity by conversation">
538
- <option value="">all conversations</option>
723
+ <option value="" data-i18n>all conversations</option>
539
724
  </select>
540
- <button id="liveWorkToggle" type="button" aria-pressed="false">show work</button>
725
+ <button id="liveWorkToggle" type="button" aria-pressed="false" data-i18n>show work</button>
541
726
  </div>
542
727
  <div id="liveContext" hidden></div>
543
728
  </header>
544
729
  <ol id="liveEvents" aria-live="polite">
545
- <li class="liveEmpty">Start the city session. Its visible conversation will appear here as it happens.</li>
730
+ <li class="liveEmpty" data-i18n>Start the city session. Its visible conversation will appear here as it happens.</li>
546
731
  </ol>
547
732
  </aside>
548
733
  </div>
@@ -551,7 +736,8 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
551
736
  <div class="edCaja">
552
737
  <div class="edCab"><b id="edTitulo"></b><span id="edLector"></span>
553
738
  <button id="edCerrar" type="button" aria-label="Close">×</button></div>
554
- <textarea id="edTexto" spellcheck="false"></textarea>
739
+ <textarea id="edTexto" name="instruction" aria-label="Agent instructions"
740
+ spellcheck="false"></textarea>
555
741
  <div class="edPie"><span id="edRuta"></span>
556
742
  <button id="edGuardar" type="button" class="bt ppal">Save</button></div>
557
743
  </div>
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,187 @@ 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
+ // Human reception must scale by people, not by duplicated composers. With
442
+ // thirty connections the review queue still comes first, then one compact
443
+ // composer whose recipient is selected explicitly.
444
+ const reception = await cdp.evalua(`(async () => {
445
+ const ir = [...document.querySelectorAll('nav li')]
446
+ .find(l => /reception|recepci[oó]n/i.test(l.textContent));
447
+ if (!ir) return { falta: 'no reception entry' };
448
+ ir.click();
449
+ for (let i = 0; i < 40; i++) {
450
+ if (document.querySelector('.recMensaje')) break;
451
+ await new Promise(r => setTimeout(r, 250));
452
+ }
453
+ const message = document.querySelector('.recMensaje');
454
+ const compose = document.querySelector('.recConexiones');
455
+ const route = document.querySelector('[data-rec-route] button[type=submit]');
456
+ const rejection = document.querySelector('.recAcciones details');
457
+ return {
458
+ navRole: ir.getAttribute('role'),
459
+ navTabIndex: ir.getAttribute('tabindex'),
460
+ messages: document.querySelectorAll('.recMensaje').length,
461
+ composers: document.querySelectorAll('.recCompose').length,
462
+ recipients: document.querySelectorAll('.recCompose select[name=connection] option').length,
463
+ reviewFirst: !!message && !!compose &&
464
+ !!(message.compareDocumentPosition(compose) & Node.DOCUMENT_POSITION_FOLLOWING),
465
+ route: route?.textContent?.trim() ?? '',
466
+ rejection: rejection?.textContent?.trim() ?? '',
467
+ };
468
+ })()`);
469
+ comprueba(
470
+ 'thirty connected people use one recipient picker, not thirty message boxes',
471
+ reception?.navRole === 'button' && reception?.navTabIndex === '0' &&
472
+ reception?.composers === 1 && reception?.recipients === 30,
473
+ JSON.stringify(reception),
474
+ );
475
+ comprueba(
476
+ 'pending messages stay above composing and use distinct route/reject language',
477
+ reception?.messages === 1 && reception?.reviewFirst === true &&
478
+ /route|dirigir/i.test(reception?.route ?? '') &&
479
+ /reason|motivo/i.test(reception?.rejection ?? ''),
480
+ JSON.stringify(reception),
481
+ );
482
+ const routed = await cdp.evalua(`(async () => {
483
+ const form = document.querySelector('[data-rec-route]');
484
+ const checkbox = form?.querySelector('input[name=destination]');
485
+ const button = form?.querySelector('button[type=submit]');
486
+ if (!form || !checkbox || !button) return { falta: true };
487
+ checkbox.click();
488
+ if (button.disabled) return { disabled: true };
489
+ button.click();
490
+ for (let i = 0; i < 40; i++) {
491
+ if (!document.querySelector('.recMensaje')) return { removed: true };
492
+ await new Promise(r => setTimeout(r, 250));
493
+ }
494
+ return { removed: false };
495
+ })()`);
496
+ comprueba(
497
+ 'choosing a city and routing removes the message from human quarantine',
498
+ routed?.removed === true,
499
+ JSON.stringify(routed),
500
+ );
501
+
502
+ // The demo shelf: three cards, and a player that actually advances.
503
+ const demo = await cdp.evalua(`(async () => {
504
+ const espera = async (sel, n = 40) => {
505
+ for (let i = 0; i < n; i++) {
506
+ const el = document.querySelector(sel);
507
+ if (el) return el;
508
+ await new Promise(r => setTimeout(r, 250));
509
+ }
510
+ return null;
511
+ };
512
+ const ir = [...document.querySelectorAll('nav li')].find(l => /demos/i.test(l.textContent));
513
+ if (!ir) return { falta: true };
514
+ ir.click();
515
+ const carta = await espera('.demoCarta');
516
+ const cartas = document.querySelectorAll('.demoCarta').length;
517
+ carta?.click();
518
+ await espera('.demoPlayer');
519
+ // Fastest speed, so the check does not sit here for half a minute.
520
+ document.querySelector('[data-demo="vel"][data-ms="350"]')?.click();
521
+ await new Promise(r => setTimeout(r, 200));
522
+ const alPrincipio = document.querySelectorAll('#demoLista .liveTurn').length;
523
+ for (let i = 0; i < 60; i++) {
524
+ if (document.querySelectorAll('#demoLista .liveTurn').length > alPrincipio + 1) break;
525
+ await new Promise(r => setTimeout(r, 250));
526
+ }
527
+ const jugando = document.querySelectorAll('#demoLista .liveTurn').length;
528
+ document.querySelector('[data-demo="pausa"]')?.click();
529
+ await new Promise(r => setTimeout(r, 700));
530
+ const enPausa = document.querySelectorAll('#demoLista .liveTurn').length;
531
+ await new Promise(r => setTimeout(r, 700));
532
+ const sigueEnPausa = document.querySelectorAll('#demoLista .liveTurn').length;
533
+ document.querySelector('[data-demo="replay"]')?.click();
534
+ await new Promise(r => setTimeout(r, 150));
535
+ const trasReplay = document.querySelectorAll('#demoLista .liveTurn').length;
536
+ document.querySelector('[data-demo="pausa"]')?.click();
537
+ return { cartas, alPrincipio, jugando, enPausa, sigueEnPausa, trasReplay,
538
+ dice: (document.querySelector('.demoPlayer .pista')?.textContent ?? '') };
539
+ })()`);
540
+ comprueba(
541
+ 'the demo shelf offers one story per domain and plays it turn by turn',
542
+ (demo?.cartas ?? 0) === 3 && demo.jugando > demo.alPrincipio,
543
+ JSON.stringify(demo),
544
+ );
545
+ comprueba(
546
+ 'and pause holds, and replay starts it over',
547
+ demo?.sigueEnPausa === demo?.enPausa && (demo?.trasReplay ?? 99) < (demo?.enPausa ?? 0),
548
+ JSON.stringify(demo),
549
+ );
550
+ comprueba(
551
+ 'and it says out loud that it is a recording, not a live committee',
552
+ /recording|grabaci/i.test(demo?.dice ?? ''),
553
+ JSON.stringify(demo?.dice),
554
+ );
555
+
366
556
  // Spanish and English, on a switch. The READMEs shipped bilingual and the
367
557
  // product did not; a language button that does not actually change the page
368
558
  // is worse than none at all.
@@ -405,7 +595,7 @@ async function main() {
405
595
  } finally {
406
596
  proceso.kill('SIGKILL');
407
597
  }
408
- console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 16 checks\n`);
598
+ console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 19 checks\n`);
409
599
  process.exit(fallos ? 1 : 0);
410
600
  }
411
601