agents-city 0.4.0 → 0.5.2

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 (98) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +240 -11
  3. package/README.md +238 -11
  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 +1 -0
  8. package/bin/connect +5 -0
  9. package/bin/hall.html +117 -18
  10. package/bin/navegador.mjs +170 -1
  11. package/bin/serve.py +201 -101
  12. package/bin/test +15 -3
  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-diario.py +185 -0
  21. package/bin/test-doctor.py +31 -0
  22. package/bin/test-i18n.py +189 -0
  23. package/bin/test-navegador.py +61 -0
  24. package/bin/test-seat.py +78 -20
  25. package/bin/test-security.py +2 -0
  26. package/bin/test-serve.py +366 -0
  27. package/city/web/dist-hall/hall.js +789 -214
  28. package/city/web/src/bienvenida.ts +14 -13
  29. package/city/web/src/casa.ts +143 -56
  30. package/city/web/src/demo.ts +61 -31
  31. package/city/web/src/es.ts +187 -27
  32. package/city/web/src/explorador.ts +70 -43
  33. package/city/web/src/hall.ts +520 -103
  34. package/city/web/src/idioma.ts +21 -1
  35. package/city/web/src/motores.ts +48 -1
  36. package/city/web/src/vista.ts +58 -0
  37. package/demo/graba.py +5 -10
  38. package/docs/managed-connect.md +237 -0
  39. package/docs/security.md +41 -2
  40. package/package.json +8 -4
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/channel/adapter-prompts.ts +10 -0
  43. package/plugin/channel/adapter.js +78 -31
  44. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  45. package/plugin/channel/bus.js +109 -65
  46. package/plugin/channel/bus.ts +11 -1
  47. package/plugin/channel/client.js +67 -30
  48. package/plugin/channel/delivery-queue.ts +160 -21
  49. package/plugin/channel/hub/remote-roads.ts +32 -1
  50. package/plugin/channel/hub/road-controller.ts +116 -16
  51. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  52. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  53. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  54. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  55. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  56. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  57. package/plugin/channel/local-hub.js +2080 -97
  58. package/plugin/channel/local-hub.ts +5 -4
  59. package/plugin/channel/managed-connect/bridge.ts +208 -0
  60. package/plugin/channel/managed-connect/cli.ts +416 -0
  61. package/plugin/channel/managed-connect/device.ts +15 -0
  62. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  63. package/plugin/channel/managed-connect/person-message.ts +74 -0
  64. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  65. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  66. package/plugin/channel/managed-connect/storage.ts +653 -0
  67. package/plugin/channel/managed-connect/transport.ts +87 -0
  68. package/plugin/channel/managed-connect-cli.js +4777 -0
  69. package/plugin/channel/managed-connect-cli.ts +7 -0
  70. package/plugin/channel/managed-connect-client.d.ts +261 -0
  71. package/plugin/channel/managed-connect-client.js +6572 -0
  72. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  73. package/plugin/channel/package-lock.json +123 -105
  74. package/plugin/channel/package.json +4 -4
  75. package/plugin/channel/protocol.ts +4 -0
  76. package/plugin/channel/reception.ts +896 -0
  77. package/plugin/channel/road-cli.ts +1 -1
  78. package/plugin/channel/runtime/arnes.json +189 -0
  79. package/plugin/channel/runtime/arnes.ts +47 -0
  80. package/plugin/channel/runtime/claude.ts +18 -0
  81. package/plugin/channel/runtime/codex-config.ts +51 -1
  82. package/plugin/channel/runtime/codex.ts +31 -11
  83. package/plugin/channel/runtime/kimi.ts +6 -4
  84. package/plugin/channel/runtime-files.ts +39 -5
  85. package/plugin/channel/runtime-gateway.js +426 -100
  86. package/plugin/channel/runtime-gateway.ts +12 -0
  87. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  88. package/plugin/scripts/arnes.py +271 -0
  89. package/plugin/scripts/busca.py +46 -10
  90. package/plugin/scripts/cage.py +5 -0
  91. package/plugin/scripts/city-session.sh +144 -28
  92. package/plugin/scripts/crecimiento.py +4 -1
  93. package/plugin/scripts/demos.py +58 -43
  94. package/plugin/scripts/desinstala.py +36 -21
  95. package/plugin/scripts/diario.py +119 -0
  96. package/plugin/scripts/doctor.py +90 -15
  97. package/plugin/scripts/read-card.py +48 -8
  98. package/plugin/scripts/reception.py +665 -0
@@ -72,6 +72,7 @@ function runNext() {
72
72
  working = true;
73
73
  const message = queue.shift();
74
74
  const text = userText(message);
75
+ const delay = behavior() === 'slow' ? 350 : 5;
75
76
  setTimeout(() => {
76
77
  emit({
77
78
  type: 'assistant',
@@ -95,7 +96,7 @@ function runNext() {
95
96
  working = false;
96
97
  runNext();
97
98
  }, 5);
98
- }, 5);
99
+ }, delay);
99
100
  }
100
101
 
101
102
  function userText(message) {
@@ -0,0 +1,16 @@
1
+ # Local reception ingest
2
+
3
+ This measures the durable, human-quarantine boundary only: decrypted messages
4
+ committed to private local SQLite in protocol-v4 batches. It uses no network,
5
+ relay, model, or fake model latency.
6
+
7
+ Build the public client, then run the 1,000-message/second gate:
8
+
9
+ ```bash
10
+ npm --prefix plugin/channel run build
11
+ node benchmarks/reception/run.mjs --messages 1000 --batch-size 32 --minimum 1000
12
+ ```
13
+
14
+ Passing this gate says the computer can quarantine the offered transport load
15
+ without loss. It does not say a human or model can answer 1,000 requests per
16
+ second. Pending-count and byte caps apply backpressure when review falls behind.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /** Reproducible local reception-ingest capacity check. No network or model. */
3
+
4
+ import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import {
9
+ receptionDatabasePath,
10
+ recordReceptionMessages,
11
+ } from '../../plugin/channel/managed-connect-client.js';
12
+
13
+ const integerArg = (name, fallback, minimum, maximum) => {
14
+ const index = process.argv.indexOf(name);
15
+ const value = index >= 0 ? Number(process.argv[index + 1]) : fallback;
16
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
17
+ throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`);
18
+ }
19
+ return value;
20
+ };
21
+
22
+ const total = integerArg('--messages', 1_000, 1, 100_000);
23
+ const batchSize = integerArg('--batch-size', 32, 1, 32);
24
+ const minimum = integerArg('--minimum', 1_000, 1, 1_000_000);
25
+ const root = mkdtempSync(join(tmpdir(), 'agents-city-reception-bench-'));
26
+ const context = {
27
+ dataDir: root,
28
+ appHome: root,
29
+ runtimeDir: join(root, '.runtime', 'bus', 'city-target'),
30
+ owner: 'owner',
31
+ city: { id: 'city_target', address: 'owner/target', name: 'Target' },
32
+ domain: 'custom',
33
+ seatRole: '',
34
+ actors: { seat: { role: 'chair' } },
35
+ engines: { seat: 'claude' },
36
+ roads: [],
37
+ };
38
+ const envelope = (index) => ({
39
+ protocol: 'agents-city-bus/2',
40
+ id: `managed_bench_${String(index).padStart(8, '0')}`,
41
+ kind: 'road.message',
42
+ scope: 'road',
43
+ thread: null,
44
+ from: { city: 'peer/source', actor: 'seat', role: 'external-seat' },
45
+ to: { city: context.city.address, actor: 'seat' },
46
+ createdAt: '2026-08-28T12:00:00.000Z',
47
+ payload: {
48
+ text: 'x'.repeat(256),
49
+ transport: 'managed-e2ee',
50
+ roadId: 'road_benchmark',
51
+ remoteMessageId: String(index),
52
+ },
53
+ });
54
+
55
+ try {
56
+ // Exclude schema creation and first WAL setup from steady ingest.
57
+ recordReceptionMessages(context, [envelope(total)]);
58
+ const messages = Array.from({ length: total }, (_, index) => envelope(index));
59
+ const started = performance.now();
60
+ for (let index = 0; index < messages.length; index += batchSize) {
61
+ recordReceptionMessages(context, messages.slice(index, index + batchSize));
62
+ }
63
+ const seconds = (performance.now() - started) / 1_000;
64
+ const database = new DatabaseSync(receptionDatabasePath(root), { readOnly: true });
65
+ const persisted = Number(
66
+ database.prepare('SELECT COUNT(*) AS count FROM reception_messages').get().count,
67
+ ) - 1;
68
+ database.close();
69
+ const messagesPerSecond = total / seconds;
70
+ const result = {
71
+ messages: total,
72
+ batchSize,
73
+ persisted,
74
+ lost: total - persisted,
75
+ seconds: Number(seconds.toFixed(3)),
76
+ messagesPerSecond: Number(messagesPerSecond.toFixed(1)),
77
+ minimum,
78
+ passed: persisted === total && messagesPerSecond >= minimum,
79
+ };
80
+ console.log(JSON.stringify(result, null, 2));
81
+ if (!result.passed) process.exitCode = 1;
82
+ } finally {
83
+ rmSync(root, { recursive: true, force: true });
84
+ }
@@ -25,6 +25,7 @@ const ORDENES = {
25
25
  cities: { que: ['bin/cities'], di: 'list, create or select your cities' },
26
26
  agents: { que: ['bin/agents'], di: 'list agents and manage their workspace mounts' },
27
27
  road: { que: ['bin/road'], di: 'connect two cities explicitly' },
28
+ connect: { que: ['bin/connect'], di: 'pair this computer and open encrypted managed Roads' },
28
29
  bus: { que: ['bin/bus'], di: 'send information over those roads (seat only)' },
29
30
  committee: { que: ['bin/committee'], di: 'chair-mediated work with repo agents' },
30
31
  benchmark: { que: ['bin/benchmark'], di: 'offline stress/governance or opt-in live runtime latency' },
package/bin/connect ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Pair this computer and connect one or more local cities to managed Roads.
3
+ set -euo pipefail
4
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
+ exec node "$ROOT/plugin/channel/managed-connect-cli.js" "$@"
package/bin/hall.html CHANGED
@@ -79,11 +79,6 @@ button,input,select{font:inherit;color:inherit}
79
79
  fallback keeps today's behaviour until somebody grabs it. */
80
80
  #app{display:grid;grid-template-columns:250px minmax(420px,1fr) var(--live-ancho,minmax(420px,500px));height:100%}
81
81
  @media(max-width:1180px){#app{grid-template-columns:210px minmax(360px,1fr) var(--live-ancho,390px)}}
82
- @media(max-width:900px){#app{grid-template-columns:minmax(0,1fr) var(--live-ancho,320px);grid-template-rows:auto 1fr}
83
- nav{grid-column:1/-1;border-right:none;border-bottom:1px solid var(--linea);padding:10px 0}
84
- nav .marca,nav .pie{display:none}nav ol{display:flex;overflow-x:auto;gap:4px}}
85
- @media(max-width:680px){#app{grid-template-columns:1fr;grid-template-rows:auto 1fr}
86
- #livePanel{display:none}}
87
82
 
88
83
  /* ── rail ───────────────────────────────────────────────────────────────── */
89
84
  nav{background:var(--suelo2);border-right:1px solid var(--linea);padding:20px 0 16px;
@@ -241,6 +236,79 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
241
236
  .dos{display:grid;grid-template-columns:1fr 1fr;gap:13px}
242
237
  @media(max-width:640px){.dos{grid-template-columns:1fr}}
243
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
+
244
312
  /* districts + parcels */
245
313
  .tr{display:flex;align-items:center;gap:10px;background:var(--suelo2);
246
314
  border-bottom:1px solid var(--linea);padding:9px 13px}
@@ -279,7 +347,7 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
279
347
  padding:2px 7px;border-radius:8px;color:#0f172a;font-weight:600}
280
348
  .kindChip.code{background:#3b82f6}.kindChip.knowledge{background:#8b5cf6;color:#f4efe5}
281
349
  .kindChip.coordinator{background:#f59e0b}
282
- .rpgRol,.rpgRun,.rpgHogar{font:10.5px var(--mono);color:var(--tinta3)}
350
+ .rpgRol,.rpgHogar{font:10.5px var(--mono);color:var(--tinta3)}
283
351
  .rpgRun{color:var(--tinta2)}
284
352
  .rpgFila{display:grid;grid-template-columns:52px minmax(0,1fr) auto;gap:8px;align-items:center}
285
353
  .rpgFila label,.rpgSkills label{font:600 9px var(--hud);letter-spacing:.09em;
@@ -347,7 +415,6 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
347
415
  .bvOpcion:hover{border-color:var(--lampara)}
348
416
  .bvOpcion.elegida{border-color:var(--lampara);background:var(--burbuja-mia);
349
417
  box-shadow:inset 3px 0 0 var(--lampara)}
350
- .bvChips{display:flex;flex-wrap:wrap;gap:6px}
351
418
  .bvChip{border:1px solid var(--linea2);background:var(--suelo2);color:var(--tinta2);
352
419
  font:11px var(--mono);padding:5px 10px;border-radius:999px;cursor:pointer}
353
420
  .bvChip:hover{border-color:var(--lampara)}
@@ -368,10 +435,6 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
368
435
  .dlg .campo input:focus,.dlg .campo select:focus{outline:none;border-color:var(--lampara)}
369
436
  .dlgBotones{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
370
437
  .dlgBotones .bt[disabled]{opacity:.45;cursor:not-allowed}
371
- .bvClase{font:600 9px var(--mono);letter-spacing:.08em;text-transform:uppercase;
372
- opacity:.55;font-style:normal;margin-right:6px}
373
- .bvChip.cl-docs .bvClase{color:var(--verde);opacity:.85}
374
- .bvChip.cl-worktree .bvClase{color:var(--lampara);opacity:.85}
375
438
  .bt.bvMini{padding:7px 12px;font-size:11px;white-space:nowrap}
376
439
  .exp{border:1px solid var(--linea2);border-radius:5px;background:var(--suelo);
377
440
  padding:8px;margin-bottom:10px}
@@ -390,8 +453,9 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
390
453
  .expMiga{border:0;background:none;color:var(--tinta2);font:11px var(--mono);
391
454
  padding:1px 3px;cursor:pointer;border-radius:3px}
392
455
  .expMiga:hover{color:var(--lampara);background:var(--suelo3)}
393
- .expLista{max-height:240px;overflow:auto;border-top:1px solid var(--linea);
456
+ .expLista{max-height:min(46vh,420px);overflow:auto;border-top:1px solid var(--linea);
394
457
  border-bottom:1px solid var(--linea)}
458
+ .expCuenta{margin:0 0 6px;font:10px var(--mono);color:var(--tinta3)}
395
459
  .expFila{display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--linea)}
396
460
  .expFila:last-child{border-bottom:0}
397
461
  .expFila.puesta{background:var(--burbuja-mia)}
@@ -441,6 +505,7 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
441
505
  margin-bottom:12px}
442
506
  .demoBarra i{display:block;height:100%;background:var(--lampara);
443
507
  transition:width .35s ease}
508
+ .dlg .expLista{max-height:min(38vh,320px)}
444
509
  .demoPlayer .liveLista{max-height:calc(100vh - 300px);border:1px solid var(--linea);
445
510
  border-radius:7px;background:var(--panel);padding:16px}
446
511
  .bvMotor{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:6px}
@@ -583,6 +648,39 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
583
648
  letter-spacing:.03em}.liveEvidence[open] summary,.liveFull[open] summary{color:var(--tono)}
584
649
  .liveEvidence ul{margin:7px 0 0;padding-left:16px;color:var(--tinta3);font-size:11.5px;line-height:1.45}
585
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
+ }
586
684
  </style>
587
685
  </head>
588
686
  <body>
@@ -617,19 +715,19 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
617
715
  aria-label="Resize the live panel" title="Drag to resize · double-click to reset"></div>
618
716
  <header>
619
717
  <div class="liveTop"><span class="liveDot" id="liveDot"></span>
620
- <h2>City live</h2><span id="liveState">waiting</span></div>
621
- <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;
622
720
  private model reasoning is never shown or stored.</p>
623
721
  <div class="liveControls">
624
722
  <select id="liveFilter" aria-label="Filter live activity by conversation">
625
- <option value="">all conversations</option>
723
+ <option value="" data-i18n>all conversations</option>
626
724
  </select>
627
- <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>
628
726
  </div>
629
727
  <div id="liveContext" hidden></div>
630
728
  </header>
631
729
  <ol id="liveEvents" aria-live="polite">
632
- <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>
633
731
  </ol>
634
732
  </aside>
635
733
  </div>
@@ -638,7 +736,8 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
638
736
  <div class="edCaja">
639
737
  <div class="edCab"><b id="edTitulo"></b><span id="edLector"></span>
640
738
  <button id="edCerrar" type="button" aria-label="Close">×</button></div>
641
- <textarea id="edTexto" spellcheck="false"></textarea>
739
+ <textarea id="edTexto" name="instruction" aria-label="Agent instructions"
740
+ spellcheck="false"></textarea>
642
741
  <div class="edPie"><span id="edRuta"></span>
643
742
  <button id="edGuardar" type="button" class="bt ppal">Save</button></div>
644
743
  </div>
package/bin/navegador.mjs CHANGED
@@ -438,6 +438,67 @@ async function main() {
438
438
  JSON.stringify(alta),
439
439
  );
440
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
+
441
502
  // The demo shelf: three cards, and a player that actually advances.
442
503
  const demo = await cdp.evalua(`(async () => {
443
504
  const espera = async (sel, n = 40) => {
@@ -492,6 +553,114 @@ async function main() {
492
553
  JSON.stringify(demo?.dice),
493
554
  );
494
555
 
556
+ // Building a house for real: type a name, press the button, get a house.
557
+ //
558
+ // The suite used to open this form, check its fields existed and press
559
+ // Escape — so the one thing it is FOR was never exercised. What that hid:
560
+ // the roles list arrives a few hundred milliseconds after the form opens,
561
+ // the form repainted when it did, and the repaint redrew the name field
562
+ // from a state that had never been told what was typed in it. Typing while
563
+ // that answer was in flight lost the name, and then the button said to
564
+ // give it one. Intermittent, which is the worst kind.
565
+ //
566
+ // So this types IMMEDIATELY, before the roles can land, which is exactly
567
+ // what a person does.
568
+ const construida = await cdp.evalua(`(async () => {
569
+ const espera = async (sel, n = 40) => {
570
+ for (let i = 0; i < n; i++) {
571
+ const el = document.querySelector(sel);
572
+ if (el) return el;
573
+ await new Promise(r => setTimeout(r, 250));
574
+ }
575
+ return null;
576
+ };
577
+ const ir = [...document.querySelectorAll('nav li')].find(l => /houses|casas/i.test(l.textContent));
578
+ ir?.click();
579
+ await new Promise(r => setTimeout(r, 600));
580
+ document.getElementById('altaAgente')?.click();
581
+ const campo = await espera('.dlgFondo #bvNombre');
582
+ if (!campo) return { falta: true };
583
+ // Straight away: the roles request is still in flight right now.
584
+ campo.value = 'urgencias';
585
+ campo.dispatchEvent(new Event('input', { bubbles: true }));
586
+ // Long enough for that answer to land and do whatever it does.
587
+ await new Promise(r => setTimeout(r, 1500));
588
+ const sobrevive = (document.querySelector('.dlgFondo #bvNombre') || {}).value;
589
+ const boton = document.querySelector('.dlgFondo [data-dlg="si"]');
590
+ boton?.click();
591
+ const leeNombres = () =>
592
+ [...document.querySelectorAll('.fichaRPG h3')].map(h => h.textContent.trim());
593
+ // Wait for the house, not for a guess at how long building takes. A
594
+ // fixed sleep here made this check itself flaky, which is the one thing
595
+ // a check for a flaky bug must not be.
596
+ let nombres = [];
597
+ for (let i = 0; i < 60; i++) {
598
+ nombres = leeNombres();
599
+ if (!document.querySelector('.dlgFondo') && nombres.includes('urgencias')) break;
600
+ await new Promise(r => setTimeout(r, 250));
601
+ }
602
+ return { sobrevive, cerrado: !document.querySelector('.dlgFondo'), nombres };
603
+ })()`);
604
+ comprueba(
605
+ 'a name typed while the roles are still loading is still there afterwards',
606
+ construida?.sobrevive === 'urgencias',
607
+ JSON.stringify(construida),
608
+ );
609
+ comprueba(
610
+ 'and pressing build actually builds the house',
611
+ !!construida?.cerrado && (construida?.nombres ?? []).includes('urgencias'),
612
+ JSON.stringify(construida),
613
+ );
614
+
615
+ // And the other half: pressing build with nothing in the name.
616
+ //
617
+ // The failure this guards is not "it refuses" — it is refusing SILENTLY, or
618
+ // refusing while closing the dialog, which loses everything the person had
619
+ // already chosen. That is the same complaint from the other side: a form
620
+ // that says no and takes the work with it.
621
+ const vacia = await cdp.evalua(`(async () => {
622
+ const espera = async (sel, n = 40) => {
623
+ for (let i = 0; i < n; i++) {
624
+ const el = document.querySelector(sel);
625
+ if (el) return el;
626
+ await new Promise(r => setTimeout(r, 250));
627
+ }
628
+ return null;
629
+ };
630
+ const antes = document.querySelectorAll('.fichaRPG h3').length;
631
+ document.getElementById('altaAgente')?.click();
632
+ const campo = await espera('.dlgFondo #bvNombre');
633
+ if (!campo) return { falta: true };
634
+ // Choose something first: whatever it says, this must not be thrown away.
635
+ document.querySelector('.dlgFondo [data-bv=\"clase\"][data-id=\"knowledge\"]')?.click();
636
+ await new Promise(r => setTimeout(r, 300));
637
+ document.querySelector('.dlgFondo [data-dlg=\"si\"]')?.click();
638
+ await new Promise(r => setTimeout(r, 1200));
639
+ const caja = document.querySelector('.dlgFondo');
640
+ const elegida = !!document.querySelector('.dlgFondo .bvOpcion.elegida[data-id=\"knowledge\"]');
641
+ const aviso = document.querySelector('#aviso,.aviso,.toast');
642
+ const fuera = {
643
+ sigueAbierto: !!caja,
644
+ elegida,
645
+ dice: (aviso?.textContent ?? '').trim().slice(0, 80),
646
+ despues: document.querySelectorAll('.fichaRPG h3').length,
647
+ antes,
648
+ };
649
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
650
+ await new Promise(r => setTimeout(r, 200));
651
+ return fuera;
652
+ })()`);
653
+ comprueba(
654
+ 'building with no name refuses without closing the form',
655
+ !!vacia && !vacia.falta && vacia.sigueAbierto === true,
656
+ JSON.stringify(vacia),
657
+ );
658
+ comprueba(
659
+ 'and keeps what was already chosen, and builds nothing',
660
+ !!vacia?.elegida && vacia?.despues === vacia?.antes,
661
+ JSON.stringify(vacia),
662
+ );
663
+
495
664
  // Spanish and English, on a switch. The READMEs shipped bilingual and the
496
665
  // product did not; a language button that does not actually change the page
497
666
  // is worse than none at all.
@@ -534,7 +703,7 @@ async function main() {
534
703
  } finally {
535
704
  proceso.kill('SIGKILL');
536
705
  }
537
- console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 16 checks\n`);
706
+ console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 19 checks\n`);
538
707
  process.exit(fallos ? 1 : 0);
539
708
  }
540
709