agents-city 0.5.6 → 0.5.7

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.5.6",
12
+ "version": "0.5.7",
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/bin/hall.html CHANGED
@@ -386,6 +386,9 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
386
386
  /* ── the guided first run ────────────────────────────────────────────────── */
387
387
  .bv{display:flex;flex-direction:column;gap:26px;max-width:min(760px,100%)}
388
388
  .bv .prosa{max-width:100%}
389
+ /* The consequence of a choice, under the button that makes it: quieter than the
390
+ question and still legible, because a warning nobody reads is decoration. */
391
+ .bv .prosa.apunte{font-size:13px;margin-top:10px;opacity:.82}
389
392
  .bv code.mono{word-break:break-all}
390
393
  /* The live rail has nothing to show before a city exists, and it costs half
391
394
  the screen exactly when the guide needs it. */
package/bin/navegador.mjs CHANGED
@@ -35,11 +35,31 @@ function dondeEstaChrome() {
35
35
  return CANDIDATOS.find((p) => existsSync(p)) || '';
36
36
  }
37
37
 
38
- /** Chrome prints its DevTools endpoint on stderr; that is the handshake. */
39
- function esperaEndpoint(proceso, limite = 20000) {
38
+ /** Chrome prints its DevTools endpoint on stderr; that is the handshake.
39
+ *
40
+ * Sixty seconds, not twenty. This is a browser cold-starting on a shared CI
41
+ * runner that is also finishing two thousand other checks, and twenty seconds
42
+ * was a coin toss we kept winning until we did not. Waiting longer costs
43
+ * nothing when it works, because the wait ends at the handshake.
44
+ *
45
+ * And a timeout now carries whatever Chrome DID say. "chrome never announced
46
+ * its port" is a sentence about the observer: it names what we stopped waiting
47
+ * for and drops the one piece of evidence — a missing library, a sandbox
48
+ * refusal, a profile it could not write — that says which of those it was.
49
+ */
50
+ function esperaEndpoint(proceso, limite = Number(process.env.CITY_BROWSER_WAIT_MS) || 60000) {
40
51
  return new Promise((listo, falla) => {
41
52
  let visto = '';
42
- const reloj = setTimeout(() => falla(new Error('chrome never announced its port')), limite);
53
+ const reloj = setTimeout(
54
+ () =>
55
+ falla(
56
+ new Error(
57
+ `chrome never announced its port in ${limite / 1000}s` +
58
+ (visto.trim() ? `; it said: ${visto.trim().slice(-400)}` : ', and said nothing'),
59
+ ),
60
+ ),
61
+ limite,
62
+ );
43
63
  proceso.stderr.on('data', (trozo) => {
44
64
  visto += trozo.toString();
45
65
  const m = visto.match(/ws:\/\/[^\s]+/);
@@ -65,7 +85,9 @@ class Cdp {
65
85
  const espera = this.pendientes.get(mensaje.id);
66
86
  if (!espera) return;
67
87
  this.pendientes.delete(mensaje.id);
68
- mensaje.error ? espera.falla(new Error(JSON.stringify(mensaje.error))) : espera.listo(mensaje.result);
88
+ mensaje.error
89
+ ? espera.falla(new Error(JSON.stringify(mensaje.error)))
90
+ : espera.listo(mensaje.result);
69
91
  });
70
92
  }
71
93
 
@@ -73,7 +95,9 @@ class Cdp {
73
95
  const socket = new WebSocket(url);
74
96
  await new Promise((listo, falla) => {
75
97
  socket.addEventListener('open', listo, { once: true });
76
- socket.addEventListener('error', () => falla(new Error('cannot reach chrome')), { once: true });
98
+ socket.addEventListener('error', () => falla(new Error('cannot reach chrome')), {
99
+ once: true,
100
+ });
77
101
  });
78
102
  return new Cdp(socket);
79
103
  }
@@ -102,7 +126,9 @@ const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
102
126
 
103
127
  let fallos = 0;
104
128
  function comprueba(texto, bien, detalle = '') {
105
- console.log(`${bien ? ' ok ·' : ' FAIL·'} ${texto}${bien || !detalle ? '' : `\n ${detalle}`}`);
129
+ console.log(
130
+ `${bien ? ' ok ·' : ' FAIL·'} ${texto}${bien || !detalle ? '' : `\n ${detalle}`}`,
131
+ );
106
132
  if (!bien) fallos += 1;
107
133
  }
108
134
 
@@ -195,7 +221,11 @@ async function main() {
195
221
  test: boton('.rpgTest'),
196
222
  };
197
223
  })()`);
198
- comprueba('the houses view renders its sheets', (abierto?.fichas ?? 0) > 0, JSON.stringify(abierto));
224
+ comprueba(
225
+ 'the houses view renders its sheets',
226
+ (abierto?.fichas ?? 0) > 0,
227
+ JSON.stringify(abierto),
228
+ );
199
229
  for (const [control, etiqueta] of [
200
230
  ['alta', 'the build-a-house button'],
201
231
  ['instrucciones', 'the CLAUDE.md / AGENTS.md editors'],
@@ -316,8 +346,11 @@ async function main() {
316
346
  })()`);
317
347
  comprueba(
318
348
  '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,
349
+ (casa?.filas ?? 0) > 0 &&
350
+ !!casa?.sube &&
351
+ !!casa?.tomaAqui &&
352
+ casa.dondeDespues !== casa.dondeAntes &&
353
+ (casa?.elegidas ?? 0) > 0,
321
354
  JSON.stringify(casa),
322
355
  );
323
356
  comprueba(
@@ -468,13 +501,16 @@ async function main() {
468
501
  })()`);
469
502
  comprueba(
470
503
  '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,
504
+ reception?.navRole === 'button' &&
505
+ reception?.navTabIndex === '0' &&
506
+ reception?.composers === 1 &&
507
+ reception?.recipients === 30,
473
508
  JSON.stringify(reception),
474
509
  );
475
510
  comprueba(
476
511
  'pending messages stay above composing and use distinct route/reject language',
477
- reception?.messages === 1 && reception?.reviewFirst === true &&
512
+ reception?.messages === 1 &&
513
+ reception?.reviewFirst === true &&
478
514
  /route|dirigir/i.test(reception?.route ?? '') &&
479
515
  /reason|motivo/i.test(reception?.rejection ?? ''),
480
516
  JSON.stringify(reception),
@@ -687,8 +723,11 @@ async function main() {
687
723
  })()`);
688
724
  comprueba(
689
725
  'the language switch really translates the page, and remembers it',
690
- !!lengua && !lengua.falta && lengua.despues !== lengua.antes &&
691
- /casas|resumen|mapa/i.test(lengua.despues) && lengua.guardado === lengua.lang &&
726
+ !!lengua &&
727
+ !lengua.falta &&
728
+ lengua.despues !== lengua.antes &&
729
+ /casas|resumen|mapa/i.test(lengua.despues) &&
730
+ lengua.guardado === lengua.lang &&
692
731
  lengua.etiqueta !== lengua.etiquetaAntes,
693
732
  JSON.stringify(lengua),
694
733
  );
@@ -698,8 +737,14 @@ async function main() {
698
737
  JSON.stringify(lengua?.marca),
699
738
  );
700
739
 
701
- const errores = await cdp.evalua('window.__erroresDePagina ? window.__erroresDePagina.length : 0');
702
- comprueba('the page raised no uncaught errors while we drove it', errores === 0, String(errores));
740
+ const errores = await cdp.evalua(
741
+ 'window.__erroresDePagina ? window.__erroresDePagina.length : 0',
742
+ );
743
+ comprueba(
744
+ 'the page raised no uncaught errors while we drove it',
745
+ errores === 0,
746
+ String(errores),
747
+ );
703
748
  } finally {
704
749
  proceso.kill('SIGKILL');
705
750
  }
@@ -145,9 +145,43 @@ def bundle_al_dia():
145
145
  return "rebuilt from src/hall.ts"
146
146
 
147
147
 
148
+ def un_chrome_que_no_arranca():
149
+ """What the driver says when the browser never answers.
150
+
151
+ This suite has exactly one way to fail without telling anybody anything, and
152
+ CI found it: `chrome never announced its port` names what we stopped waiting
153
+ for and drops the only evidence of why — a missing library, a sandbox
154
+ refusal, a profile it could not write. A stub browser that talks and never
155
+ announces proves the message carries what it heard.
156
+
157
+ CHROME_PATH is the first candidate the driver considers, which is what makes
158
+ this testable without a browser at all.
159
+ """
160
+ print(" and a browser that never starts says why")
161
+ casa = tempfile.mkdtemp()
162
+ falso = os.path.join(casa, "chrome")
163
+ with open(falso, "w", encoding="utf-8") as f:
164
+ f.write("#!/bin/bash\n"
165
+ "echo 'FATAL: could not open display, and this is the useful half' >&2\n"
166
+ "sleep 30\n")
167
+ os.chmod(falso, 0o755)
168
+ salida = subprocess.run(
169
+ ["node", os.path.join(RAIZ, "bin", "navegador.mjs"), "http://127.0.0.1:1/"],
170
+ capture_output=True, text=True, timeout=90,
171
+ env=dict(os.environ, CHROME_PATH=falso, CITY_BROWSER_WAIT_MS="1500"),
172
+ )
173
+ shutil.rmtree(casa, ignore_errors=True)
174
+ todo = salida.stdout + salida.stderr
175
+ afirma("· the timeout names what it waited for",
176
+ "never announced its port" in todo, todo[-300:])
177
+ afirma("· and repeats what the browser actually said",
178
+ "could not open display" in todo, todo[-300:])
179
+
180
+
148
181
  def main():
149
182
  print()
150
183
  print(" the hall, in a browser that clicks")
184
+ un_chrome_que_no_arranca()
151
185
  print(f" · {bundle_al_dia()}")
152
186
  datos = tempfile.mkdtemp()
153
187
  casa = tempfile.mkdtemp()
package/bin/test-seat.py CHANGED
@@ -1521,6 +1521,11 @@ def arranque_escalonado():
1521
1521
  corre(**extra)
1522
1522
  return open(registro).read()
1523
1523
 
1524
+ primera = crudo(CITY_SETTLE="0", CITY_STAGGER="0")
1525
+ afirma(
1526
+ "· happy: a city opened for the first time gets its tmux comforts",
1527
+ "set-option" in primera, primera[:300],
1528
+ )
1524
1529
  log = crudo(FAKE_SESSION="1", FAKE_WINDOWS="seat api", CITY_SETTLE="0", CITY_STAGGER="0")
1525
1530
  afirma(
1526
1531
  "· happy: an agent added to a running city gets its window",
@@ -1542,6 +1547,12 @@ def arranque_escalonado():
1542
1547
  "new-session" not in log,
1543
1548
  log[:600],
1544
1549
  )
1550
+ afirma(
1551
+ "· non-happy: and a running session's tmux options are left exactly as found",
1552
+ "set-option" not in log,
1553
+ "re-applying `set -g mouse on` under a live full-screen app types raw SGR "
1554
+ "sequences into its prompt: " + log[:400],
1555
+ )
1545
1556
  afirma(
1546
1557
  "· the new house is what you are looking at when it opens",
1547
1558
  "select-window" in log and ":docs" in log.split("select-window")[-1][:40],
@@ -1,8 +1,8 @@
1
- var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t=_e(e);return $e.set(e,t),t}function _e(e){return e.replace(/[\u2018\u2019]/g,"'").replace(/\s+/g," ").trim()}function j(e){for(let[n,t]of Object.entries(e))ke[Ee(n)]=t}var q="";function D(){if(q==="es"||q==="en")return q;try{let n=localStorage.getItem("hall-idioma");if(n==="es"||n==="en")return q=n,q}catch{}return q=(navigator.language||"en").toLowerCase().startsWith("es")?"es":"en",q}function Te(e){q=e;try{localStorage.setItem("hall-idioma",e)}catch{}document.documentElement.lang=e}function s(e,n){let t=D()==="es"?ke[Ee(e)]??e:e;return n?t.replace(/\{(\w+)\}/g,(a,i)=>n[i]===void 0?`{${i}}`:String(n[i])):t}function x(e,n,t){return s(e===1?n:t,{n:String(e)})}j({Overview:"Resumen","The map":"El mapa","My seat":"Mi asiento",Districts:"Distritos",Roads:"Carreteras",Reception:"Recepci\xF3n",Committee:"Comit\xE9",Houses:"Casas","All cities":"Todas las ciudades","you are in":"est\xE1s en","{n} house":"{n} casa","{n} houses":"{n} casas","Create another city \u2192":"Crear otra ciudad \u2192",reception:"recepci\xF3n","Messages wait for you, not your agents":"Los mensajes te esperan a ti, no a tus agentes","Remote text stops here as inert text. Read it, reject it with a reason, or choose the cities that should receive it. Until then no model can read it.":"El texto remoto se detiene aqu\xED como texto inerte. L\xE9elo, rech\xE1zalo con un motivo o elige qu\xE9 ciudades deben recibirlo. Hasta entonces ning\xFAn modelo puede leerlo.","routing mode":"modo de enrutado","Manual review":"Revisi\xF3n manual","Every message needs a person before it reaches a city.":"Cada mensaje necesita a una persona antes de llegar a una ciudad.","Auto router not configured":"Router autom\xE1tico sin configurar","Configure Auto":"Configurar Auto","Automatic routing":"Enrutado autom\xE1tico","Only one clear, low-risk rule match can leave the human queue automatically.":"Solo una coincidencia clara y de bajo riesgo puede salir autom\xE1ticamente de la cola humana.","Routing policy":"Pol\xEDtica de enrutado","Keep human review":"Mantener la revisi\xF3n humana","Use the local rule router":"Usar el router local de reglas","Write comma-separated words or phrases for each destination. Empty cities are never selected.":"Escribe palabras o frases separadas por comas para cada destino. Las ciudades vac\xEDas nunca se eligen.","e.g. contract, privacy, legal review":"p. ej. contrato, privacidad, revisi\xF3n legal","Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.":"El texto sospechoso, sin coincidencia o ambiguo siempre te espera. Auto nunca ejecuta, responde ni abre enlaces.","Save routing policy":"Guardar pol\xEDtica de enrutado","Could not save the routing policy":"No se pudo guardar la pol\xEDtica de enrutado","Routing policy saved.":"Pol\xEDtica de enrutado guardada.","Reading your reception\u2026":"Leyendo tu recepci\xF3n\u2026","Could not read your reception":"No se ha podido leer tu recepci\xF3n","Reception clear":"Recepci\xF3n despejada","No remote message is waiting for a decision.":"No hay mensajes remotos esperando una decisi\xF3n.","message waiting":"{n} mensaje en espera","messages waiting":"{n} mensajes en espera","local only":"solo local","No agent has read this":"Ning\xFAn agente ha le\xEDdo esto","Send to":"Dirigir a","Route to selected cities":"Dirigir a las ciudades elegidas","Reject and reply with a reason":"Rechazar y responder con un motivo","Reason sent back securely":"Motivo que se devolver\xE1 cifrado","Reason for rejecting this message":"Motivo para rechazar este mensaje","Reject and send reason":"Rechazar y enviar el motivo","Could not route the message":"No se pudo dirigir el mensaje","Message routed. Only the selected cities can now read it.":"Mensaje dirigido. Solo las ciudades elegidas pueden leerlo ahora.","Could not reject the message":"No se pudo rechazar el mensaje","Message rejected. Your reason is queued for encrypted delivery.":"Mensaje rechazado. Tu motivo ha quedado en cola para enviarse cifrado.","New encrypted message":"Nuevo mensaje cifrado","Messages leave from this computer with end-to-end encryption.":"Los mensajes salen de este ordenador con cifrado de extremo a extremo.","message queued":"{n} mensaje en cola","messages queued":"{n} mensajes en cola","Write to":"Escribir a",Message:"Mensaje","Your message will stop in their private reception.":"Tu mensaje se detendr\xE1 en su recepci\xF3n privada.","Send securely":"Enviar de forma segura","Reply to a rejected message":"Respuesta a un mensaje rechazado",Dismiss:"Descartar","Could not queue the message":"No se pudo poner el mensaje en cola","Message queued on this computer.":"Mensaje puesto en cola en este ordenador.","Could not dismiss the response":"No se pudo descartar la respuesta","City live":"Ciudad en directo",waiting:"esperando","A moderated conversation over the WebSocket bus. The seat routes each turn; private model reasoning is never shown or stored.":"Una conversaci\xF3n moderada sobre el bus WebSocket. El asiento dirige cada turno; el razonamiento privado de los modelos no se muestra ni se guarda.","all conversations":"todas las conversaciones","show work":"mostrar trabajo","hide work":"ocultar trabajo","websocket live":"websocket activo",reconnecting:"reconectando","session offline":"sesi\xF3n desconectada","The bus is live. Questions, positions and moderated replies will appear here.":"El bus est\xE1 activo. Aqu\xED aparecer\xE1n preguntas, posiciones y respuestas moderadas.","Start the city session. Its visible conversation will appear here as it happens.":"Inicia la sesi\xF3n de la ciudad. Su conversaci\xF3n visible aparecer\xE1 aqu\xED mientras sucede.",Welcome:"Bienvenida","The work":"El trabajo","Your chair":"Tu silla","The houses":"Las casas",Ready:"Listo","A city is you, and the houses around you":"Una ciudad eres t\xFA y las casas que te rodean","Let\u2019s build it":"Vamos a construirla","I\u2019ll set it up myself":"Ya lo configuro yo","What kind of work happens here?":"\xBFQu\xE9 clase de trabajo se hace aqu\xED?","And what are you, here?":"\xBFY t\xFA qu\xE9 eres aqu\xED?","Who lives in your city?":"\xBFQui\xE9n vive en tu ciudad?","Who else lives here?":"\xBFQui\xE9n m\xE1s vive aqu\xED?","Build the first house":"Construye la primera casa","Add another house":"A\xF1ade otra casa","That is everyone":"Ya est\xE1n todos","Skip \u2014 just me for now":"S\xE1ltalo \u2014 de momento solo yo","a new house":"una casa nueva","Who lives in it?":"\xBFQui\xE9n vive en ella?","What do you call it?":"\xBFC\xF3mo la llamas?","What kind of work does it do?":"\xBFQu\xE9 clase de trabajo hace?","It writes code":"Escribe c\xF3digo","It keeps knowledge":"Guarda conocimiento","It coordinates":"Coordina","What does it work on?":"\xBFSobre qu\xE9 trabaja?","Build it":"Constr\xFAyela","building\u2026":"construyendo\u2026",Cancel:"Cancelar",Back:"Atr\xE1s",Next:"Siguiente","question 1 of 4":"pregunta 1 de 4","question 2 of 4":"pregunta 2 de 4","question 3 of 4":"pregunta 3 de 4","Open my session":"Abrir mi sesi\xF3n","See the houses":"Ver las casas","Draw the map":"Dibujar el mapa","Set a goal":"Poner un objetivo","{city} is alive":"{city} est\xE1 viva","Who lives in {city}":"Qui\xE9n vive en {city}","+ Build a house":"+ Construir una casa","+ folder":"+ carpeta","+ zip":"+ zip",test:"probar",engine:"motor",effort:"esfuerzo",provider:"proveedor",growth:"crecimiento","works on":"trabaja sobre",skills:"skills","nothing mounted yet":"a\xFAn sin nada montado","none discovered":"ninguna descubierta",connected:"conectado",idle:"inactivo",missing:"no instalado",default:"por defecto","custom\u2026":"otro\u2026",cities:"ciudades","Your cities":"Tus ciudades","Start another city":"Empezar otra ciudad","Create it":"Crearla",open:"abrir",archive:"archivar","\xB7 open now":"\xB7 abierta ahora","Start this city over":"Empezar esta ciudad de cero","Start over\u2026":"Empezar de cero\u2026","the map":"el mapa","Your city, drawn":"Tu ciudad, dibujada","Draw my city":"Dibujar mi ciudad","Draw it anyway":"Dibujarla igualmente","baking the map \u2014 first time takes a minute":"horneando el mapa \u2014 la primera vez tarda un minuto",Saved:"Guardado",Copied:"Copiado","reading your city":"leyendo tu ciudad","Could not save":"No se pudo guardar"});j({"town hall":"ayuntamiento","Manage one autonomous city: its seat, repo agents, roads and map. Everything here is a plain file you can also inspect by hand.":"Gobierna una ciudad aut\xF3noma: su asiento, sus agentes, sus carreteras y su mapa. Todo lo que hay aqu\xED es un fichero de texto que tambi\xE9n puedes abrir a mano.","Its role \u2014 its speciality, never authority":"Su rol \u2014 su especialidad, nunca su autoridad","What runs it?":"\xBFQu\xE9 lo mueve?"});j({"You take the chair. Around it, one <b>house</b> per worker \u2014 an <b>agent</b> \u2014 each with its own window, its own role and its own corner of your disk: a repository, three of them, a folder of documents, whatever it actually works on. The map draws them as houses that grow with the work done in them, and many houses are a city. They never talk to each other behind your back: you chair, they answer.":"T\xFA presides. A tu alrededor, una <b>casa</b> por trabajador \u2014 un <b>agente</b> \u2014 cada una con su ventana, su rol y su rinc\xF3n de tu disco: un repositorio, tres, una carpeta de documentos, lo que de verdad tenga entre manos. El mapa las dibuja como casas que crecen con el trabajo hecho dentro, y muchas casas son una ciudad. Nunca hablan entre ellas a tus espaldas: t\xFA presides, ellas responden.","Everything lives as plain files in {donde}. Nothing leaves this machine, there is no account, and you can edit any of it by hand afterwards.":"Todo vive como ficheros de texto en {donde}. Nada sale de esta m\xE1quina, no hay cuenta que crear, y luego puedes editarlo a mano.","<b>Four questions and you are working.</b> You can skip any of them and change everything later.":"<b>Cuatro preguntas y a trabajar.</b> Puedes saltarte cualquiera y cambiarlo todo despu\xE9s.","It decides the vocabulary, the roles you will be offered and what counts as evidence in a decision. A clinic does not ship pull requests, and a law firm does not measure story points. Pick the closest one \u2014 you can change it later.":"Decide el vocabulario, los roles que se te ofrecen y qu\xE9 cuenta como prueba en una decisi\xF3n. Una cl\xEDnica no entrega pull requests, y un despacho no mide story points. Elige el m\xE1s cercano \u2014 luego se cambia.","You chair this city whatever you answer \u2014 this is your <b>speciality</b>, not your authority. It shapes the perspective you bring to a decision and the knowledge files the city writes for you. <b>Blank</b> is a real answer: it means no preset knowledge.":"Presides esta ciudad respondas lo que respondas \u2014 esto es tu <b>especialidad</b>, no tu autoridad. Da forma a la perspectiva con la que entras en una decisi\xF3n y a los ficheros de conocimiento que la ciudad escribe para ti. <b>En blanco</b> es una respuesta de verdad: significa sin conocimiento previo.","Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window, its own role and its own corner of your disk. A house is <b>not</b> a repository: one can answer for three services and a folder of documents at once, and a house whose work is documents needs no git anywhere.":"Cada casa aloja a un trabajador \u2014 un <b>agente</b> \u2014 con su ventana, su rol y su rinc\xF3n de tu disco. Una casa <b>no</b> es un repositorio: una sola puede responder por tres servicios y una carpeta de documentos a la vez, y una casa cuyo trabajo son documentos no necesita git en ninguna parte.","Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.":"Tu asiento queda escrito y {cuantas}. Todo lo que acabas de responder es un fichero de texto que puedes leer y editar.","<b>{n}</b> house stands around it":"a su alrededor hay <b>{n}</b> casa","<b>{n}</b> houses stand around it":"a su alrededor hay <b>{n}</b> casas","the city is waiting for its first house":"la ciudad espera su primera casa","What people usually do next:":"Lo que suele hacerse ahora:","A goal is optional, and the city works without one \u2014 but a round with no goal is a status report, because there is nothing to argue against.":"El objetivo es opcional y la ciudad funciona sin \xE9l \u2014 pero una ronda sin objetivo es un parte de estado, porque no hay nada contra lo que discutir.","Its house grows with the pull requests it merges. Mounts repositories and worktrees.":"Su casa crece con las pull requests que fusiona. Monta repositorios y worktrees.","Its house grows with the documents it writes. Needs no git at all.":"Su casa crece con los documentos que escribe. No necesita git para nada.","Its house grows with the decisions it records.":"Su casa crece con las decisiones que deja escritas.","urgencias, api, the handbook \u2014 whatever you would say out loud":"urgencias, api, el manual \u2014 como lo llamar\xEDas en voz alta"});j({Yes:"S\xED","What is it called?":"\xBFC\xF3mo se llama?","home, clients, the lab \u2014 whatever you would say out loud":"casa, clientes, el laboratorio \u2014 como lo dir\xEDas en voz alta","A city is a place with its own seat, its own houses and its own map. Yours stay where they are.":"Una ciudad es un sitio con su propio asiento, sus casas y su mapa. Las que ya tienes se quedan donde est\xE1n.","Start over {city}?":"\xBFEmpezar {city} de cero?","Start over":"Empezar de cero","You lose:":"Pierdes:","You keep:":"Conservas:","Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).":"Ahora mismo eso son {agents} casa(s), {roads} carretera(s) y {acts} acta(s) de comit\xE9.","Type the city\u2019s name to confirm":"Escribe el nombre de la ciudad para confirmar","Archive {city}?":"\xBFArchivar {city}?","Archive it":"Archivarla","It moves into .backups. Nothing is deleted, and you can put it back by hand.":"Se mueve a .backups. No se borra nada, y puedes devolverla a mano.","Which engine?":"\xBFQu\xE9 motor?","Model alias":"Alias del modelo","An alias the CLI resolves when the window opens. Anything it accepts works here.":"Un alias que la CLI resuelve al abrir la ventana. Vale cualquiera que ella acepte.","Use it":"Usarlo","Remove the skill {skill}?":"\xBFQuitar la skill {skill}?","Remove it":"Quitarla","It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.":"Se borra de la casa de este agente. Nada fuera de esa carpeta se toca.","Build a house":"Construir una casa","One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.":"Un trabajador, su ventana y su rinc\xF3n de tu disco. Todo esto se puede cambiar despu\xE9s.","{name} has a house now":"{name} ya tiene casa","{name} joined the city":"{name} se ha mudado a la ciudad","What else does it work on?":"\xBFSobre qu\xE9 m\xE1s trabaja?","Mount it":"Montarlo",Mounted:"Montado","Stop this agent working on {what}?":"\xBFQue este agente deje de trabajar sobre {what}?","Unmount it":"Desmontarlo","The link goes. The folder it points at stays exactly where it is.":"Se va el enlace. La carpeta a la que apunta se queda exactamente donde est\xE1.","Give it a name \u2014 it is how you will call it in its window":"Ponle un nombre \u2014 es como la llamar\xE1s en su ventana","Could not add that agent":"No se pudo a\xF1adir ese agente","could not mount":"no se pudo montar","Could not set its engine":"No se pudo fijar su motor","{n} mount":"{n} montaje","{n} mounts":"{n} montajes"});j({"Walk your disk and pick whatever you like: a repository, a worktree, a folder of documents, one exact file. As many as you want. Nothing is copied \u2014 each one is linked into this agent\u2019s own home.":"Recorre tu disco y coge lo que quieras: un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Tantos como quieras. No se copia nada \u2014 cada uno se enlaza dentro de la casa de este agente.","Working on":"Trabaja sobre","Nothing chosen yet \u2014 an agent with no mounts is fine too.":"A\xFAn no has cogido nada \u2014 un agente sin montajes tambi\xE9n vale.","Add this folder":"A\xF1adir esta carpeta","Add this":"A\xF1adir esto","Already chosen":"Ya est\xE1 cogido","Up one folder":"Subir una carpeta","reading that folder":"leyendo esa carpeta","This folder is empty.":"Esta carpeta est\xE1 vac\xEDa.","Only the first 2000 shown.":"S\xF3lo se muestran los 2000 primeros.","A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.":"Un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Se enlaza, nunca se copia."});j({Demos:"Demos",demos:"demos","See a committee happen":"Mira un comit\xE9 de verdad","reading the demo shelf":"leyendo la estanter\xEDa de demos","A real question, answered by a city of agents that never talk to each other behind the chair\u2019s back. Pick the field closest to yours \u2014 the machine is the same one in all three; only the work changes.":"Una pregunta de verdad, respondida por una ciudad de agentes que nunca hablan entre ellos a espaldas de quien preside. Elige el campo m\xE1s cercano al tuyo \u2014 la maquinaria es la misma en los tres; lo que cambia es el trabajo.","{n} turns":"{n} turnos","These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.":"Son grabaciones de ejecuciones reales sobre el bus real, reproducidas aqu\xED. Para lanzar una en vivo desde terminal: agents-city demo --domain software.","All demos":"Todas las demos",Play:"Reproducir",Pause:"Pausa",Replay:"Repetir","{done} of {total}":"{done} de {total}","Press play. The turns arrive one by one, exactly as they did.":"Dale a reproducir. Los turnos van llegando uno a uno, tal cual llegaron.","A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.":"Una grabaci\xF3n de una ejecuci\xF3n real: estos eventos salieron del bus real, del comit\xE9 real. Aqu\xED no se est\xE1 decidiendo nada ahora mismo.","Nothing recorded here":"Aqu\xED no hay nada grabado","This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.":"Esta instalaci\xF3n no tiene grabaciones. Cr\xE9alas con demo/graba.py, o lanza la cosa entera desde terminal con agents-city demo."});j({"{n} folder":"{n} carpeta","{n} folders":"{n} carpetas","{n} file":"{n} fichero","{n} files":"{n} ficheros","files are below the folders":"los ficheros van debajo de las carpetas"});j({"Leave all three on default and it runs the way you do. Whatever you set here is written once on the card, and the launcher hands it to whichever CLI runs this house.":"D\xE9jalos los tres por defecto y funcionar\xE1 como t\xFA. Lo que pongas aqu\xED se escribe una sola vez en la ficha, y el lanzador se lo pasa a la CLI que mueva esta casa.","This CLI has no effort setting.":"Esta CLI no tiene ajuste de esfuerzo.","An alias the Claude CLI resolves when the window opens.":"Un alias que la CLI de Claude resuelve al abrir la ventana.","The model name your Codex uses \u2014 the one in ~/.codex/config.toml.":"El nombre del modelo que usa tu Codex \u2014 el de ~/.codex/config.toml.","OpenCode names a model provider/model, like anthropic/claude-sonnet-4.":"OpenCode nombra los modelos proveedor/modelo, como anthropic/claude-sonnet-4.","The model name your Kimi CLI uses.":"El nombre del modelo que usa tu CLI de Kimi."});j({"my seat":"mi asiento","Save my seat":"Guardar mi asiento","work domain":"dominio de trabajo","your day":"tu d\xEDa","the agents":"los agentes","repo agents":"agentes de repo","one goal \u2014 optional":"un objetivo \u2014 opcional","The goal, in one line":"El objetivo, en una l\xEDnea","Concrete enough to argue with \u2014 empty skips it":"Lo bastante concreto como para discutirlo \u2014 vac\xEDo lo salta","How it is measured":"C\xF3mo se mide","in prose: the architect reads the AGENTS.md files on Fridays":"en prosa: el arquitecto lee los AGENTS.md los viernes","The command that returns it, if a command can":"El comando que lo devuelve, si un comando puede","What it returns today":"Lo que devuelve hoy","Where it has to get to":"A d\xF3nde tiene que llegar","By when":"Para cu\xE1ndo","empty for a qualitative goal":"vac\xEDo para un objetivo cualitativo","\u2026or who judges it, and how often":"\u2026o qui\xE9n lo juzga, y cada cu\xE1nto","what is left":"lo que queda","open my session":"abrir mi sesi\xF3n","Open the roster":"Abrir la lista","districts & houses":"distritos y casas","Save districts & houses":"Guardar distritos y casas","another district\u2026":"otro distrito\u2026","The modelling no tool can do for you":"El modelado que ninguna herramienta puede hacer por ti","A house is <b>not a repo</b> \u2014 it is a parcel, a slice of one serving a single business unit. Split the interesting repos, give every house its district, and the map can say \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.":"Una casa <b>no es un repo</b> \u2014 es una parcela, una porci\xF3n de uno al servicio de una sola unidad de negocio. Parte los repos interesantes, dale a cada casa su distrito, y el mapa podr\xE1 decir \xABeste cambio toca banca\xBB en vez de \xABeste cambio toca src/lib\xBB.","Cities this one may reach":"Ciudades a las que \xE9sta puede llegar","No roads yet. This city is isolated on purpose.":"A\xFAn no hay carreteras. Esta ciudad est\xE1 aislada a prop\xF3sito.","No unconnected local cities.":"No hay ciudades locales sin conectar.","other cities on this machine":"otras ciudades en esta m\xE1quina","remote invitation \xB7 public, no token":"invitaci\xF3n remota \xB7 p\xFAblica, sin token","committee acts":"actas del comit\xE9","Decisions with a visible chain of custody":"Decisiones con una cadena de custodia visible","No committee acts yet. Open one only when the seat needs specialised evidence.":"A\xFAn no hay actas. Abre un comit\xE9 s\xF3lo cuando el asiento necesite pruebas especializadas.","agents-city committee schema open":"agents-city committee schema open","guided committee":"comit\xE9 guiado","Play the guided committee from the top":"Reproducir el comit\xE9 guiado desde el principio","Pause or resume mid-scene":"Pausar o seguir a mitad de escena","\u27F3 replay":"\u27F3 repetir","\u23F8 pause":"\u23F8 pausa","seat moderates":"el asiento modera","read full message":"leer el mensaje entero",'One person, several autonomous cities: each has its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that city\u2019s folder \u2014 so two cities can each have a <code class="mono">docs</code> house and they are two different workers. They share nothing unless you build a road between them.':'Una persona, varias ciudades aut\xF3nomas: cada una con su identidad, su dominio, su silla, <b>sus propias casas</b> y sus carreteras. Una casa no se comparte \u2014 est\xE1 dentro de la ciudad que la posee, con su espacio de trabajo y sus montajes bajo la carpeta de esa ciudad \u2014 as\xED que dos ciudades pueden tener cada una su casa <code class="mono">docs</code> y son dos trabajadores distintos. No comparten nada salvo que construyas una carretera entre ellas.',"client-a, research, the book\u2026":"cliente-a, investigaci\xF3n, el libro\u2026","which city this hall manages":"qu\xE9 ciudad gobierna este ayuntamiento","Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> \u2014 only the city that points at them.":"Devuelve {city} a su primer d\xEDa: sin asiento, sin agentes, sin historial de comit\xE9, sin mapa. Tus repositorios y carpetas de documentos <b>no se tocan</b> \u2014 s\xF3lo la ciudad que apunta a ellos.","Agents &amp; skills":"Agentes y skills",'<b>A house is where an agent lives and works</b>, and many houses are a city \u2014 it is the same thing the map draws, growing with what that agent actually does. The card and the CLI call them <code class="mono">agents</code>; here you see their houses.':'<b>Una casa es donde vive y trabaja un agente</b>, y muchas casas son una ciudad \u2014 es lo mismo que dibuja el mapa, creciendo con lo que ese agente hace de verdad. La ficha y la CLI los llaman <code class="mono">agents</code>; aqu\xED ves sus casas.',"They belong to <b>this</b> city and only to it: each one\u2019s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.":"Pertenecen a <b>esta</b> ciudad y s\xF3lo a ella: el espacio de trabajo de cada una y sus montajes viven dentro de {donde}, as\xED que otra ciudad tiene su propia gente aunque les pongas los mismos nombres.","a legacy repo agent works on its own repo":"un agente de repo heredado trabaja sobre su propio repo","data repo":"repo de datos","skills recognised":"skills reconocidas","plugin installed":"plugin instalado","Reroll this agent\u2019s face \u2014 deterministic, persisted on the card":"Vuelve a tirar la cara de este agente \u2014 determinista, guardada en la ficha","Instructions the Claude runtime reads":"Instrucciones que lee el runtime de Claude","Instructions Codex, OpenCode and Kimi read":"Instrucciones que leen Codex, OpenCode y Kimi","Run the engine for real: --version, and the login state on Claude":"Ejecuta el motor de verdad: --version, y el estado de sesi\xF3n en Claude","Install a skill zip into this agent\u2019s own home \u2014 the Claude runtime reads skills; other engines ignore them":"Instala un zip de skill en la casa de este agente \u2014 el runtime de Claude lee skills; los dem\xE1s motores las ignoran","Remove this skill from the agent\u2019s home":"Quitar esta skill de la casa del agente","reading the domain packs":"leyendo los paquetes de dominio","reading the role files":"leyendo los ficheros de rol","an isometric house":"una casa isom\xE9trica"});var H=class{host=null;monta(n){this.host=n,this.dibuja()}dibuja(){this.host&&(this.host.innerHTML=this.html(),this.enlaza(this.host))}repinta(){this.dibuja()}};var Ue={repo:"git",worktree:"wt"},B=class extends H{constructor(t,a,i){super();this.p=t;this.elige=a;this.yaElegidas=i}aqui="";listado=null;cargando=!1;fallo="";monta(t){super.monta(t),!this.listado&&!this.cargando&&this.ve(this.aqui||"~")}async ve(t){this.cargando=!0,this.fallo="",this.listado||this.repinta();try{let a=await this.p.api("/api/carpeta?path="+encodeURIComponent(t));a.error?this.fallo=a.error:(this.listado=a,this.aqui=a.ruta)}catch(a){this.fallo=String(a)}finally{this.cargando=!1,this.repinta()}}migas(){let t=this.p.esc,a=this.listado;if(!a)return"";let i=a.ruta.split("/").filter(Boolean),c="";return'<button type="button" class="expMiga" data-exp="ve" data-ruta="/">/</button>'+i.map(r=>(c+="/"+r,`<button type="button" class="expMiga" data-exp="ve" data-ruta="${t(c)}">${t(r)}</button>`)).join("<i>/</i>")}html(){let t=this.p.esc,a=this.listado,i=new Set(this.yaElegidas()),c=s("Already chosen"),o=s("Add this"),r=(a?.entradas??[]).map(u=>{let h=i.has(u.ruta);return`<div class="expFila ${u.dir?"dir":"fich"} ${h?"puesta":""}">
1
+ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t=_e(e);return $e.set(e,t),t}function _e(e){return e.replace(/[\u2018\u2019]/g,"'").replace(/\s+/g," ").trim()}function A(e){for(let[n,t]of Object.entries(e))ke[Ee(n)]=t}var R="";function D(){if(R==="es"||R==="en")return R;try{let n=localStorage.getItem("hall-idioma");if(n==="es"||n==="en")return R=n,R}catch{}return R=(navigator.language||"en").toLowerCase().startsWith("es")?"es":"en",R}function Te(e){R=e;try{localStorage.setItem("hall-idioma",e)}catch{}document.documentElement.lang=e}function s(e,n){let t=D()==="es"?ke[Ee(e)]??e:e;return n?t.replace(/\{(\w+)\}/g,(a,i)=>n[i]===void 0?`{${i}}`:String(n[i])):t}function x(e,n,t){return s(e===1?n:t,{n:String(e)})}A({Overview:"Resumen","The map":"El mapa","My seat":"Mi asiento",Districts:"Distritos",Roads:"Carreteras",Reception:"Recepci\xF3n",Committee:"Comit\xE9",Houses:"Casas","All cities":"Todas las ciudades","you are in":"est\xE1s en","{n} house":"{n} casa","{n} houses":"{n} casas","Create another city \u2192":"Crear otra ciudad \u2192",reception:"recepci\xF3n","Messages wait for you, not your agents":"Los mensajes te esperan a ti, no a tus agentes","Remote text stops here as inert text. Read it, reject it with a reason, or choose the cities that should receive it. Until then no model can read it.":"El texto remoto se detiene aqu\xED como texto inerte. L\xE9elo, rech\xE1zalo con un motivo o elige qu\xE9 ciudades deben recibirlo. Hasta entonces ning\xFAn modelo puede leerlo.","routing mode":"modo de enrutado","Manual review":"Revisi\xF3n manual","Every message needs a person before it reaches a city.":"Cada mensaje necesita a una persona antes de llegar a una ciudad.","Auto router not configured":"Router autom\xE1tico sin configurar","Configure Auto":"Configurar Auto","Automatic routing":"Enrutado autom\xE1tico","Only one clear, low-risk rule match can leave the human queue automatically.":"Solo una coincidencia clara y de bajo riesgo puede salir autom\xE1ticamente de la cola humana.","Routing policy":"Pol\xEDtica de enrutado","Keep human review":"Mantener la revisi\xF3n humana","Use the local rule router":"Usar el router local de reglas","Write comma-separated words or phrases for each destination. Empty cities are never selected.":"Escribe palabras o frases separadas por comas para cada destino. Las ciudades vac\xEDas nunca se eligen.","e.g. contract, privacy, legal review":"p. ej. contrato, privacidad, revisi\xF3n legal","Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.":"El texto sospechoso, sin coincidencia o ambiguo siempre te espera. Auto nunca ejecuta, responde ni abre enlaces.","Save routing policy":"Guardar pol\xEDtica de enrutado","Could not save the routing policy":"No se pudo guardar la pol\xEDtica de enrutado","Routing policy saved.":"Pol\xEDtica de enrutado guardada.","Reading your reception\u2026":"Leyendo tu recepci\xF3n\u2026","Could not read your reception":"No se ha podido leer tu recepci\xF3n","Reception clear":"Recepci\xF3n despejada","No remote message is waiting for a decision.":"No hay mensajes remotos esperando una decisi\xF3n.","message waiting":"{n} mensaje en espera","messages waiting":"{n} mensajes en espera","local only":"solo local","No agent has read this":"Ning\xFAn agente ha le\xEDdo esto","Send to":"Dirigir a","Route to selected cities":"Dirigir a las ciudades elegidas","Reject and reply with a reason":"Rechazar y responder con un motivo","Reason sent back securely":"Motivo que se devolver\xE1 cifrado","Reason for rejecting this message":"Motivo para rechazar este mensaje","Reject and send reason":"Rechazar y enviar el motivo","Could not route the message":"No se pudo dirigir el mensaje","Message routed. Only the selected cities can now read it.":"Mensaje dirigido. Solo las ciudades elegidas pueden leerlo ahora.","Could not reject the message":"No se pudo rechazar el mensaje","Message rejected. Your reason is queued for encrypted delivery.":"Mensaje rechazado. Tu motivo ha quedado en cola para enviarse cifrado.","New encrypted message":"Nuevo mensaje cifrado","Messages leave from this computer with end-to-end encryption.":"Los mensajes salen de este ordenador con cifrado de extremo a extremo.","message queued":"{n} mensaje en cola","messages queued":"{n} mensajes en cola","Write to":"Escribir a",Message:"Mensaje","Your message will stop in their private reception.":"Tu mensaje se detendr\xE1 en su recepci\xF3n privada.","Send securely":"Enviar de forma segura","Reply to a rejected message":"Respuesta a un mensaje rechazado",Dismiss:"Descartar","Could not queue the message":"No se pudo poner el mensaje en cola","Message queued on this computer.":"Mensaje puesto en cola en este ordenador.","Could not dismiss the response":"No se pudo descartar la respuesta","City live":"Ciudad en directo",waiting:"esperando","A moderated conversation over the WebSocket bus. The seat routes each turn; private model reasoning is never shown or stored.":"Una conversaci\xF3n moderada sobre el bus WebSocket. El asiento dirige cada turno; el razonamiento privado de los modelos no se muestra ni se guarda.","all conversations":"todas las conversaciones","show work":"mostrar trabajo","hide work":"ocultar trabajo","websocket live":"websocket activo",reconnecting:"reconectando","session offline":"sesi\xF3n desconectada","The bus is live. Questions, positions and moderated replies will appear here.":"El bus est\xE1 activo. Aqu\xED aparecer\xE1n preguntas, posiciones y respuestas moderadas.","Start the city session. Its visible conversation will appear here as it happens.":"Inicia la sesi\xF3n de la ciudad. Su conversaci\xF3n visible aparecer\xE1 aqu\xED mientras sucede.",Welcome:"Bienvenida","The work":"El trabajo","Your chair":"Tu silla","The houses":"Las casas",Ready:"Listo","A city is you, and the houses around you":"Una ciudad eres t\xFA y las casas que te rodean","Let\u2019s build it":"Vamos a construirla","I\u2019ll set it up myself":"Ya lo configuro yo","What kind of work happens here?":"\xBFQu\xE9 clase de trabajo se hace aqu\xED?","And what are you, here?":"\xBFY t\xFA qu\xE9 eres aqu\xED?","Who lives in your city?":"\xBFQui\xE9n vive en tu ciudad?","Who else lives here?":"\xBFQui\xE9n m\xE1s vive aqu\xED?","Build the first house":"Construye la primera casa","Add another house":"A\xF1ade otra casa","That is everyone":"Ya est\xE1n todos","Nobody yet \u2014 I answer alone":"Nadie todav\xEDa \u2014 contesto yo solo","A city with no houses does not delegate: the seat answers, and it is the only one who can. That is a real choice for a role whose work is other people's cities rather than folders \u2014 and it is not the usual one. Houses can be added later, from here or with <b>agents-city seat --agents</b>.":"Una ciudad sin casas no delega: contesta el asiento, y es el \xFAnico que puede. Es una elecci\xF3n de verdad para un rol cuyo trabajo son las ciudades de otros y no las carpetas \u2014 y no es la habitual. Las casas se pueden a\xF1adir luego, desde aqu\xED o con <b>agents-city seat --agents</b>.","a new house":"una casa nueva","Who lives in it?":"\xBFQui\xE9n vive en ella?","What do you call it?":"\xBFC\xF3mo la llamas?","What kind of work does it do?":"\xBFQu\xE9 clase de trabajo hace?","It writes code":"Escribe c\xF3digo","It keeps knowledge":"Guarda conocimiento","It coordinates":"Coordina","What does it work on?":"\xBFSobre qu\xE9 trabaja?","Build it":"Constr\xFAyela","building\u2026":"construyendo\u2026",Cancel:"Cancelar",Back:"Atr\xE1s",Next:"Siguiente","question 1 of 4":"pregunta 1 de 4","question 2 of 4":"pregunta 2 de 4","question 3 of 4":"pregunta 3 de 4","Open my session":"Abrir mi sesi\xF3n","See the houses":"Ver las casas","Draw the map":"Dibujar el mapa","Set a goal":"Poner un objetivo","{city} is alive":"{city} est\xE1 viva","Who lives in {city}":"Qui\xE9n vive en {city}","+ Build a house":"+ Construir una casa","+ folder":"+ carpeta","+ zip":"+ zip",test:"probar",engine:"motor",effort:"esfuerzo",provider:"proveedor",growth:"crecimiento","works on":"trabaja sobre",skills:"skills","nothing mounted yet":"a\xFAn sin nada montado","none discovered":"ninguna descubierta",connected:"conectado",idle:"inactivo",missing:"no instalado",default:"por defecto","custom\u2026":"otro\u2026",cities:"ciudades","Your cities":"Tus ciudades","Start another city":"Empezar otra ciudad","Create it":"Crearla",open:"abrir",archive:"archivar","\xB7 open now":"\xB7 abierta ahora","Start this city over":"Empezar esta ciudad de cero","Start over\u2026":"Empezar de cero\u2026","the map":"el mapa","Your city, drawn":"Tu ciudad, dibujada","Draw my city":"Dibujar mi ciudad","Draw it anyway":"Dibujarla igualmente","baking the map \u2014 first time takes a minute":"horneando el mapa \u2014 la primera vez tarda un minuto",Saved:"Guardado",Copied:"Copiado","reading your city":"leyendo tu ciudad","Could not save":"No se pudo guardar"});A({"town hall":"ayuntamiento","Manage one autonomous city: its seat, repo agents, roads and map. Everything here is a plain file you can also inspect by hand.":"Gobierna una ciudad aut\xF3noma: su asiento, sus agentes, sus carreteras y su mapa. Todo lo que hay aqu\xED es un fichero de texto que tambi\xE9n puedes abrir a mano.","Its role \u2014 its speciality, never authority":"Su rol \u2014 su especialidad, nunca su autoridad","What runs it?":"\xBFQu\xE9 lo mueve?"});A({"You take the chair. Around it, one <b>house</b> per worker \u2014 an <b>agent</b> \u2014 each with its own window, its own role and its own corner of your disk: a repository, three of them, a folder of documents, whatever it actually works on. The map draws them as houses that grow with the work done in them, and many houses are a city. They never talk to each other behind your back: you chair, they answer.":"T\xFA presides. A tu alrededor, una <b>casa</b> por trabajador \u2014 un <b>agente</b> \u2014 cada una con su ventana, su rol y su rinc\xF3n de tu disco: un repositorio, tres, una carpeta de documentos, lo que de verdad tenga entre manos. El mapa las dibuja como casas que crecen con el trabajo hecho dentro, y muchas casas son una ciudad. Nunca hablan entre ellas a tus espaldas: t\xFA presides, ellas responden.","Everything lives as plain files in {donde}. Nothing leaves this machine, there is no account, and you can edit any of it by hand afterwards.":"Todo vive como ficheros de texto en {donde}. Nada sale de esta m\xE1quina, no hay cuenta que crear, y luego puedes editarlo a mano.","<b>Four questions and you are working.</b> You can skip any of them and change everything later.":"<b>Cuatro preguntas y a trabajar.</b> Puedes saltarte cualquiera y cambiarlo todo despu\xE9s.","It decides the vocabulary, the roles you will be offered and what counts as evidence in a decision. A clinic does not ship pull requests, and a law firm does not measure story points. Pick the closest one \u2014 you can change it later.":"Decide el vocabulario, los roles que se te ofrecen y qu\xE9 cuenta como prueba en una decisi\xF3n. Una cl\xEDnica no entrega pull requests, y un despacho no mide story points. Elige el m\xE1s cercano \u2014 luego se cambia.","You chair this city whatever you answer \u2014 this is your <b>speciality</b>, not your authority. It shapes the perspective you bring to a decision and the knowledge files the city writes for you. <b>Blank</b> is a real answer: it means no preset knowledge.":"Presides esta ciudad respondas lo que respondas \u2014 esto es tu <b>especialidad</b>, no tu autoridad. Da forma a la perspectiva con la que entras en una decisi\xF3n y a los ficheros de conocimiento que la ciudad escribe para ti. <b>En blanco</b> es una respuesta de verdad: significa sin conocimiento previo.","Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window, its own role and its own corner of your disk. A house is <b>not</b> a repository: one can answer for three services and a folder of documents at once, and a house whose work is documents needs no git anywhere.":"Cada casa aloja a un trabajador \u2014 un <b>agente</b> \u2014 con su ventana, su rol y su rinc\xF3n de tu disco. Una casa <b>no</b> es un repositorio: una sola puede responder por tres servicios y una carpeta de documentos a la vez, y una casa cuyo trabajo son documentos no necesita git en ninguna parte.","Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.":"Tu asiento queda escrito y {cuantas}. Todo lo que acabas de responder es un fichero de texto que puedes leer y editar.","<b>{n}</b> house stands around it":"a su alrededor hay <b>{n}</b> casa","<b>{n}</b> houses stand around it":"a su alrededor hay <b>{n}</b> casas","the city is waiting for its first house":"la ciudad espera su primera casa","What people usually do next:":"Lo que suele hacerse ahora:","A goal is optional, and the city works without one \u2014 but a round with no goal is a status report, because there is nothing to argue against.":"El objetivo es opcional y la ciudad funciona sin \xE9l \u2014 pero una ronda sin objetivo es un parte de estado, porque no hay nada contra lo que discutir.","Its house grows with the pull requests it merges. Mounts repositories and worktrees.":"Su casa crece con las pull requests que fusiona. Monta repositorios y worktrees.","Its house grows with the documents it writes. Needs no git at all.":"Su casa crece con los documentos que escribe. No necesita git para nada.","Its house grows with the decisions it records.":"Su casa crece con las decisiones que deja escritas.","urgencias, api, the handbook \u2014 whatever you would say out loud":"urgencias, api, el manual \u2014 como lo llamar\xEDas en voz alta"});A({Yes:"S\xED","What is it called?":"\xBFC\xF3mo se llama?","home, clients, the lab \u2014 whatever you would say out loud":"casa, clientes, el laboratorio \u2014 como lo dir\xEDas en voz alta","A city is a place with its own seat, its own houses and its own map. Yours stay where they are.":"Una ciudad es un sitio con su propio asiento, sus casas y su mapa. Las que ya tienes se quedan donde est\xE1n.","Start over {city}?":"\xBFEmpezar {city} de cero?","Start over":"Empezar de cero","You lose:":"Pierdes:","You keep:":"Conservas:","Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).":"Ahora mismo eso son {agents} casa(s), {roads} carretera(s) y {acts} acta(s) de comit\xE9.","Type the city\u2019s name to confirm":"Escribe el nombre de la ciudad para confirmar","Archive {city}?":"\xBFArchivar {city}?","Archive it":"Archivarla","It moves into .backups. Nothing is deleted, and you can put it back by hand.":"Se mueve a .backups. No se borra nada, y puedes devolverla a mano.","Which engine?":"\xBFQu\xE9 motor?","Model alias":"Alias del modelo","An alias the CLI resolves when the window opens. Anything it accepts works here.":"Un alias que la CLI resuelve al abrir la ventana. Vale cualquiera que ella acepte.","Use it":"Usarlo","Remove the skill {skill}?":"\xBFQuitar la skill {skill}?","Remove it":"Quitarla","It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.":"Se borra de la casa de este agente. Nada fuera de esa carpeta se toca.","Build a house":"Construir una casa","One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.":"Un trabajador, su ventana y su rinc\xF3n de tu disco. Todo esto se puede cambiar despu\xE9s.","{name} has a house now":"{name} ya tiene casa","{name} joined the city":"{name} se ha mudado a la ciudad","What else does it work on?":"\xBFSobre qu\xE9 m\xE1s trabaja?","Mount it":"Montarlo",Mounted:"Montado","Stop this agent working on {what}?":"\xBFQue este agente deje de trabajar sobre {what}?","Unmount it":"Desmontarlo","The link goes. The folder it points at stays exactly where it is.":"Se va el enlace. La carpeta a la que apunta se queda exactamente donde est\xE1.","Give it a name \u2014 it is how you will call it in its window":"Ponle un nombre \u2014 es como la llamar\xE1s en su ventana","Could not add that agent":"No se pudo a\xF1adir ese agente","could not mount":"no se pudo montar","Could not set its engine":"No se pudo fijar su motor","{n} mount":"{n} montaje","{n} mounts":"{n} montajes"});A({"Walk your disk and pick whatever you like: a repository, a worktree, a folder of documents, one exact file. As many as you want. Nothing is copied \u2014 each one is linked into this agent\u2019s own home.":"Recorre tu disco y coge lo que quieras: un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Tantos como quieras. No se copia nada \u2014 cada uno se enlaza dentro de la casa de este agente.","Working on":"Trabaja sobre","Nothing chosen yet \u2014 an agent with no mounts is fine too.":"A\xFAn no has cogido nada \u2014 un agente sin montajes tambi\xE9n vale.","Add this folder":"A\xF1adir esta carpeta","Add this":"A\xF1adir esto","Already chosen":"Ya est\xE1 cogido","Up one folder":"Subir una carpeta","reading that folder":"leyendo esa carpeta","This folder is empty.":"Esta carpeta est\xE1 vac\xEDa.","Only the first 2000 shown.":"S\xF3lo se muestran los 2000 primeros.","A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.":"Un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Se enlaza, nunca se copia."});A({Demos:"Demos",demos:"demos","See a committee happen":"Mira un comit\xE9 de verdad","reading the demo shelf":"leyendo la estanter\xEDa de demos","A real question, answered by a city of agents that never talk to each other behind the chair\u2019s back. Pick the field closest to yours \u2014 the machine is the same one in all three; only the work changes.":"Una pregunta de verdad, respondida por una ciudad de agentes que nunca hablan entre ellos a espaldas de quien preside. Elige el campo m\xE1s cercano al tuyo \u2014 la maquinaria es la misma en los tres; lo que cambia es el trabajo.","{n} turns":"{n} turnos","These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.":"Son grabaciones de ejecuciones reales sobre el bus real, reproducidas aqu\xED. Para lanzar una en vivo desde terminal: agents-city demo --domain software.","All demos":"Todas las demos",Play:"Reproducir",Pause:"Pausa",Replay:"Repetir","{done} of {total}":"{done} de {total}","Press play. The turns arrive one by one, exactly as they did.":"Dale a reproducir. Los turnos van llegando uno a uno, tal cual llegaron.","A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.":"Una grabaci\xF3n de una ejecuci\xF3n real: estos eventos salieron del bus real, del comit\xE9 real. Aqu\xED no se est\xE1 decidiendo nada ahora mismo.","Nothing recorded here":"Aqu\xED no hay nada grabado","This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.":"Esta instalaci\xF3n no tiene grabaciones. Cr\xE9alas con demo/graba.py, o lanza la cosa entera desde terminal con agents-city demo."});A({"{n} folder":"{n} carpeta","{n} folders":"{n} carpetas","{n} file":"{n} fichero","{n} files":"{n} ficheros","files are below the folders":"los ficheros van debajo de las carpetas"});A({"Leave all three on default and it runs the way you do. Whatever you set here is written once on the card, and the launcher hands it to whichever CLI runs this house.":"D\xE9jalos los tres por defecto y funcionar\xE1 como t\xFA. Lo que pongas aqu\xED se escribe una sola vez en la ficha, y el lanzador se lo pasa a la CLI que mueva esta casa.","This CLI has no effort setting.":"Esta CLI no tiene ajuste de esfuerzo.","An alias the Claude CLI resolves when the window opens.":"Un alias que la CLI de Claude resuelve al abrir la ventana.","The model name your Codex uses \u2014 the one in ~/.codex/config.toml.":"El nombre del modelo que usa tu Codex \u2014 el de ~/.codex/config.toml.","OpenCode names a model provider/model, like anthropic/claude-sonnet-4.":"OpenCode nombra los modelos proveedor/modelo, como anthropic/claude-sonnet-4.","The model name your Kimi CLI uses.":"El nombre del modelo que usa tu CLI de Kimi."});A({"my seat":"mi asiento","Save my seat":"Guardar mi asiento","work domain":"dominio de trabajo","your day":"tu d\xEDa","the agents":"los agentes","repo agents":"agentes de repo","one goal \u2014 optional":"un objetivo \u2014 opcional","The goal, in one line":"El objetivo, en una l\xEDnea","Concrete enough to argue with \u2014 empty skips it":"Lo bastante concreto como para discutirlo \u2014 vac\xEDo lo salta","How it is measured":"C\xF3mo se mide","in prose: the architect reads the AGENTS.md files on Fridays":"en prosa: el arquitecto lee los AGENTS.md los viernes","The command that returns it, if a command can":"El comando que lo devuelve, si un comando puede","What it returns today":"Lo que devuelve hoy","Where it has to get to":"A d\xF3nde tiene que llegar","By when":"Para cu\xE1ndo","empty for a qualitative goal":"vac\xEDo para un objetivo cualitativo","\u2026or who judges it, and how often":"\u2026o qui\xE9n lo juzga, y cada cu\xE1nto","what is left":"lo que queda","open my session":"abrir mi sesi\xF3n","Open the roster":"Abrir la lista","districts & houses":"distritos y casas","Save districts & houses":"Guardar distritos y casas","another district\u2026":"otro distrito\u2026","The modelling no tool can do for you":"El modelado que ninguna herramienta puede hacer por ti","A house is <b>not a repo</b> \u2014 it is a parcel, a slice of one serving a single business unit. Split the interesting repos, give every house its district, and the map can say \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.":"Una casa <b>no es un repo</b> \u2014 es una parcela, una porci\xF3n de uno al servicio de una sola unidad de negocio. Parte los repos interesantes, dale a cada casa su distrito, y el mapa podr\xE1 decir \xABeste cambio toca banca\xBB en vez de \xABeste cambio toca src/lib\xBB.","Cities this one may reach":"Ciudades a las que \xE9sta puede llegar","No roads yet. This city is isolated on purpose.":"A\xFAn no hay carreteras. Esta ciudad est\xE1 aislada a prop\xF3sito.","No unconnected local cities.":"No hay ciudades locales sin conectar.","other cities on this machine":"otras ciudades en esta m\xE1quina","remote invitation \xB7 public, no token":"invitaci\xF3n remota \xB7 p\xFAblica, sin token","committee acts":"actas del comit\xE9","Decisions with a visible chain of custody":"Decisiones con una cadena de custodia visible","No committee acts yet. Open one only when the seat needs specialised evidence.":"A\xFAn no hay actas. Abre un comit\xE9 s\xF3lo cuando el asiento necesite pruebas especializadas.","agents-city committee schema open":"agents-city committee schema open","guided committee":"comit\xE9 guiado","Play the guided committee from the top":"Reproducir el comit\xE9 guiado desde el principio","Pause or resume mid-scene":"Pausar o seguir a mitad de escena","\u27F3 replay":"\u27F3 repetir","\u23F8 pause":"\u23F8 pausa","seat moderates":"el asiento modera","read full message":"leer el mensaje entero",'One person, several autonomous cities: each has its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that city\u2019s folder \u2014 so two cities can each have a <code class="mono">docs</code> house and they are two different workers. They share nothing unless you build a road between them.':'Una persona, varias ciudades aut\xF3nomas: cada una con su identidad, su dominio, su silla, <b>sus propias casas</b> y sus carreteras. Una casa no se comparte \u2014 est\xE1 dentro de la ciudad que la posee, con su espacio de trabajo y sus montajes bajo la carpeta de esa ciudad \u2014 as\xED que dos ciudades pueden tener cada una su casa <code class="mono">docs</code> y son dos trabajadores distintos. No comparten nada salvo que construyas una carretera entre ellas.',"client-a, research, the book\u2026":"cliente-a, investigaci\xF3n, el libro\u2026","which city this hall manages":"qu\xE9 ciudad gobierna este ayuntamiento","Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> \u2014 only the city that points at them.":"Devuelve {city} a su primer d\xEDa: sin asiento, sin agentes, sin historial de comit\xE9, sin mapa. Tus repositorios y carpetas de documentos <b>no se tocan</b> \u2014 s\xF3lo la ciudad que apunta a ellos.","Agents &amp; skills":"Agentes y skills",'<b>A house is where an agent lives and works</b>, and many houses are a city \u2014 it is the same thing the map draws, growing with what that agent actually does. The card and the CLI call them <code class="mono">agents</code>; here you see their houses.':'<b>Una casa es donde vive y trabaja un agente</b>, y muchas casas son una ciudad \u2014 es lo mismo que dibuja el mapa, creciendo con lo que ese agente hace de verdad. La ficha y la CLI los llaman <code class="mono">agents</code>; aqu\xED ves sus casas.',"They belong to <b>this</b> city and only to it: each one\u2019s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.":"Pertenecen a <b>esta</b> ciudad y s\xF3lo a ella: el espacio de trabajo de cada una y sus montajes viven dentro de {donde}, as\xED que otra ciudad tiene su propia gente aunque les pongas los mismos nombres.","a legacy repo agent works on its own repo":"un agente de repo heredado trabaja sobre su propio repo","data repo":"repo de datos","skills recognised":"skills reconocidas","plugin installed":"plugin instalado","Reroll this agent\u2019s face \u2014 deterministic, persisted on the card":"Vuelve a tirar la cara de este agente \u2014 determinista, guardada en la ficha","Instructions the Claude runtime reads":"Instrucciones que lee el runtime de Claude","Instructions Codex, OpenCode and Kimi read":"Instrucciones que leen Codex, OpenCode y Kimi","Run the engine for real: --version, and the login state on Claude":"Ejecuta el motor de verdad: --version, y el estado de sesi\xF3n en Claude","Install a skill zip into this agent\u2019s own home \u2014 the Claude runtime reads skills; other engines ignore them":"Instala un zip de skill en la casa de este agente \u2014 el runtime de Claude lee skills; los dem\xE1s motores las ignoran","Remove this skill from the agent\u2019s home":"Quitar esta skill de la casa del agente","reading the domain packs":"leyendo los paquetes de dominio","reading the role files":"leyendo los ficheros de rol","an isometric house":"una casa isom\xE9trica"});var H=class{host=null;monta(n){this.host=n,this.dibuja()}dibuja(){this.host&&(this.host.innerHTML=this.html(),this.enlaza(this.host))}repinta(){this.dibuja()}};var Ue={repo:"git",worktree:"wt"},B=class extends H{constructor(t,a,i){super();this.p=t;this.elige=a;this.yaElegidas=i}aqui="";listado=null;cargando=!1;fallo="";monta(t){super.monta(t),!this.listado&&!this.cargando&&this.ve(this.aqui||"~")}async ve(t){this.cargando=!0,this.fallo="",this.listado||this.repinta();try{let a=await this.p.api("/api/carpeta?path="+encodeURIComponent(t));a.error?this.fallo=a.error:(this.listado=a,this.aqui=a.ruta)}catch(a){this.fallo=String(a)}finally{this.cargando=!1,this.repinta()}}migas(){let t=this.p.esc,a=this.listado;if(!a)return"";let i=a.ruta.split("/").filter(Boolean),d="";return'<button type="button" class="expMiga" data-exp="ve" data-ruta="/">/</button>'+i.map(r=>(d+="/"+r,`<button type="button" class="expMiga" data-exp="ve" data-ruta="${t(d)}">${t(r)}</button>`)).join("<i>/</i>")}html(){let t=this.p.esc,a=this.listado,i=new Set(this.yaElegidas()),d=s("Already chosen"),o=s("Add this"),r=(a?.entradas??[]).map(u=>{let h=i.has(u.ruta);return`<div class="expFila ${u.dir?"dir":"fich"} ${h?"puesta":""}">
2
2
  <button type="button" class="expNombre" ${u.dir?`data-exp="ve" data-ruta="${t(u.ruta)}"`:"disabled"} title="${t(u.ruta)}">
3
3
  <i class="expIcono">${u.dir?"\u25B8":"\xB7"}</i>${t(u.nombre)}${u.git?`<em class="expMarca">${t(Ue[u.git]??u.git)}</em>`:""}${u.enlace?'<em class="expMarca">link</em>':""}</button>
4
4
  <button type="button" class="expMas" data-exp="toma" data-ruta="${t(u.ruta)}"
5
- title="${t(h?c:o)}">${h?"\u2713":"+"}</button>
5
+ title="${t(h?d:o)}">${h?"\u2713":"+"}</button>
6
6
  </div>`}).join("");return`
7
7
  <div class="exp">
8
8
  <div class="expAtajos">${(a?.atajos??[]).map(u=>`<button type="button" class="expAtajo ${u.ruta===a?.ruta?"aqui":""}" data-exp="ve" data-ruta="${t(u.ruta)}">${t(u.nombre)}</button>`).join("")}</div>
@@ -17,17 +17,17 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
17
17
  ${a?.recortada?`<p class="pista">${s("Only the first 2000 shown.")}</p>`:""}
18
18
  <button type="button" class="bt bvMini expTomaAqui" data-exp="toma"
19
19
  data-ruta="${t(a?.ruta??"")}" ${a?"":"disabled"}>${s("Add this folder")}</button>
20
- </div>`}cuenta(t){if(!t||!t.entradas.length)return"";let a=t.entradas.filter(o=>o.dir).length,i=t.entradas.length-a,c=[];return a&&c.push(x(a,"{n} folder","{n} folders")),i&&c.push(x(i,"{n} file","{n} files")),`<p class="expCuenta">${this.p.esc(c.join(" \xB7 "))}${i?` \u2014 ${s("files are below the folders")}`:""}</p>`}enlaza(t){t.dataset.expEnlazado!=="1"&&(t.dataset.expEnlazado="1",t.addEventListener("click",a=>{let i=a.target?.closest("[data-exp]");if(!i||!t.contains(i))return;a.preventDefault(),a.stopPropagation();let c=i.dataset.ruta??"";c&&(i.dataset.exp==="ve"?this.ve(c):(this.elige(c),this.repinta()))}))}};var G=["claude","codex","opencode","kimi"],ce={claude:{modelos:["haiku","sonnet","opus","fable"],esfuerzo:!0,pista:"An alias the Claude CLI resolves when the window opens."},codex:{modelos:[],esfuerzo:!0,pista:"The model name your Codex uses \u2014 the one in ~/.codex/config.toml."},opencode:{modelos:[],esfuerzo:!1,pista:"OpenCode names a model provider/model, like anthropic/claude-sonnet-4."},kimi:{modelos:[],esfuerzo:!1,pista:"The model name your Kimi CLI uses."}};function J(e){return ce[e||"claude"]??ce.claude}var bt=ce.claude.modelos,de={low:1,medium:2,high:3,xhigh:4,max:5},Q=Object.keys(de);function z(e,n,t,a="default"){return e.concat(n&&!e.includes(n)?[n]:[]).map(i=>`<option value="${t(i)}"${i===n?" selected":""}>${t(i||a)}</option>`).join("")}function Me(e){let n=e.toLowerCase();if(!n)return{ancho:.5,texto:"default",defecto:!0};for(let[t,a]of[["fable",1],["opus",.8],["sonnet",.55],["haiku",.35]])if(n.includes(t))return{ancho:a,texto:n,defecto:!1};return{ancho:.6,texto:n,defecto:!1}}var We=[["code","It writes code","Its house grows with the pull requests it merges. Mounts repositories and worktrees."],["knowledge","It keeps knowledge","Its house grows with the documents it writes. Needs no git at all."],["coordinator","It coordinates","Its house grows with the decisions it records."]],_=class extends H{constructor(t,a=[]){super();this.p=t;this.roles=a}datos={nombre:"",clase:"code",rol:"blank",montajes:[],runtime:"",modelo:"",esfuerzo:""};roles=[];rolesPedidos=!1;monta(t){this.aseguraRoles(),super.monta(t);let a=t.querySelector(".casaExp");a&&this.explora().monta(a)}repinta(){this.host&&this.monta(this.host)}async aseguraRoles(){if(!this.rolesPedidos){this.rolesPedidos=!0;try{let t=await this.p.api("/api/roles?scope=agent");this.roles=t.roles??[],this.pintaRoles()}catch{this.roles=[]}}}pintaRoles(){let t=this.host?.querySelector("#bvRol");!t||!this.roles.length||(t.innerHTML=this.opcionesDeRol(),t.value=this.datos.rol,t.onchange=()=>this.recoge(),this.recoge())}opcionesDeRol(){return this.roles.map(t=>`<option value="${this.p.esc(t.id)}"${t.id===this.datos.rol?" selected":""}>${this.p.esc(t.name)}</option>`).join("")}explorador=null;explora(){return this.explorador||(this.explorador=new B(this.p,t=>{let a=this.datos.montajes.indexOf(t);a>=0?this.datos.montajes.splice(a,1):this.datos.montajes.push(t),this.pintaElegidas()},()=>this.datos.montajes)),this.explorador}html(){let t=this.datos,a=this.p.esc,i=We.map(([r,p,u])=>`
20
+ </div>`}cuenta(t){if(!t||!t.entradas.length)return"";let a=t.entradas.filter(o=>o.dir).length,i=t.entradas.length-a,d=[];return a&&d.push(x(a,"{n} folder","{n} folders")),i&&d.push(x(i,"{n} file","{n} files")),`<p class="expCuenta">${this.p.esc(d.join(" \xB7 "))}${i?` \u2014 ${s("files are below the folders")}`:""}</p>`}enlaza(t){t.dataset.expEnlazado!=="1"&&(t.dataset.expEnlazado="1",t.addEventListener("click",a=>{let i=a.target?.closest("[data-exp]");if(!i||!t.contains(i))return;a.preventDefault(),a.stopPropagation();let d=i.dataset.ruta??"";d&&(i.dataset.exp==="ve"?this.ve(d):(this.elige(d),this.repinta()))}))}};var G=["claude","codex","opencode","kimi"],de={claude:{modelos:["haiku","sonnet","opus","fable"],esfuerzo:!0,pista:"An alias the Claude CLI resolves when the window opens."},codex:{modelos:[],esfuerzo:!0,pista:"The model name your Codex uses \u2014 the one in ~/.codex/config.toml."},opencode:{modelos:[],esfuerzo:!1,pista:"OpenCode names a model provider/model, like anthropic/claude-sonnet-4."},kimi:{modelos:[],esfuerzo:!1,pista:"The model name your Kimi CLI uses."}};function J(e){return de[e||"claude"]??de.claude}var yt=de.claude.modelos,ce={low:1,medium:2,high:3,xhigh:4,max:5},Q=Object.keys(ce);function z(e,n,t,a="default"){return e.concat(n&&!e.includes(n)?[n]:[]).map(i=>`<option value="${t(i)}"${i===n?" selected":""}>${t(i||a)}</option>`).join("")}function Me(e){let n=e.toLowerCase();if(!n)return{ancho:.5,texto:"default",defecto:!0};for(let[t,a]of[["fable",1],["opus",.8],["sonnet",.55],["haiku",.35]])if(n.includes(t))return{ancho:a,texto:n,defecto:!1};return{ancho:.6,texto:n,defecto:!1}}var We=[["code","It writes code","Its house grows with the pull requests it merges. Mounts repositories and worktrees."],["knowledge","It keeps knowledge","Its house grows with the documents it writes. Needs no git at all."],["coordinator","It coordinates","Its house grows with the decisions it records."]],_=class extends H{constructor(t,a=[]){super();this.p=t;this.roles=a}datos={nombre:"",clase:"code",rol:"blank",montajes:[],runtime:"",modelo:"",esfuerzo:""};roles=[];rolesPedidos=!1;monta(t){this.aseguraRoles(),super.monta(t);let a=t.querySelector(".casaExp");a&&this.explora().monta(a)}repinta(){this.host&&this.monta(this.host)}async aseguraRoles(){if(!this.rolesPedidos){this.rolesPedidos=!0;try{let t=await this.p.api("/api/roles?scope=agent");this.roles=t.roles??[],this.pintaRoles()}catch{this.roles=[]}}}pintaRoles(){let t=this.host?.querySelector("#bvRol");!t||!this.roles.length||(t.innerHTML=this.opcionesDeRol(),t.value=this.datos.rol,t.onchange=()=>this.recoge(),this.recoge())}opcionesDeRol(){return this.roles.map(t=>`<option value="${this.p.esc(t.id)}"${t.id===this.datos.rol?" selected":""}>${this.p.esc(t.name)}</option>`).join("")}explorador=null;explora(){return this.explorador||(this.explorador=new B(this.p,t=>{let a=this.datos.montajes.indexOf(t);a>=0?this.datos.montajes.splice(a,1):this.datos.montajes.push(t),this.pintaElegidas()},()=>this.datos.montajes)),this.explorador}html(){let t=this.datos,a=this.p.esc,i=We.map(([r,p,u])=>`
21
21
  <button type="button" class="bvOpcion ${t.clase===r?"elegida":""}"
22
22
  data-bv="clase" data-id="${r}">
23
- <b>${s(p)}</b><span>${s(u)}</span></button>`).join(""),c=this.roles.map(r=>`<option value="${a(r.id)}"${r.id===t.rol?" selected":""}>${a(r.name)}</option>`).join(""),o=J(t.runtime);return`
23
+ <b>${s(p)}</b><span>${s(u)}</span></button>`).join(""),d=this.roles.map(r=>`<option value="${a(r.id)}"${r.id===t.rol?" selected":""}>${a(r.name)}</option>`).join(""),o=J(t.runtime);return`
24
24
  <div class="campo"><label>${s("What do you call it?")}</label>
25
25
  <input type="text" id="bvNombre" value="${a(t.nombre)}"
26
26
  placeholder="${s("urgencias, api, the handbook \u2014 whatever you would say out loud")}"></div>
27
27
  <label class="bvEtiqueta">${s("What kind of work does it do?")}</label>
28
28
  <div class="bvRejilla bvTres">${i}</div>
29
29
  <div class="campo"><label>${s("Its role \u2014 its speciality, never authority")}</label>
30
- <select id="bvRol">${c}</select></div>
30
+ <select id="bvRol">${d}</select></div>
31
31
  <label class="bvEtiqueta">${s("What runs it?")}</label>
32
32
  <p class="pista">${s("Leave all three on default and it runs the way you do. Whatever you set here is written once on the card, and the launcher hands it to whichever CLI runs this house.")}</p>
33
33
  <div class="bvMotor">
@@ -48,7 +48,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
48
48
  <div class="casaExp"></div>
49
49
  <div id="casaElegidas">${this.elegidas()}</div>`}elegidas(){let t=this.p.esc,a=this.datos.montajes;return a.length?`<div class="expElegidas"><label class="bvEtiqueta">${s("Working on")}</label>
50
50
  ${a.map(i=>`<button type="button" class="bvChip elegida" data-bv="quita"
51
- data-ruta="${t(i)}" title="${t(i)}">${t(i.split("/").pop()||i)}<i class="expQuita">\u2715</i></button>`).join("")}</div>`:`<p class="pista">${s("Nothing chosen yet \u2014 an agent with no mounts is fine too.")}</p>`}pintaElegidas(){let t=this.host?.querySelector("#casaElegidas");t&&(t.innerHTML=this.elegidas(),this.enlaza(t))}enlaza(t){t.querySelectorAll("#bvNombre,#bvRol,#bvModelo,#bvEsfuerzo").forEach(a=>{a.oninput=()=>this.recoge(),a.onchange=()=>this.recoge()}),t.querySelectorAll("[data-bv]").forEach(a=>{a.onclick=i=>{i.preventDefault();let c=a.dataset.bv;if(this.recoge(),c==="clase")this.datos.clase=a.dataset.id??"code";else if(c==="quita"){let o=this.datos.montajes.indexOf(a.dataset.ruta??"");o>=0&&this.datos.montajes.splice(o,1)}this.repinta()}}),t.querySelector("#bvRuntime")?.addEventListener("change",()=>{this.recoge(),this.repinta()})}recoge(){let t=this.host;if(!t)return;let a=t.querySelector("#bvNombre"),i=t.querySelector("#bvRol");a&&(this.datos.nombre=a.value),i&&(this.datos.rol=i.value);for(let[c,o]of[["#bvRuntime","runtime"],["#bvModelo","modelo"],["#bvEsfuerzo","esfuerzo"]]){let r=t.querySelector(c);r&&(this.datos[o]=r.value.trim())}}async guarda(){this.recoge();let t=this.datos,a=t.nombre.trim();if(!a)return this.p.aviso(s("Give it a name \u2014 it is how you will call it in its window"),!0),null;try{let i=await this.p.api("/api/agentes",{method:"POST",body:JSON.stringify({name:a,kind:t.clase,role:t.rol})});if(!i.ok||!i.agent)return this.p.aviso(i.error||s("Could not add that agent"),!0),null;let c=await Promise.all(t.montajes.map(r=>this.p.api("/api/montaje",{method:"POST",body:JSON.stringify({agent:i.agent,add:r})}).then(p=>p.ok?"":`${r}: ${p.error??s("could not mount")}`).catch(p=>`${r}: ${String(p)}`)));for(let r of c.filter(Boolean))this.p.aviso(r,!0);let o={};if(t.runtime&&(o.runtime=t.runtime),t.modelo&&(o.model=t.modelo),t.esfuerzo&&(o.effort=t.esfuerzo),Object.keys(o).length){let r=await this.p.api("/api/agente",{method:"POST",body:JSON.stringify({agent:i.agent,...o})});r.ok||this.p.aviso(r.error??s("Could not set its engine"),!0)}return{slug:i.agent,nombre:a}}catch(i){return this.p.aviso(String(i),!0),null}}resumen(){let t=this.datos;return t.montajes.length?x(t.montajes.length,"{n} mount","{n} mounts"):s("nothing mounted yet")}};function E(e){return String(e).replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}var K=null;function I(e){K?.remove();let n=e.campos??[],t=document.createElement("div");t.className="dlgFondo",t.innerHTML=`
51
+ data-ruta="${t(i)}" title="${t(i)}">${t(i.split("/").pop()||i)}<i class="expQuita">\u2715</i></button>`).join("")}</div>`:`<p class="pista">${s("Nothing chosen yet \u2014 an agent with no mounts is fine too.")}</p>`}pintaElegidas(){let t=this.host?.querySelector("#casaElegidas");t&&(t.innerHTML=this.elegidas(),this.enlaza(t))}enlaza(t){t.querySelectorAll("#bvNombre,#bvRol,#bvModelo,#bvEsfuerzo").forEach(a=>{a.oninput=()=>this.recoge(),a.onchange=()=>this.recoge()}),t.querySelectorAll("[data-bv]").forEach(a=>{a.onclick=i=>{i.preventDefault();let d=a.dataset.bv;if(this.recoge(),d==="clase")this.datos.clase=a.dataset.id??"code";else if(d==="quita"){let o=this.datos.montajes.indexOf(a.dataset.ruta??"");o>=0&&this.datos.montajes.splice(o,1)}this.repinta()}}),t.querySelector("#bvRuntime")?.addEventListener("change",()=>{this.recoge(),this.repinta()})}recoge(){let t=this.host;if(!t)return;let a=t.querySelector("#bvNombre"),i=t.querySelector("#bvRol");a&&(this.datos.nombre=a.value),i&&(this.datos.rol=i.value);for(let[d,o]of[["#bvRuntime","runtime"],["#bvModelo","modelo"],["#bvEsfuerzo","esfuerzo"]]){let r=t.querySelector(d);r&&(this.datos[o]=r.value.trim())}}async guarda(){this.recoge();let t=this.datos,a=t.nombre.trim();if(!a)return this.p.aviso(s("Give it a name \u2014 it is how you will call it in its window"),!0),null;try{let i=await this.p.api("/api/agentes",{method:"POST",body:JSON.stringify({name:a,kind:t.clase,role:t.rol})});if(!i.ok||!i.agent)return this.p.aviso(i.error||s("Could not add that agent"),!0),null;let d=await Promise.all(t.montajes.map(r=>this.p.api("/api/montaje",{method:"POST",body:JSON.stringify({agent:i.agent,add:r})}).then(p=>p.ok?"":`${r}: ${p.error??s("could not mount")}`).catch(p=>`${r}: ${String(p)}`)));for(let r of d.filter(Boolean))this.p.aviso(r,!0);let o={};if(t.runtime&&(o.runtime=t.runtime),t.modelo&&(o.model=t.modelo),t.esfuerzo&&(o.effort=t.esfuerzo),Object.keys(o).length){let r=await this.p.api("/api/agente",{method:"POST",body:JSON.stringify({agent:i.agent,...o})});r.ok||this.p.aviso(r.error??s("Could not set its engine"),!0)}return{slug:i.agent,nombre:a}}catch(i){return this.p.aviso(String(i),!0),null}}resumen(){let t=this.datos;return t.montajes.length?x(t.montajes.length,"{n} mount","{n} mounts"):s("nothing mounted yet")}};function E(e){return String(e).replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}var K=null;function P(e){K?.remove();let n=e.campos??[],t=document.createElement("div");t.className="dlgFondo",t.innerHTML=`
52
52
  <div class="dlg" role="dialog" aria-modal="true" aria-label="${E(e.titulo)}"
53
53
  style="${e.ancho?`max-width:${e.ancho}px`:""}">
54
54
  <h2>${E(e.titulo)}</h2>
@@ -62,7 +62,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
62
62
  <button type="button" class="bt" data-dlg="no">${E(e.cancelar??"Cancel")}</button>
63
63
  <button type="button" class="bt ${e.peligro?"malo":"ppal"}" data-dlg="si">${E(e.aceptar??"OK")}</button>
64
64
  </div>
65
- </div>`,document.body.appendChild(t),K=t;let a=document.activeElement,i=t.querySelector(".dlg"),c=t.querySelector('[data-dlg="si"]');return new Promise(o=>{let r=!1,p=y=>{r||(r=!0,document.removeEventListener("keydown",w,!0),t.remove(),K===t&&(K=null),a?.focus?.(),o(y))},u=()=>{let y={};return t.querySelectorAll("[data-campo]").forEach($=>{y[$.dataset.campo??""]=$.value}),y},h=()=>{let y=u();n.some($=>$.requerido&&!y[$.id]?.trim())||p(y)},v=()=>{let y=u(),$=n.some(R=>R.requerido&&!y[R.id]?.trim()),le=e.exige!==void 0&&(y[n[0]?.id??""]??"").trim()!==e.exige;c.disabled=$||le};c.onclick=h,t.querySelector('[data-dlg="no"]').onclick=()=>p(null),t.onclick=y=>{y.target===t&&p(null)},i.addEventListener("input",v),i.addEventListener("change",v),t.querySelectorAll("input").forEach(y=>{y.onkeydown=$=>{$.key==="Enter"&&!c.disabled&&($.preventDefault(),h())}});let w=y=>{y.key==="Escape"&&(y.preventDefault(),p(null))};document.addEventListener("keydown",w,!0),e.enlaza?.(i,p),v(),(i.querySelector("input,select,textarea")??(c.disabled?i:c)).focus?.()})}async function Z(e,n=[],t={}){return await I({titulo:e,cuerpo:n,aceptar:t.aceptar??"Yes",peligro:t.peligro})!==null}var Fe=[["0.5\xD7",2900],["1\xD7",1450],["2\xD7",700],["4\xD7",350]],X=class extends H{constructor(t){super();this.p=t}catalogo=[];cargando=!1;elegida=null;hasta=0;corriendo=!1;velocidad=1450;reloj=null;monta(t){!this.catalogo.length&&!this.cargando&&this.lee(),super.monta(t)}desmonta(){this.corriendo=!1,this.reloj!==null&&(clearTimeout(this.reloj),this.reloj=null)}repinta(){super.repinta();let t=this.elegida?this.host?.querySelector("#demoLista"):null;t&&(t.scrollTop=t.scrollHeight)}async lee(){this.cargando=!0;try{let t=await this.p.api("/api/demos");this.catalogo=t.demos??[]}catch{this.catalogo=[]}finally{this.cargando=!1,this.repinta()}}async abre(t){this.desmonta(),this.hasta=0,this.elegida=null,this.repinta();try{let a=await this.p.api("/api/demos?story="+encodeURIComponent(t));a.error||(this.elegida=a)}catch{this.elegida=null}this.repinta(),this.elegida&&this.arranca()}arranca(){this.elegida&&(this.hasta>=this.elegida.eventos.length&&(this.hasta=0),this.corriendo=!0,this.repinta(),this.siguiente())}siguiente(){this.reloj!==null&&clearTimeout(this.reloj),this.reloj=window.setTimeout(()=>{if(!this.corriendo||!this.elegida)return;let t=this.elegida.eventos[this.hasta];this.hasta+=1;let a=this.hasta>=this.elegida.eventos.length;a&&(this.hasta=this.elegida.eventos.length,this.corriendo=!1),t&&!a?this.anade(t):this.repinta(),a||this.siguiente()},this.velocidad)}anade(t){let a=this.host?.querySelector("#demoLista");if(!a||!this.elegida)return this.repinta();a.querySelector(".liveEmpty")&&(a.innerHTML=""),a.insertAdjacentHTML("beforeend",this.p.pinta(t)),a.scrollTop=a.scrollHeight,this.marcador()}marcador(){if(!this.host||!this.elegida)return;let t=this.elegida.eventos.length,a=Math.min(this.hasta,t),i=this.host.querySelector(".demoCuenta");i&&(i.textContent=s("{done} of {total}",{done:a,total:t}));let c=this.host.querySelector(".demoBarra i");c&&(c.style.width=`${t?a/t*100:0}%`)}html(){if(this.elegida)return this.reproductor(this.elegida);if(this.cargando)return`<p class="cargando">${s("reading the demo shelf")}</p>`;if(!this.catalogo.length)return`<div><span class="sub">${s("demos")}</span>
65
+ </div>`,document.body.appendChild(t),K=t;let a=document.activeElement,i=t.querySelector(".dlg"),d=t.querySelector('[data-dlg="si"]');return new Promise(o=>{let r=!1,p=b=>{r||(r=!0,document.removeEventListener("keydown",w,!0),t.remove(),K===t&&(K=null),a?.focus?.(),o(b))},u=()=>{let b={};return t.querySelectorAll("[data-campo]").forEach($=>{b[$.dataset.campo??""]=$.value}),b},h=()=>{let b=u();n.some($=>$.requerido&&!b[$.id]?.trim())||p(b)},v=()=>{let b=u(),$=n.some(q=>q.requerido&&!b[q.id]?.trim()),le=e.exige!==void 0&&(b[n[0]?.id??""]??"").trim()!==e.exige;d.disabled=$||le};d.onclick=h,t.querySelector('[data-dlg="no"]').onclick=()=>p(null),t.onclick=b=>{b.target===t&&p(null)},i.addEventListener("input",v),i.addEventListener("change",v),t.querySelectorAll("input").forEach(b=>{b.onkeydown=$=>{$.key==="Enter"&&!d.disabled&&($.preventDefault(),h())}});let w=b=>{b.key==="Escape"&&(b.preventDefault(),p(null))};document.addEventListener("keydown",w,!0),e.enlaza?.(i,p),v(),(i.querySelector("input,select,textarea")??(d.disabled?i:d)).focus?.()})}async function Z(e,n=[],t={}){return await P({titulo:e,cuerpo:n,aceptar:t.aceptar??"Yes",peligro:t.peligro})!==null}var Fe=[["0.5\xD7",2900],["1\xD7",1450],["2\xD7",700],["4\xD7",350]],X=class extends H{constructor(t){super();this.p=t}catalogo=[];cargando=!1;elegida=null;hasta=0;corriendo=!1;velocidad=1450;reloj=null;monta(t){!this.catalogo.length&&!this.cargando&&this.lee(),super.monta(t)}desmonta(){this.corriendo=!1,this.reloj!==null&&(clearTimeout(this.reloj),this.reloj=null)}repinta(){super.repinta();let t=this.elegida?this.host?.querySelector("#demoLista"):null;t&&(t.scrollTop=t.scrollHeight)}async lee(){this.cargando=!0;try{let t=await this.p.api("/api/demos");this.catalogo=t.demos??[]}catch{this.catalogo=[]}finally{this.cargando=!1,this.repinta()}}async abre(t){this.desmonta(),this.hasta=0,this.elegida=null,this.repinta();try{let a=await this.p.api("/api/demos?story="+encodeURIComponent(t));a.error||(this.elegida=a)}catch{this.elegida=null}this.repinta(),this.elegida&&this.arranca()}arranca(){this.elegida&&(this.hasta>=this.elegida.eventos.length&&(this.hasta=0),this.corriendo=!0,this.repinta(),this.siguiente())}siguiente(){this.reloj!==null&&clearTimeout(this.reloj),this.reloj=window.setTimeout(()=>{if(!this.corriendo||!this.elegida)return;let t=this.elegida.eventos[this.hasta];this.hasta+=1;let a=this.hasta>=this.elegida.eventos.length;a&&(this.hasta=this.elegida.eventos.length,this.corriendo=!1),t&&!a?this.anade(t):this.repinta(),a||this.siguiente()},this.velocidad)}anade(t){let a=this.host?.querySelector("#demoLista");if(!a||!this.elegida)return this.repinta();a.querySelector(".liveEmpty")&&(a.innerHTML=""),a.insertAdjacentHTML("beforeend",this.p.pinta(t)),a.scrollTop=a.scrollHeight,this.marcador()}marcador(){if(!this.host||!this.elegida)return;let t=this.elegida.eventos.length,a=Math.min(this.hasta,t),i=this.host.querySelector(".demoCuenta");i&&(i.textContent=s("{done} of {total}",{done:a,total:t}));let d=this.host.querySelector(".demoBarra i");d&&(d.style.width=`${t?a/t*100:0}%`)}html(){if(this.elegida)return this.reproductor(this.elegida);if(this.cargando)return`<p class="cargando">${s("reading the demo shelf")}</p>`;if(!this.catalogo.length)return`<div><span class="sub">${s("demos")}</span>
66
66
  <h1 style="margin-top:6px">${s("Nothing recorded here")}</h1>
67
67
  <p class="prosa">${s("This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.")}</p></div>`;let t=this.p.esc;return`<div>
68
68
  <span class="sub">${s("demos")}</span>
@@ -78,7 +78,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
78
78
  <span class="demoPlay">\u25B6 ${s("{n} turns",{n:a.turnos})}</span>
79
79
  </button>`).join("")}</div>
80
80
  <p class="pista">${s("These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.")}</p>
81
- </div>`}reproductor(t){let a=this.p.esc,i=t.eventos.length,c=Math.min(this.hasta,i),o=t.eventos.slice(0,c).map(this.p.pinta).join(""),r=c>=i;return`<div class="demoPlayer">
81
+ </div>`}reproductor(t){let a=this.p.esc,i=t.eventos.length,d=Math.min(this.hasta,i),o=t.eventos.slice(0,d).map(this.p.pinta).join(""),r=d>=i;return`<div class="demoPlayer">
82
82
  <div class="demoCab">
83
83
  <button type="button" class="bt bvMini" data-demo="atras">\u2190 ${s("All demos")}</button>
84
84
  <div><span class="sub">${a(t.dominio)} \xB7 ${a(t.ciudad)}</span>
@@ -86,11 +86,11 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
86
86
  </div>
87
87
  <div class="demoMandos">
88
88
  <button type="button" class="bt ppal" data-demo="${this.corriendo?"pausa":"play"}">${this.corriendo?`\u275A\u275A ${s("Pause")}`:r?`\u21BA ${s("Replay")}`:`\u25B6 ${s("Play")}`}</button>
89
- ${c&&!r?`<button type="button" class="bt" data-demo="replay">\u21BA ${s("Replay")}</button>`:""}
89
+ ${d&&!r?`<button type="button" class="bt" data-demo="replay">\u21BA ${s("Replay")}</button>`:""}
90
90
  <span class="demoVel">${Fe.map(([p,u])=>`<button type="button" class="demoVelBoton ${u===this.velocidad?"aqui":""}" data-demo="vel" data-ms="${u}">${a(p)}</button>`).join("")}</span>
91
- <span class="demoCuenta">${s("{done} of {total}",{done:c,total:i})}</span>
91
+ <span class="demoCuenta">${s("{done} of {total}",{done:d,total:i})}</span>
92
92
  </div>
93
- <div class="demoBarra"><i style="width:${i?c/i*100:0}%"></i></div>
93
+ <div class="demoBarra"><i style="width:${i?d/i*100:0}%"></i></div>
94
94
  <ol class="liveLista" id="demoLista">${o||`<li class="liveEmpty">${s("Press play. The turns arrive one by one, exactly as they did.")}</li>`}</ol>
95
95
  <p class="pista">${s("A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.")}</p>
96
96
  </div>`}enlaza(t){t.querySelectorAll("[data-demo]").forEach(a=>{a.onclick=i=>{switch(i.preventDefault(),a.dataset.demo){case"abre":this.abre(a.dataset.id??"");break;case"atras":this.desmonta(),this.elegida=null,this.hasta=0,this.repinta();break;case"play":this.arranca();break;case"pausa":this.desmonta(),this.repinta();break;case"replay":this.desmonta(),this.hasta=0,this.arranca();break;case"vel":{this.velocidad=Number(a.dataset.ms)||1450,this.corriendo&&this.siguiente(),this.repinta();break}}}})}};var xe=["Welcome","The work","Your chair","The houses","Ready"];function ue(e){return`<svg viewBox="0 0 120 108" role="img" aria-label="${s("an isometric house")}" class="bvCasa">
@@ -173,8 +173,12 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
173
173
  ${this.roster.length?`<div class="bvFichas">${n}</div>`:""}
174
174
  <div class="bvBotones">
175
175
  <button class="bt ppal" data-bv="nuevo">${s(this.roster.length?"Add another house":"Build the first house")}</button>
176
- <button class="bt" data-bv="siguiente">${s(this.roster.length?"That is everyone":"Skip \u2014 just me for now")}</button>
176
+ <button class="bt" data-bv="siguiente">${s(this.roster.length?"That is everyone":"Nobody yet \u2014 I answer alone")}</button>
177
177
  </div>
178
+ ${this.roster.length?"":`<p class="prosa apunte">${s(`A city with no houses does not delegate: the seat
179
+ answers, and it is the only one who can. That is a real choice for a role whose
180
+ work is other people's cities rather than folders \u2014 and it is not the usual one.
181
+ Houses can be added later, from here or with <b>agents-city seat --agents</b>.`)}</p>`}
178
182
  </div>`}pantallaFinal(){return`
179
183
  <div class="bvPaso">
180
184
  <span class="sub">${s("Ready").toLowerCase()}</span>
@@ -189,28 +193,28 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
189
193
  </div>
190
194
  <p class="pista">${s(`A goal is optional, and the city works without one \u2014 but a round with
191
195
  no goal is a status report, because there is nothing to argue against.`)}</p>
192
- </div>`}enlaza(n){let t=()=>void this.pinta(n);n.querySelectorAll("[data-bv]").forEach(i=>{i.onclick=async c=>{c.preventDefault();let o=i.dataset.bv;if(o==="salir")return this.p.vete("resumen");if(o==="atras")return this.paso=Math.max(0,this.paso-1),t();if(o==="dominio")return this.dominio=i.dataset.id??"",this.roles=[],this.rol="",t();if(o==="rol")return this.rol=i.dataset.id??"",t();if(o==="nuevo")return this.enCurso=new _({api:this.p.api,esc:this.p.esc,aviso:this.p.aviso,yo:this.p.yo},this.rolesDeAgente),t();if(o==="cancela")return this.enCurso=null,t();if(o==="guarda")return void await this.guardaAgente(n);if(o==="siguiente")return void await this.avanza(n);if(o==="sesion")return void await this.abreSesion();if(o==="ir-gente")return this.p.vete("gente");if(o==="ir-mapa")return this.p.vete("mapa");if(o==="ir-puesto")return this.p.vete("puesto")}});let a=n.querySelector("#bvCasa");a&&this.enCurso&&(this.enCurso.monta(a),a.querySelector("#bvNombre")?.focus())}async guardaAgente(n){let t=this.enCurso;if(t){this.guardando=!0,await this.pinta(n);try{await this.aseguraFicha();let a=await t.guarda();if(!a)return;this.roster.push({...t.datos,nombre:a.nombre}),this.enCurso=null,this.p.aviso(s("{name} joined the city",{name:a.nombre})),await this.p.refresca()}finally{this.guardando=!1,await this.pinta(n)}}}async aseguraFicha(){await this.p.api("/api/ficha",{method:"POST",body:JSON.stringify({domain:this.dominio||"software",role:this.rol||"blank"})})}async avanza(n){let t=this.paso===2;this.paso=Math.min(xe.length-1,this.paso+1),await this.pinta(n),t&&(await this.aseguraFicha(),await this.p.refresca())}async abreSesion(){try{let n=await this.p.api("/api/sesion",{method:"POST",body:JSON.stringify({user:this.p.yo})});this.p.aviso(n.ok&&n.attach?`Session built \u2014 attach with: ${n.attach}`:n.error??"Could not",!n.ok)}catch(n){this.p.aviso(String(n),!0)}}};var te="agents-city-map-activity/1";function Le(e){if(!e||typeof e!="object")return!1;let n=e;return n.protocol===te&&n.type==="map.nav"&&typeof n.view=="string"}function pe(e){if(!e||typeof e!="object")return!1;let n=e;return n.protocol==="agents-city-activity/1"&&typeof n.id=="string"&&typeof n.seq=="number"&&typeof n.kind=="string"&&typeof n.actor=="string"&&typeof n.summary=="string"&&typeof n.title=="string"&&Array.isArray(n.details)}function Se(e){return e.kind==="conversation.user"||e.kind==="conversation.agent"||e.kind==="runtime.session.started"||e.kind==="runtime.session.ended"}function Ce(e){return e.kind==="conversation.agent"||e.kind==="conversation.agent.commentary"?!!e.summary.trim():e.kind==="committee.opened"||e.kind==="committee.position.revealed"||e.kind==="committee.synthesis.published"||e.kind.startsWith("committee.floor.")||e.kind==="committee.decision.recorded"||e.kind.startsWith("committee.verification.")||e.kind==="committee.replanned"||e.kind==="committee.closed"||e.kind==="committee.command.rejected"&&e.tone==="error"}var F=window.PASE,oe=new URLSearchParams(location.search).get("city")??"";function ne(e,n,t){try{let a="/api/diario?PASE="+encodeURIComponent(F);oe&&(a+="&city="+encodeURIComponent(oe)),fetch(a,{method:"POST",headers:{"X-City-Pase":F,"Content-Type":"application/json"},body:JSON.stringify({que:e,detalle:n,donde:t??location.hash??""}),keepalive:!0}).catch(()=>{})}catch{}}async function b(e,n){let t=e+(e.includes("?")?"&":"?")+"PASE="+encodeURIComponent(F);oe&&(t+="&city="+encodeURIComponent(oe));let a;try{a=await fetch(t,{headers:{"X-City-Pase":F,"Content-Type":"application/json"},...n})}catch(c){throw e!=="/api/diario"&&ne("fetch failed",String(c),e),c}let i=await a.json();return e!=="/api/diario"&&(!a.ok||i?.error)&&ne("api refused",{estado:a.status,error:i?.error},e),i}function m(e,n=document){let t=n.querySelector(e);if(!t)throw new Error(`no element matches ${e}`);return t}function f(e,n=document){return[...n.querySelectorAll(e)]}function l(e){return String(e??"").replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}var N=e=>e.replace(d.casa,"~"),je=0;function g(e,n=!1){let t=m("#toast");t.textContent=e,t.className="ver"+(n?" mal":""),clearTimeout(je),je=window.setTimeout(()=>{t.className=""},3200)}var Re=e=>new Promise(n=>setTimeout(n,e)),d,S="mapa",Ye=[["resumen","Overview"],["mapa","The map"],["puesto","My seat"],["barrios","Districts"],["recepcion","Reception"],["red","Roads"],["committee","Committee"],["gente","Houses"],["demos","Demos"],["ciudades","Cities"]],be=!0;async function k(){d=await b("/api/estado"),be&&(be=!1,!d.tarjetas.find(n=>n.user===d.yo)||(d.agents??[]).length===0?S="bienvenida":d.mapa||(S="resumen")),C(),Ve(),Ge(),He(d.live_bus)}function Ve(){let e=document.getElementById("avisoVersion");if(!e)return;let n=(d.update??"").trim();e.textContent=n,e.hidden=!n}function Ge(){let e=document.querySelector("#livePanel header"),n=document.getElementById("demoControles");if(!e||!d.demo){n?.remove();return}if(n)return;let t=document.createElement("div");t.id="demoControles",t.innerHTML=`<span>${s("guided committee")}</span><button id="demoReplay" type="button" title="${s("Play the guided committee from the top")}">${s("\u27F3 replay")}</button><button id="demoPausa" type="button" title="${s("Pause or resume mid-scene")}">${s("\u23F8 pause")}</button>`,e.appendChild(t);let a=m("#demoPausa",t),i=c=>b("/api/demo",{method:"POST",body:JSON.stringify({action:c})});m("#demoReplay",t).onclick=async()=>{let c=await i("restart");a.textContent="\u23F8 pause",c.ok||g("Could not restart the demo",!0)},a.onclick=async()=>{let c=a.textContent?.includes("resume")??!1,o=await i(c?"resume":"pause");if(!o.running){a.textContent="\u23F8 pause",g("Nothing playing \u2014 hit replay",!0);return}a.textContent=o.paused?"\u25B6 resume":"\u23F8 pause"}}var A=null,ye="",L=[],O=!1,ae=0,Y=null,qe=!1,U=!1,se=null;function He(e){if(!e?.online||!e.url){O=!1,P(),fe();return}if(e.url===ye&&A&&(A.readyState===WebSocket.OPEN||A.readyState===WebSocket.CONNECTING))return;let n=A;n&&(n.onclose=null,n.close()),ye=e.url;let t=new WebSocket(e.url);A=t,O=!1,P(),t.onopen=()=>{A===t&&(O=!0,ae=0,P())},t.onmessage=a=>{if(A!==t)return;let i;try{i=JSON.parse(String(a.data))}catch{return}if(i.type==="activity.state")L=Array.isArray(i.events)?i.events.filter(pe):[],L.sort((c,o)=>c.seq-o.seq),Y===null&&(Y=Je()),P(!0);else if(i.type==="activity.event"&&pe(i.event)){let c=i.event;L.some(o=>o.id===c.id)||L.push(c),L=L.sort((o,r)=>o.seq-r.seq).slice(-200),!qe&&c.kind==="committee.opened"&&c.thread&&(Y=c.thread),P(!0),Pe(c)}},t.onclose=()=>{A===t&&(A=null,O=!1,P(),fe())},t.onerror=()=>{}}function Pe(e){let n=e.kind.startsWith("committee.");if(!Ce(e)&&!Se(e)&&!n)return;let t=document.querySelector("#cityMapFrame");if(!t)return;if(t.dataset.ready!=="1"){se=e;return}let a=new URL(t.src,location.href).origin;t.contentWindow?.postMessage({protocol:te,type:"activity.event",event:e},a)}function fe(){ae||(ae=window.setTimeout(async()=>{ae=0;try{He(await b("/api/live"))}catch{fe()}},1500))}function P(e=!1){let n=document.querySelector("#liveDot"),t=document.querySelector("#liveState"),a=document.querySelector("#liveEvents"),i=document.querySelector("#liveFilter"),c=document.querySelector("#liveContext"),o=document.querySelector("#liveWorkToggle");if(!n||!t||!a||!i||!c||!o)return;n.classList.toggle("on",O),t.textContent=O?s("websocket live"):ye?s("reconnecting"):s("session offline");let r=new Map;for(let w of L)w.thread&&(w.kind==="committee.opened"||w.kind==="conversation.user")&&!we(w)&&!r.has(w.thread)&&r.set(w.thread,w.summary);i.innerHTML=`<option value="">${s("all conversations")}</option>`+[...r.entries()].reverse().map(([w,y])=>`<option value="${l(w)}">${l(y.slice(0,54)||w)}</option>`).join("");let p=Y||"";i.value=[...i.options].some(w=>w.value===p)?p:"",i.onchange=()=>{Y=i.value,qe=!0,P(!0)},o.classList.toggle("on",U),o.setAttribute("aria-pressed",String(U)),o.textContent=U?s("hide work"):s("show work"),o.onclick=()=>{U=!U,P(!0)},Qe(i.value,c);let h=(i.value?L.filter(w=>w.thread===i.value):L).filter(w=>!we(w)&&(U||!Ke(w)||w.tone==="error"));if(!h.length){a.innerHTML=`<li class="liveEmpty">${O?s("The bus is live. Questions, positions and moderated replies will appear here."):s("Start the city session. Its visible conversation will appear here as it happens.")}</li>`;return}let v=a.scrollHeight-a.scrollTop-a.clientHeight<90;a.innerHTML=h.map(Ie).join(""),(e||v)&&(a.scrollTop=a.scrollHeight)}function Ie(e){let n=new Date(e.at),t=Number.isNaN(n.getTime())?"":n.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),a=e.role==="chair"?"chair":e.role==="member"?"member":"system",i=Oe(e.actor),c=e.target&&!["committee","seat"].includes(e.target)?`to ${e.target}`:e.target==="committee"?"to the committee":"",o=e.summary.trim(),r=Xe(o),p=r!==o,u=e.details.length?`<details class="liveEvidence"><summary>${e.details.length} ${e.details.length===1?"detail":"details"}</summary><ul>${e.details.map(v=>`<li>${l(v)}</li>`).join("")}</ul></details>`:"",h=p?`<details class="liveFull"><summary>${s("read full message")}</summary><p>${l(o)}</p></details>`:"";return`<li class="liveTurn ${a} ${l(e.tone)}">
193
- <div class="liveAvatar${d?.avatars?.[e.actor]?" conCara":""}"
196
+ </div>`}enlaza(n){let t=()=>void this.pinta(n);n.querySelectorAll("[data-bv]").forEach(i=>{i.onclick=async d=>{d.preventDefault();let o=i.dataset.bv;if(o==="salir")return this.p.vete("resumen");if(o==="atras")return this.paso=Math.max(0,this.paso-1),t();if(o==="dominio")return this.dominio=i.dataset.id??"",this.roles=[],this.rol="",t();if(o==="rol")return this.rol=i.dataset.id??"",t();if(o==="nuevo")return this.enCurso=new _({api:this.p.api,esc:this.p.esc,aviso:this.p.aviso,yo:this.p.yo},this.rolesDeAgente),t();if(o==="cancela")return this.enCurso=null,t();if(o==="guarda")return void await this.guardaAgente(n);if(o==="siguiente")return void await this.avanza(n);if(o==="sesion")return void await this.abreSesion();if(o==="ir-gente")return this.p.vete("gente");if(o==="ir-mapa")return this.p.vete("mapa");if(o==="ir-puesto")return this.p.vete("puesto")}});let a=n.querySelector("#bvCasa");a&&this.enCurso&&(this.enCurso.monta(a),a.querySelector("#bvNombre")?.focus())}async guardaAgente(n){let t=this.enCurso;if(t){this.guardando=!0,await this.pinta(n);try{await this.aseguraFicha();let a=await t.guarda();if(!a)return;this.roster.push({...t.datos,nombre:a.nombre}),this.enCurso=null,this.p.aviso(s("{name} joined the city",{name:a.nombre})),await this.p.refresca()}finally{this.guardando=!1,await this.pinta(n)}}}async aseguraFicha(){await this.p.api("/api/ficha",{method:"POST",body:JSON.stringify({domain:this.dominio||"software",role:this.rol||"blank"})})}async avanza(n){let t=this.paso===2;this.paso=Math.min(xe.length-1,this.paso+1),await this.pinta(n),t&&(await this.aseguraFicha(),await this.p.refresca())}async abreSesion(){try{let n=await this.p.api("/api/sesion",{method:"POST",body:JSON.stringify({user:this.p.yo})});this.p.aviso(n.ok&&n.attach?`Session built \u2014 attach with: ${n.attach}`:n.error??"Could not",!n.ok)}catch(n){this.p.aviso(String(n),!0)}}};var te="agents-city-map-activity/1";function Le(e){if(!e||typeof e!="object")return!1;let n=e;return n.protocol===te&&n.type==="map.nav"&&typeof n.view=="string"}function pe(e){if(!e||typeof e!="object")return!1;let n=e;return n.protocol==="agents-city-activity/1"&&typeof n.id=="string"&&typeof n.seq=="number"&&typeof n.kind=="string"&&typeof n.actor=="string"&&typeof n.summary=="string"&&typeof n.title=="string"&&Array.isArray(n.details)}function Se(e){return e.kind==="conversation.user"||e.kind==="conversation.agent"||e.kind==="runtime.session.started"||e.kind==="runtime.session.ended"}function Ce(e){return e.kind==="conversation.agent"||e.kind==="conversation.agent.commentary"?!!e.summary.trim():e.kind==="committee.opened"||e.kind==="committee.position.revealed"||e.kind==="committee.synthesis.published"||e.kind.startsWith("committee.floor.")||e.kind==="committee.decision.recorded"||e.kind.startsWith("committee.verification.")||e.kind==="committee.replanned"||e.kind==="committee.closed"||e.kind==="committee.command.rejected"&&e.tone==="error"}var F=window.PASE,oe=new URLSearchParams(location.search).get("city")??"";function ne(e,n,t){try{let a="/api/diario?PASE="+encodeURIComponent(F);oe&&(a+="&city="+encodeURIComponent(oe)),fetch(a,{method:"POST",headers:{"X-City-Pase":F,"Content-Type":"application/json"},body:JSON.stringify({que:e,detalle:n,donde:t??location.hash??""}),keepalive:!0}).catch(()=>{})}catch{}}async function y(e,n){let t=e+(e.includes("?")?"&":"?")+"PASE="+encodeURIComponent(F);oe&&(t+="&city="+encodeURIComponent(oe));let a;try{a=await fetch(t,{headers:{"X-City-Pase":F,"Content-Type":"application/json"},...n})}catch(d){throw e!=="/api/diario"&&ne("fetch failed",String(d),e),d}let i=await a.json();return e!=="/api/diario"&&(!a.ok||i?.error)&&ne("api refused",{estado:a.status,error:i?.error},e),i}function m(e,n=document){let t=n.querySelector(e);if(!t)throw new Error(`no element matches ${e}`);return t}function f(e,n=document){return[...n.querySelectorAll(e)]}function l(e){return String(e??"").replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}var N=e=>e.replace(c.casa,"~"),Ae=0;function g(e,n=!1){let t=m("#toast");t.textContent=e,t.className="ver"+(n?" mal":""),clearTimeout(Ae),Ae=window.setTimeout(()=>{t.className=""},3200)}var qe=e=>new Promise(n=>setTimeout(n,e)),c,S="mapa",Ye=[["resumen","Overview"],["mapa","The map"],["puesto","My seat"],["barrios","Districts"],["recepcion","Reception"],["red","Roads"],["committee","Committee"],["gente","Houses"],["demos","Demos"],["ciudades","Cities"]],ye=!0;async function k(){c=await y("/api/estado"),ye&&(ye=!1,!c.tarjetas.find(n=>n.user===c.yo)||(c.agents??[]).length===0?S="bienvenida":c.mapa||(S="resumen")),C(),Ve(),Ge(),He(c.live_bus)}function Ve(){let e=document.getElementById("avisoVersion");if(!e)return;let n=(c.update??"").trim();e.textContent=n,e.hidden=!n}function Ge(){let e=document.querySelector("#livePanel header"),n=document.getElementById("demoControles");if(!e||!c.demo){n?.remove();return}if(n)return;let t=document.createElement("div");t.id="demoControles",t.innerHTML=`<span>${s("guided committee")}</span><button id="demoReplay" type="button" title="${s("Play the guided committee from the top")}">${s("\u27F3 replay")}</button><button id="demoPausa" type="button" title="${s("Pause or resume mid-scene")}">${s("\u23F8 pause")}</button>`,e.appendChild(t);let a=m("#demoPausa",t),i=d=>y("/api/demo",{method:"POST",body:JSON.stringify({action:d})});m("#demoReplay",t).onclick=async()=>{let d=await i("restart");a.textContent="\u23F8 pause",d.ok||g("Could not restart the demo",!0)},a.onclick=async()=>{let d=a.textContent?.includes("resume")??!1,o=await i(d?"resume":"pause");if(!o.running){a.textContent="\u23F8 pause",g("Nothing playing \u2014 hit replay",!0);return}a.textContent=o.paused?"\u25B6 resume":"\u23F8 pause"}}var j=null,be="",L=[],O=!1,ae=0,Y=null,Re=!1,U=!1,se=null;function He(e){if(!e?.online||!e.url){O=!1,I(),fe();return}if(e.url===be&&j&&(j.readyState===WebSocket.OPEN||j.readyState===WebSocket.CONNECTING))return;let n=j;n&&(n.onclose=null,n.close()),be=e.url;let t=new WebSocket(e.url);j=t,O=!1,I(),t.onopen=()=>{j===t&&(O=!0,ae=0,I())},t.onmessage=a=>{if(j!==t)return;let i;try{i=JSON.parse(String(a.data))}catch{return}if(i.type==="activity.state")L=Array.isArray(i.events)?i.events.filter(pe):[],L.sort((d,o)=>d.seq-o.seq),Y===null&&(Y=Je()),I(!0);else if(i.type==="activity.event"&&pe(i.event)){let d=i.event;L.some(o=>o.id===d.id)||L.push(d),L=L.sort((o,r)=>o.seq-r.seq).slice(-200),!Re&&d.kind==="committee.opened"&&d.thread&&(Y=d.thread),I(!0),Ie(d)}},t.onclose=()=>{j===t&&(j=null,O=!1,I(),fe())},t.onerror=()=>{}}function Ie(e){let n=e.kind.startsWith("committee.");if(!Ce(e)&&!Se(e)&&!n)return;let t=document.querySelector("#cityMapFrame");if(!t)return;if(t.dataset.ready!=="1"){se=e;return}let a=new URL(t.src,location.href).origin;t.contentWindow?.postMessage({protocol:te,type:"activity.event",event:e},a)}function fe(){ae||(ae=window.setTimeout(async()=>{ae=0;try{He(await y("/api/live"))}catch{fe()}},1500))}function I(e=!1){let n=document.querySelector("#liveDot"),t=document.querySelector("#liveState"),a=document.querySelector("#liveEvents"),i=document.querySelector("#liveFilter"),d=document.querySelector("#liveContext"),o=document.querySelector("#liveWorkToggle");if(!n||!t||!a||!i||!d||!o)return;n.classList.toggle("on",O),t.textContent=O?s("websocket live"):be?s("reconnecting"):s("session offline");let r=new Map;for(let w of L)w.thread&&(w.kind==="committee.opened"||w.kind==="conversation.user")&&!we(w)&&!r.has(w.thread)&&r.set(w.thread,w.summary);i.innerHTML=`<option value="">${s("all conversations")}</option>`+[...r.entries()].reverse().map(([w,b])=>`<option value="${l(w)}">${l(b.slice(0,54)||w)}</option>`).join("");let p=Y||"";i.value=[...i.options].some(w=>w.value===p)?p:"",i.onchange=()=>{Y=i.value,Re=!0,I(!0)},o.classList.toggle("on",U),o.setAttribute("aria-pressed",String(U)),o.textContent=U?s("hide work"):s("show work"),o.onclick=()=>{U=!U,I(!0)},Qe(i.value,d);let h=(i.value?L.filter(w=>w.thread===i.value):L).filter(w=>!we(w)&&(U||!Ke(w)||w.tone==="error"));if(!h.length){a.innerHTML=`<li class="liveEmpty">${O?s("The bus is live. Questions, positions and moderated replies will appear here."):s("Start the city session. Its visible conversation will appear here as it happens.")}</li>`;return}let v=a.scrollHeight-a.scrollTop-a.clientHeight<90;a.innerHTML=h.map(Pe).join(""),(e||v)&&(a.scrollTop=a.scrollHeight)}function Pe(e){let n=new Date(e.at),t=Number.isNaN(n.getTime())?"":n.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),a=e.role==="chair"?"chair":e.role==="member"?"member":"system",i=Oe(e.actor),d=e.target&&!["committee","seat"].includes(e.target)?`to ${e.target}`:e.target==="committee"?"to the committee":"",o=e.summary.trim(),r=Xe(o),p=r!==o,u=e.details.length?`<details class="liveEvidence"><summary>${e.details.length} ${e.details.length===1?"detail":"details"}</summary><ul>${e.details.map(v=>`<li>${l(v)}</li>`).join("")}</ul></details>`:"",h=p?`<details class="liveFull"><summary>${s("read full message")}</summary><p>${l(o)}</p></details>`:"";return`<li class="liveTurn ${a} ${l(e.tone)}">
197
+ <div class="liveAvatar${c?.avatars?.[e.actor]?" conCara":""}"
194
198
  style="--actor-hue:${De(e.actor)}" aria-hidden="true">
195
199
  ${Ne(e.actor,e.role==="chair")}
196
200
  </div>
197
201
  <article class="liveBubble">
198
202
  <div class="liveMeta"><span class="liveActor">${l(e.actor)}</span>
199
203
  ${i?`<span class="liveRole">${l(i)}</span>`:""}
200
- ${c?`<span>${l(c)}</span>`:""}<span class="liveWhen">${l(t)}</span></div>
204
+ ${d?`<span>${l(d)}</span>`:""}<span class="liveWhen">${l(t)}</span></div>
201
205
  <div class="liveHeading">${Be(e.kind)}<h3>${l(et(e))}</h3></div>
202
206
  <p>${l(r)}</p>${h}${u}
203
207
  </article>
204
- </li>`}function Je(){return[...L].reverse().find(e=>!!e.thread&&(e.kind==="committee.opened"||e.kind==="conversation.user"&&!we(e)))?.thread||""}function Qe(e,n){let t=e?d?.deliberations?.find(v=>v.id===e):void 0,a=e?L.filter(v=>v.thread===e):[],i=a.find(v=>v.kind==="committee.opened");if(!t&&!i){n.innerHTML="",n.hidden=!0;return}let c=i?.details.find(v=>v.startsWith("Invited:"))?.slice(8).split(",").map(v=>v.trim()).filter(Boolean)||[],o=t?.participants||c,r=t?.received??new Set(a.filter(v=>v.kind==="committee.position.submitted").map(v=>v.actor)).size,p=t?.total??o.length,u=a.at(-1)?.phase||t?.status||"starting",h=["seat",...o];n.hidden=!1,n.innerHTML=`<div class="livePeople">${h.map(v=>`<span class="livePerson${d?.avatars?.[v]?" conCara":""}" title="${l(v==="seat"?"seat \xB7 chair":`${v} \xB7 ${Oe(v)||"member"}`)}" style="--actor-hue:${De(v)}">${Ne(v,v==="seat")}</span>`).join("")}</div><span class="liveContextText"><b>${s("seat moderates")}</b> \xB7 ${l(r)}/${l(p)} positions \xB7 ${l(u)}</span>`}function we(e){if(e.kind!=="conversation.user")return!1;let n=e.summary.trimStart();return n.startsWith("[Agents City authenticated local bus]")||n.startsWith("<channel")&&n.slice(0,500).includes("plugin:city:city-bus")}function Ke(e){return e.kind==="work.command.started"||e.kind==="work.command.completed"||e.kind==="runtime.turn.started"||e.kind==="runtime.turn.completed"||e.kind==="runtime.gateway.ready"||e.kind==="runtime.session.started"||e.kind==="runtime.session.ended"}function Oe(e){return e==="seat"?"chair":d?.skills?.[e]?.role||""}function Ne(e,n){let t=d?.avatars?.[e];return t&&t.startsWith("data:image/svg+xml;base64,")?`<img class="liveCara" src="${l(t)}" alt="">`:n?Be("chair"):l(Ze(e))}function Ze(e){let n=e.split(/[-_.\s]+/).filter(Boolean);return(n.length>1?n.slice(0,2).map(a=>a[0]).join(""):e.slice(0,2)).toUpperCase()}function De(e){let n=17;for(let t of e)n=(n*31+t.charCodeAt(0))%360;return n}function Xe(e){if(e.length<=520)return e;let n=e.slice(0,520).lastIndexOf(" ");return e.slice(0,n>360?n:520).trimEnd()+"\u2026"}function et(e){return e.kind==="conversation.user"?e.actor==="seat"?"Question":"Brief received":e.kind==="conversation.agent.commentary"?"Working note":e.kind==="conversation.agent"?"Response":e.kind==="committee.opened"?"Question to the committee":e.kind==="committee.position.submitted"?"Position sealed":e.kind==="committee.positions.revealed"?"Blind round complete":e.kind==="committee.position.revealed"?"Position":e.kind==="committee.synthesis.published"?"Chair synthesis":e.kind==="committee.floor.requested"?"Requests the floor":e.kind==="committee.floor.granted"?"Floor granted":e.kind==="committee.floor.denied"?"Floor denied":e.kind==="committee.floor.spoke"?"Intervention":e.kind==="committee.decision.recorded"?"Decision":e.kind.startsWith("committee.verification.")?"Verification":e.kind==="committee.closed"?"Committee closed":e.title}function Be(e){return e==="chair"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17h16l-1.4-9-4.1 3L12 5l-2.5 6-4.1-3L4 17Zm1 2h14"/></svg>':e.includes("question")||e==="committee.opened"||e==="conversation.user"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v10H9l-4 4V5Zm5 4a2 2 0 1 1 3.4 1.4c-.9.6-1.4 1-1.4 2.1M12 14.7v.1"/></svg>':e.includes("decision")||e.includes("verification")||e==="committee.closed"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>':e.includes("floor")?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 11V5a1.5 1.5 0 0 1 3 0v5-7a1.5 1.5 0 0 1 3 0v7-5a1.5 1.5 0 0 1 3 0v7l1-2a1.5 1.5 0 0 1 2.6 1.5L17 18a4 4 0 0 1-3.5 2H10a5 5 0 0 1-5-5v-3.5a1.5 1.5 0 0 1 3 0V13"/></svg>':e.includes("command")||e.includes("work.")?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8 4 4-4 4m6 0h6"/></svg>':'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>'}function ze(){let e=(d.agents??[]).length;m("#railCiudad").innerHTML=`<span class="railEtiqueta">${s("you are in")}</span>
205
- <b>${l(d.city_name)}</b>
206
- <span class="railDe">${l(d.domain)} \xB7 ${x(e,"{n} house","{n} houses")}</span>`,m("#rail").innerHTML=Ye.filter(([a])=>a!=="ciudades").map(([a,i])=>{let c=a==="demos"?"":a==="gente"?String(Object.keys(d.skills).length):a==="committee"?String(d.deliberations.length):a==="recepcion"?String(d.reception?.pending??0):a==="red"?String(d.roads.length):a==="barrios"?String(d.parcelas.length):"";return`<li class="${a===S?"aqui":""}" data-s="${a}" role="button"
208
+ </li>`}function Je(){return[...L].reverse().find(e=>!!e.thread&&(e.kind==="committee.opened"||e.kind==="conversation.user"&&!we(e)))?.thread||""}function Qe(e,n){let t=e?c?.deliberations?.find(v=>v.id===e):void 0,a=e?L.filter(v=>v.thread===e):[],i=a.find(v=>v.kind==="committee.opened");if(!t&&!i){n.innerHTML="",n.hidden=!0;return}let d=i?.details.find(v=>v.startsWith("Invited:"))?.slice(8).split(",").map(v=>v.trim()).filter(Boolean)||[],o=t?.participants||d,r=t?.received??new Set(a.filter(v=>v.kind==="committee.position.submitted").map(v=>v.actor)).size,p=t?.total??o.length,u=a.at(-1)?.phase||t?.status||"starting",h=["seat",...o];n.hidden=!1,n.innerHTML=`<div class="livePeople">${h.map(v=>`<span class="livePerson${c?.avatars?.[v]?" conCara":""}" title="${l(v==="seat"?"seat \xB7 chair":`${v} \xB7 ${Oe(v)||"member"}`)}" style="--actor-hue:${De(v)}">${Ne(v,v==="seat")}</span>`).join("")}</div><span class="liveContextText"><b>${s("seat moderates")}</b> \xB7 ${l(r)}/${l(p)} positions \xB7 ${l(u)}</span>`}function we(e){if(e.kind!=="conversation.user")return!1;let n=e.summary.trimStart();return n.startsWith("[Agents City authenticated local bus]")||n.startsWith("<channel")&&n.slice(0,500).includes("plugin:city:city-bus")}function Ke(e){return e.kind==="work.command.started"||e.kind==="work.command.completed"||e.kind==="runtime.turn.started"||e.kind==="runtime.turn.completed"||e.kind==="runtime.gateway.ready"||e.kind==="runtime.session.started"||e.kind==="runtime.session.ended"}function Oe(e){return e==="seat"?"chair":c?.skills?.[e]?.role||""}function Ne(e,n){let t=c?.avatars?.[e];return t&&t.startsWith("data:image/svg+xml;base64,")?`<img class="liveCara" src="${l(t)}" alt="">`:n?Be("chair"):l(Ze(e))}function Ze(e){let n=e.split(/[-_.\s]+/).filter(Boolean);return(n.length>1?n.slice(0,2).map(a=>a[0]).join(""):e.slice(0,2)).toUpperCase()}function De(e){let n=17;for(let t of e)n=(n*31+t.charCodeAt(0))%360;return n}function Xe(e){if(e.length<=520)return e;let n=e.slice(0,520).lastIndexOf(" ");return e.slice(0,n>360?n:520).trimEnd()+"\u2026"}function et(e){return e.kind==="conversation.user"?e.actor==="seat"?"Question":"Brief received":e.kind==="conversation.agent.commentary"?"Working note":e.kind==="conversation.agent"?"Response":e.kind==="committee.opened"?"Question to the committee":e.kind==="committee.position.submitted"?"Position sealed":e.kind==="committee.positions.revealed"?"Blind round complete":e.kind==="committee.position.revealed"?"Position":e.kind==="committee.synthesis.published"?"Chair synthesis":e.kind==="committee.floor.requested"?"Requests the floor":e.kind==="committee.floor.granted"?"Floor granted":e.kind==="committee.floor.denied"?"Floor denied":e.kind==="committee.floor.spoke"?"Intervention":e.kind==="committee.decision.recorded"?"Decision":e.kind.startsWith("committee.verification.")?"Verification":e.kind==="committee.closed"?"Committee closed":e.title}function Be(e){return e==="chair"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17h16l-1.4-9-4.1 3L12 5l-2.5 6-4.1-3L4 17Zm1 2h14"/></svg>':e.includes("question")||e==="committee.opened"||e==="conversation.user"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v10H9l-4 4V5Zm5 4a2 2 0 1 1 3.4 1.4c-.9.6-1.4 1-1.4 2.1M12 14.7v.1"/></svg>':e.includes("decision")||e.includes("verification")||e==="committee.closed"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>':e.includes("floor")?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 11V5a1.5 1.5 0 0 1 3 0v5-7a1.5 1.5 0 0 1 3 0v7-5a1.5 1.5 0 0 1 3 0v7l1-2a1.5 1.5 0 0 1 2.6 1.5L17 18a4 4 0 0 1-3.5 2H10a5 5 0 0 1-5-5v-3.5a1.5 1.5 0 0 1 3 0V13"/></svg>':e.includes("command")||e.includes("work.")?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8 4 4-4 4m6 0h6"/></svg>':'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>'}function ze(){let e=(c.agents??[]).length;m("#railCiudad").innerHTML=`<span class="railEtiqueta">${s("you are in")}</span>
209
+ <b>${l(c.city_name)}</b>
210
+ <span class="railDe">${l(c.domain)} \xB7 ${x(e,"{n} house","{n} houses")}</span>`,m("#rail").innerHTML=Ye.filter(([a])=>a!=="ciudades").map(([a,i])=>{let d=a==="demos"?"":a==="gente"?String(Object.keys(c.skills).length):a==="committee"?String(c.deliberations.length):a==="recepcion"?String(c.reception?.pending??0):a==="red"?String(c.roads.length):a==="barrios"?String(c.parcelas.length):"";return`<li class="${a===S?"aqui":""}" data-s="${a}" role="button"
207
211
  tabindex="0" ${a===S?'aria-current="page"':""}>
208
- <span>${l(s(i))}</span>${c?`<span class="n">${c}</span>`:""}</li>`}).join(""),m("#railOtras").innerHTML=`<li class="${S==="ciudades"?"aqui":""}" data-s="ciudades" role="button"
212
+ <span>${l(s(i))}</span>${d?`<span class="n">${d}</span>`:""}</li>`}).join(""),m("#railOtras").innerHTML=`<li class="${S==="ciudades"?"aqui":""}" data-s="ciudades" role="button"
209
213
  tabindex="0" ${S==="ciudades"?'aria-current="page"':""}>
210
- <span>${s("All cities")}</span><span class="n">${d.ciudades.length}</span></li>`,f("#rail li,#railOtras li").forEach(a=>{let i=()=>{document.body.classList.remove("enGuia"),S=a.dataset.s??"mapa",C()};a.onclick=i,a.onkeydown=c=>{c.key!=="Enter"&&c.key!==" "||(c.preventDefault(),i())}}),m("#dondeDatos").textContent=N(d.datos);let n=m("#ciudades");d.ciudades.length>1?(n.innerHTML=`<select id="cambiaCiudad" aria-label="${s("which city this hall manages")}"
214
+ <span>${s("All cities")}</span><span class="n">${c.ciudades.length}</span></li>`,f("#rail li,#railOtras li").forEach(a=>{let i=()=>{document.body.classList.remove("enGuia"),S=a.dataset.s??"mapa",C()};a.onclick=i,a.onkeydown=d=>{d.key!=="Enter"&&d.key!==" "||(d.preventDefault(),i())}}),m("#dondeDatos").textContent=N(c.datos);let n=m("#ciudades");c.ciudades.length>1?(n.innerHTML=`<select id="cambiaCiudad" aria-label="${s("which city this hall manages")}"
211
215
  title="${s("which city this hall manages")}">
212
- ${d.ciudades.map(a=>`<option value="${l(a.ruta)}" ${a.actual?"selected":""}>
213
- ${l(a.nombre)}</option>`).join("")}</select>`,m("#cambiaCiudad").onchange=a=>{let i=a.target.value;location.href="/?PASE="+encodeURIComponent(F)+"&city="+encodeURIComponent(i)}):n.innerHTML="";let t=m("#alWizard");t.onclick=async a=>{a.preventDefault();let c=(await I({titulo:s("Start another city"),cuerpo:[s("A city is a place with its own seat, its own houses and its own map. Yours stay where they are.")],campos:[{id:"name",etiqueta:s("What is it called?"),pista:s("home, clients, the lab \u2014 whatever you would say out loud"),requerido:!0}],aceptar:s("Create it")}))?.name;if(!c?.trim())return;let o=await b("/api/ciudades",{method:"POST",body:JSON.stringify({name:c})});if(o.error||!o.city)return g(o.error??"Could not create the city",!0);location.href="/?PASE="+encodeURIComponent(F)+"&city="+encodeURIComponent(o.city)}}var ie=null;function C(){ie?.desmonta?.(),ie=null,ze(),m(".cuerpo").style.padding="",m("#lienzo").style.maxWidth="",T[S](),m(".cuerpo").scrollTop=0}var T={},me=null;T.bienvenida=()=>{document.body.classList.add("enGuia"),me??=new ee({api:b,esc:l,aviso:g,refresca:k,vete:e=>{document.body.classList.remove("enGuia"),S=e,C()},ciudad:d.city_name,yo:d.yo,datos:N(d.datos)}),me.monta(m("#lienzo")),ie=me};T.ciudades=()=>{let e=d.ciudades.map(a=>`
216
+ ${c.ciudades.map(a=>`<option value="${l(a.ruta)}" ${a.actual?"selected":""}>
217
+ ${l(a.nombre)}</option>`).join("")}</select>`,m("#cambiaCiudad").onchange=a=>{let i=a.target.value;location.href="/?PASE="+encodeURIComponent(F)+"&city="+encodeURIComponent(i)}):n.innerHTML="";let t=m("#alWizard");t.onclick=async a=>{a.preventDefault();let d=(await P({titulo:s("Start another city"),cuerpo:[s("A city is a place with its own seat, its own houses and its own map. Yours stay where they are.")],campos:[{id:"name",etiqueta:s("What is it called?"),pista:s("home, clients, the lab \u2014 whatever you would say out loud"),requerido:!0}],aceptar:s("Create it")}))?.name;if(!d?.trim())return;let o=await y("/api/ciudades",{method:"POST",body:JSON.stringify({name:d})});if(o.error||!o.city)return g(o.error??"Could not create the city",!0);location.href="/?PASE="+encodeURIComponent(F)+"&city="+encodeURIComponent(o.city)}}var ie=null;function C(){ie?.desmonta?.(),ie=null,ze(),m(".cuerpo").style.padding="",m("#lienzo").style.maxWidth="",T[S](),m(".cuerpo").scrollTop=0}var T={},me=null;T.bienvenida=()=>{document.body.classList.add("enGuia"),me??=new ee({api:y,esc:l,aviso:g,refresca:k,vete:e=>{document.body.classList.remove("enGuia"),S=e,C()},ciudad:c.city_name,yo:c.yo,datos:N(c.datos)}),me.monta(m("#lienzo")),ie=me};T.ciudades=()=>{let e=c.ciudades.map(a=>`
214
218
  <div class="fila ${a.actual?"on":""}">
215
219
  <span class="et">${l(a.nombre)}${a.actual?' <b class="de">\xB7 open now</b>':""}</span>
216
220
  <span class="de">${a.agentes??0} house${(a.agentes??0)===1?"":"s"} of its own</span>
@@ -236,35 +240,35 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
236
240
  </div>
237
241
  <div class="peligro">
238
242
  <h3>${s("Start this city over")}</h3>
239
- <p class="prosa">${s("Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> \u2014 only the city that points at them.",{city:`<b>${l(d.city_name)}</b>`})}</p>
243
+ <p class="prosa">${s("Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> \u2014 only the city that points at them.",{city:`<b>${l(c.city_name)}</b>`})}</p>
240
244
  <button class="bt malo" id="reiniciaCiudad">${s("Start over\u2026")}</button>
241
245
  </div>
242
246
  <p class="pista">Archiving <b>moves</b> a city into
243
247
  <code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
244
248
  <code class="mono">mv</code>. The city you are standing in, and the last one you own,
245
- cannot be archived.</p>`,re();let n=m("#nuevaCiudad");m("#creaCiudad").onclick=async()=>{let a=n.value.trim();if(!a){g("Give it a name",!0);return}let i=await b("/api/ciudades",{method:"POST",body:JSON.stringify({name:a})});if(!i.ok){g(i.error??"Could not create it",!0);return}g(`${a} created`),await k()};let t=document.getElementById("reiniciaCiudad");t&&(t.onclick=async()=>{let a=await b("/api/ciudad-reinicia",{method:"POST",body:"{}"});if(!a.preview){g(a.error??"Could not read what a reset would do",!0);return}let i=a.preview;if(await I({titulo:s("Start over {city}?",{city:i.city}),cuerpo:[s("You lose:")+" "+i.loses.join(", ")+".",s("Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).",{agents:i.agents,roads:i.roads,acts:i.deliberations}),s("You keep:")+" "+i.keeps.join(", ")+"."],campos:[{id:"nombre",etiqueta:s("Type the city\u2019s name to confirm"),requerido:!0}],exige:i.city,aceptar:s("Start over"),peligro:!0})===null)return;let o=await b("/api/ciudad-reinicia",{method:"POST",body:JSON.stringify({confirm:i.city})});if(!o.ok){g(o.error??"Could not reset it",!0);return}g(`${i.city} starts over \u2014 the old one is at ${o.backup??"its backup"}`),be=!0,await k()}),f(".archiva").forEach(a=>{a.onclick=async()=>{let i=a.dataset.nombre??"";if(!await Z(s("Archive {city}?",{city:i}),[s("It moves into .backups. Nothing is deleted, and you can put it back by hand.")],{aceptar:s("Archive it")}))return;let o=await b("/api/ciudad-archiva",{method:"POST",body:JSON.stringify({city:a.dataset.ciudad??""})});if(!o.ok){g(o.error??"Could not archive it",!0);return}g(`${i} archived \u2014 it is in ${o.backup??".backups"}`),await k()}})};var he=null;T.demos=()=>{he??=new X({api:b,esc:l,pinta:Ie}),m("#lienzo").innerHTML='<div id="demoHueco"></div>',he.monta(m("#demoHueco")),ie=he};T.resumen=()=>{let e=d.tarjetas.find(r=>r.user===d.yo),n=d.parcelas.filter(r=>r.unidad==="none"||r.unidad==="mine").length,t=d.agents??[],a=[{ok:!!e,txt:e?`Your seat is taken \u2014 you are <b>${l(e.agent)}</b> in <b>${l(d.domain)}</b>`:"Take your seat: the work domain and your role in it",ir:"puesto"},{ok:t.length>0,txt:t.length===0?"Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on":`<b>${t.length}</b> house${t.length===1?"":"s"} in this city: ${l(t.map(r=>r.name).join(", "))}`,ir:"gente"},{ok:t.length===0||t.some(r=>(r.mounts??[]).length),txt:t.some(r=>(r.mounts??[]).length)?"Every house works on something real":"Give a house something to work on \u2014 a repository, a worktree, a folder of documents",ir:"gente"},{ok:!!(e&&e.goals_defined),txt:e&&e.goals_defined?`Your goal: <b>${l(e.objetivo?e.objetivo.title:"")}</b>`:"Set <b>one goal</b>, with the command that measures it",ir:"puesto"},{ok:d.parcelas.length===0||n===0,txt:d.parcelas.length===0?"Districts appear on the map once agents have houses":n?`<b>${n}</b> of ${d.parcelas.length} houses sit in no real district \u2014 assign them`:"Every house has its district",ir:"barrios"}],i=!e||t.length===0,c=d.tmux.includes(d.sesion);m("#lienzo").innerHTML=`
249
+ cannot be archived.</p>`,re();let n=m("#nuevaCiudad");m("#creaCiudad").onclick=async()=>{let a=n.value.trim();if(!a){g("Give it a name",!0);return}let i=await y("/api/ciudades",{method:"POST",body:JSON.stringify({name:a})});if(!i.ok){g(i.error??"Could not create it",!0);return}g(`${a} created`),await k()};let t=document.getElementById("reiniciaCiudad");t&&(t.onclick=async()=>{let a=await y("/api/ciudad-reinicia",{method:"POST",body:"{}"});if(!a.preview){g(a.error??"Could not read what a reset would do",!0);return}let i=a.preview;if(await P({titulo:s("Start over {city}?",{city:i.city}),cuerpo:[s("You lose:")+" "+i.loses.join(", ")+".",s("Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).",{agents:i.agents,roads:i.roads,acts:i.deliberations}),s("You keep:")+" "+i.keeps.join(", ")+"."],campos:[{id:"nombre",etiqueta:s("Type the city\u2019s name to confirm"),requerido:!0}],exige:i.city,aceptar:s("Start over"),peligro:!0})===null)return;let o=await y("/api/ciudad-reinicia",{method:"POST",body:JSON.stringify({confirm:i.city})});if(!o.ok){g(o.error??"Could not reset it",!0);return}g(`${i.city} starts over \u2014 the old one is at ${o.backup??"its backup"}`),ye=!0,await k()}),f(".archiva").forEach(a=>{a.onclick=async()=>{let i=a.dataset.nombre??"";if(!await Z(s("Archive {city}?",{city:i}),[s("It moves into .backups. Nothing is deleted, and you can put it back by hand.")],{aceptar:s("Archive it")}))return;let o=await y("/api/ciudad-archiva",{method:"POST",body:JSON.stringify({city:a.dataset.ciudad??""})});if(!o.ok){g(o.error??"Could not archive it",!0);return}g(`${i} archived \u2014 it is in ${o.backup??".backups"}`),await k()}})};var he=null;T.demos=()=>{he??=new X({api:y,esc:l,pinta:Pe}),m("#lienzo").innerHTML='<div id="demoHueco"></div>',he.monta(m("#demoHueco")),ie=he};T.resumen=()=>{let e=c.tarjetas.find(r=>r.user===c.yo),n=c.parcelas.filter(r=>r.unidad==="none"||r.unidad==="mine").length,t=c.agents??[],a=[{ok:!!e,txt:e?`Your seat is taken \u2014 you are <b>${l(e.agent)}</b> in <b>${l(c.domain)}</b>`:"Take your seat: the work domain and your role in it",ir:"puesto"},{ok:t.length>0,txt:t.length===0?"Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on":`<b>${t.length}</b> house${t.length===1?"":"s"} in this city: ${l(t.map(r=>r.name).join(", "))}`,ir:"gente"},{ok:t.length===0||t.some(r=>(r.mounts??[]).length),txt:t.some(r=>(r.mounts??[]).length)?"Every house works on something real":"Give a house something to work on \u2014 a repository, a worktree, a folder of documents",ir:"gente"},{ok:!!(e&&e.goals_defined),txt:e&&e.goals_defined?`Your goal: <b>${l(e.objetivo?e.objetivo.title:"")}</b>`:"Set <b>one goal</b>, with the command that measures it",ir:"puesto"},{ok:c.parcelas.length===0||n===0,txt:c.parcelas.length===0?"Districts appear on the map once agents have houses":n?`<b>${n}</b> of ${c.parcelas.length} houses sit in no real district \u2014 assign them`:"Every house has its district",ir:"barrios"}],i=!e||t.length===0,d=c.tmux.includes(c.sesion);m("#lienzo").innerHTML=`
246
250
  <div><span class="sub">${i?"welcome":"your city"}</span>
247
- <h1 style="margin-top:6px">${i?"Let\u2019s build "+l(d.city_name):l(d.city_name)}</h1>
248
- <p class="de mono" style="margin-top:5px">${l(d.address)}</p>
251
+ <h1 style="margin-top:6px">${i?"Let\u2019s build "+l(c.city_name):l(c.city_name)}</h1>
252
+ <p class="de mono" style="margin-top:5px">${l(c.address)}</p>
249
253
  ${i?`<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
250
254
  it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
251
255
  it \u2014 then add the agents, each one asked for whole: what kind of work it does,
252
256
  its role, everything it works on, the engine that runs it and the skills it
253
257
  starts with. Everything below is a plain file in
254
- <code class="mono">${l(N(d.datos))}</code> you can also edit by hand.</p>`:`<p class="prosa" style="margin-top:8px">Domain: <b>${l(d.domain)}</b>. Growth is
255
- counted with <code class="mono">${l(d.grow||"nothing yet")}</code>.</p>`}</div>
258
+ <code class="mono">${l(N(c.datos))}</code> you can also edit by hand.</p>`:`<p class="prosa" style="margin-top:8px">Domain: <b>${l(c.domain)}</b>. Growth is
259
+ counted with <code class="mono">${l(c.grow||"nothing yet")}</code>.</p>`}</div>
256
260
  <div class="cifras">
257
- <div class="cifra"><b>${Object.keys(d.skills).length}</b><span>${s("repo agents")}</span></div>
258
- <div class="cifra"><b>${d.parcelas.length}</b><span>houses</span></div>
259
- <div class="cifra"><b>${d.roads.length}</b><span>roads</span></div>
260
- <div class="cifra"><b>${d.deliberations.length}</b><span>${s("committee acts")}</span></div>
261
- <div class="cifra"><b>${Object.values(d.skills).reduce((r,p)=>r+p.skills.length,0)}</b><span>${s("skills recognised")}</span></div>
261
+ <div class="cifra"><b>${Object.keys(c.skills).length}</b><span>${s("repo agents")}</span></div>
262
+ <div class="cifra"><b>${c.parcelas.length}</b><span>houses</span></div>
263
+ <div class="cifra"><b>${c.roads.length}</b><span>roads</span></div>
264
+ <div class="cifra"><b>${c.deliberations.length}</b><span>${s("committee acts")}</span></div>
265
+ <div class="cifra"><b>${Object.values(c.skills).reduce((r,p)=>r+p.skills.length,0)}</b><span>${s("skills recognised")}</span></div>
262
266
  </div>
263
267
  <div class="luces">
264
- <span class="luz ${d.tarjetas.length?"on":""}">${s("data repo")}</span>
265
- <span class="luz ${d.gh?"on":"neutra"}">github${d.gh?"":" \u2014 optional"}</span>
266
- <span class="luz ${d.plugin?"on":""}">${s("plugin installed")}</span>
267
- <span class="luz ${c?"on":"neutra"}">tmux session${c?" up":""}</span>
268
+ <span class="luz ${c.tarjetas.length?"on":""}">${s("data repo")}</span>
269
+ <span class="luz ${c.gh?"on":"neutra"}">github${c.gh?"":" \u2014 optional"}</span>
270
+ <span class="luz ${c.plugin?"on":""}">${s("plugin installed")}</span>
271
+ <span class="luz ${d?"on":"neutra"}">tmux session${d?" up":""}</span>
268
272
  </div>
269
273
  <div><span class="sub">${s("what is left")}</span>
270
274
  <div class="tareas" style="margin-top:9px">${a.map(r=>`
@@ -274,15 +278,15 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
274
278
  <div><span class="sub">work</span>
275
279
  <div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
276
280
  <div class="orden"><span class="et2">${s("your day")}</span>
277
- <code>${c?"tmux attach -t "+l(d.sesion):"one window per folder, an agent in each"}</code>
278
- ${c?`<button class="bt mini" data-copia="tmux attach -t ${l(d.sesion)}">copy</button>`:`<button class="bt mini ppal" id="abreSesion">${s("open my session")}</button>`}
281
+ <code>${d?"tmux attach -t "+l(c.sesion):"one window per folder, an agent in each"}</code>
282
+ ${d?`<button class="bt mini" data-copia="tmux attach -t ${l(c.sesion)}">copy</button>`:`<button class="bt mini ppal" id="abreSesion">${s("open my session")}</button>`}
279
283
  </div>
280
284
  <div class="orden"><span class="et2">the map</span>
281
- <code>${d.mapa?l(d.mapa):"not drawn yet"}</code>
282
- <button class="bt mini ${d.mapa?"":"ppal"}" data-ir="mapa">${d.mapa?"open":"draw it"}</button></div>
283
- </div></div>`,re();let o=document.querySelector("#abreSesion");o&&(o.onclick=async()=>{o.disabled=!0,o.textContent="building\u2026";let r=await b("/api/sesion",{method:"POST",body:JSON.stringify({user:d.yo})});await Re(1500),await k(),g(r.ok&&r.attach?`Session built \u2014 attach with: ${r.attach}`:"Could not build it",!r.ok)})};T.mapa=()=>{if(d.mapa){m(".cuerpo").style.padding="0",m("#lienzo").style.maxWidth="none";let n=new URL(d.mapa,location.href);n.searchParams.set("embed","1"),n.searchParams.set("parent_origin",location.origin),m("#lienzo").innerHTML=`<iframe id="cityMapFrame" src="${l(n.toString())}" title="the map"
285
+ <code>${c.mapa?l(c.mapa):"not drawn yet"}</code>
286
+ <button class="bt mini ${c.mapa?"":"ppal"}" data-ir="mapa">${c.mapa?"open":"draw it"}</button></div>
287
+ </div></div>`,re();let o=document.querySelector("#abreSesion");o&&(o.onclick=async()=>{o.disabled=!0,o.textContent="building\u2026";let r=await y("/api/sesion",{method:"POST",body:JSON.stringify({user:c.yo})});await qe(1500),await k(),g(r.ok&&r.attach?`Session built \u2014 attach with: ${r.attach}`:"Could not build it",!r.ok)})};T.mapa=()=>{if(c.mapa){m(".cuerpo").style.padding="0",m("#lienzo").style.maxWidth="none";let n=new URL(c.mapa,location.href);n.searchParams.set("embed","1"),n.searchParams.set("parent_origin",location.origin),m("#lienzo").innerHTML=`<iframe id="cityMapFrame" src="${l(n.toString())}" title="the map"
284
288
  allow="fullscreen" allowfullscreen
285
- style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;let t=m("#cityMapFrame");t.onload=()=>{t.dataset.ready="1";let a=new URL(t.src,location.href).origin;if(t.contentWindow?.postMessage({protocol:te,type:"map.config",roads:d.roads.map(i=>({name:i.name,address:i.address})),agents:(d.agents??[]).map(i=>({name:i.name,kind:i.kind})),avatars:d.avatars??{}},a),t.contentWindow?.postMessage({type:"agents-city-map-theme/1",theme:lt()},a),se){let i=se;se=null,Pe(i)}};return}let e=(d.agents??[]).length;m("#lienzo").innerHTML=`<div><span class="sub">the map</span>
289
+ style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;let t=m("#cityMapFrame");t.onload=()=>{t.dataset.ready="1";let a=new URL(t.src,location.href).origin;if(t.contentWindow?.postMessage({protocol:te,type:"map.config",roads:c.roads.map(i=>({name:i.name,address:i.address})),agents:(c.agents??[]).map(i=>({name:i.name,kind:i.kind})),avatars:c.avatars??{}},a),t.contentWindow?.postMessage({type:"agents-city-map-theme/1",theme:lt()},a),se){let i=se;se=null,Ie(i)}};return}let e=(c.agents??[]).length;m("#lienzo").innerHTML=`<div><span class="sub">the map</span>
286
290
  <h1 style="margin-top:6px">Your city, drawn</h1>
287
291
  <p class="prosa" style="margin-top:8px">${e?`<b>${e}</b> agent${e===1?"":"s"} live here, and the map shows
288
292
  them as houses that grow with the work they do \u2014 with the town hall in the middle
@@ -292,12 +296,12 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
292
296
  The first time takes about a minute: it builds the front end and seeds a local database.
293
297
  After that it opens instantly, and it is yours \u2014 nothing is uploaded anywhere.</p></div>
294
298
  <div><button class="bt ppal" id="arrancaMapa">${e?"Draw my city":"Draw it anyway"}</button></div>
295
- <p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`,m("#arrancaMapa").onclick=async()=>{m("#arrancaMapa").disabled=!0,m("#mapaEspera").style.display="",await b("/api/mapa",{method:"POST",body:"{}"});for(let n=0;n<60;n++)if(await Re(3e3),d=await b("/api/estado"),d.mapa){C();return}g("It did not come up \u2014 run ./bin/city by hand and look at its output",!0),C()}};T.puesto=()=>{tt()};async function tt(){let e=d.tarjetas.find(h=>h.user===d.yo);m("#lienzo").innerHTML=`<div><span class="sub">${s("my seat")}</span>
296
- <h1 style="margin-top:6px">${l(d.yo)}</h1>
299
+ <p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`,m("#arrancaMapa").onclick=async()=>{m("#arrancaMapa").disabled=!0,m("#mapaEspera").style.display="",await y("/api/mapa",{method:"POST",body:"{}"});for(let n=0;n<60;n++)if(await qe(3e3),c=await y("/api/estado"),c.mapa){C();return}g("It did not come up \u2014 run ./bin/city by hand and look at its output",!0),C()}};T.puesto=()=>{tt()};async function tt(){let e=c.tarjetas.find(h=>h.user===c.yo);m("#lienzo").innerHTML=`<div><span class="sub">${s("my seat")}</span>
300
+ <h1 style="margin-top:6px">${l(c.yo)}</h1>
297
301
  <p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
298
302
  role inside it, and one goal. The seat stays the boss even when its
299
303
  professional role is blank. Saving writes your card \u2014
300
- <code class="mono">${l(N(d.datos))}/${l(d.yo)}.md</code> \u2014 the same file
304
+ <code class="mono">${l(N(c.datos))}/${l(c.yo)}.md</code> \u2014 the same file
301
305
  every other door writes, and it never touches your roster.</p></div>
302
306
  <div><span class="sub">${s("work domain")}</span><div class="rolejilla" id="domains" style="margin-top:9px">
303
307
  <p class="cargando">${s("reading the domain packs")}</p></div></div>
@@ -312,10 +316,10 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
312
316
  <div><span class="sub">${s("one goal \u2014 optional")}</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
313
317
  <div style="display:flex;gap:10px;align-items:center">
314
318
  <button class="bt ppal" id="guardaPuesto">${s("Save my seat")}</button>
315
- <span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;let n=(await b("/api/domains")).domains,t=d.domain||d.kind||"software",a=e?e.role:"",i=[],c=()=>{m("#roles").innerHTML=i.map(h=>`
319
+ <span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;let n=(await y("/api/domains")).domains,t=c.domain||c.kind||"software",a=e?e.role:"",i=[],d=()=>{m("#roles").innerHTML=i.map(h=>`
316
320
  <button class="rol ${h.id===a?"on":""}" data-r="${l(h.id)}">
317
321
  <h3>${l(h.name)}</h3><span class="de">${l(h.id)} \xB7 ${l(h.trade.toLowerCase())}</span>
318
- <p>${l(h.summary)}</p></button>`).join(""),f("#roles .rol").forEach(h=>{h.onclick=()=>{a=h.dataset.r??"",c()}})},o=async()=>{i=(await b("/api/roles?domain="+encodeURIComponent(t))).roles,i.some(h=>h.id===a)||(a=""),c()},r=()=>{m("#domains").innerHTML=n.map(h=>`
322
+ <p>${l(h.summary)}</p></button>`).join(""),f("#roles .rol").forEach(h=>{h.onclick=()=>{a=h.dataset.r??"",d()}})},o=async()=>{i=(await y("/api/roles?domain="+encodeURIComponent(t))).roles,i.some(h=>h.id===a)||(a=""),d()},r=()=>{m("#domains").innerHTML=n.map(h=>`
319
323
  <button class="rol ${h.id===t?"on":""}" data-d="${l(h.id)}">
320
324
  <h3>${l(h.name)}</h3><span class="de">${l(h.id)}</span>
321
325
  <p>${l(h.summary)}</p></button>`).join(""),f("#domains .rol").forEach(h=>{h.onclick=()=>{t=h.dataset.d??"software",r(),o()}})};r(),await o();let p=e&&e.objetivo||{};m("#meta").innerHTML=`
@@ -341,7 +345,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
341
345
  <input type="text" class="mono" id="g_target" value="${l(p.target)}"></div>
342
346
  </div>
343
347
  <div class="campo" style="max-width:240px"><label>${s("By when")}</label>
344
- <input type="text" id="g_by" value="${l(p.by??"this quarter")}"></div>`;let u=h=>m(h).value.trim();m("#guardaPuesto").onclick=async()=>{if(!a){g("Pick a role \u2014 it is the name your seat answers to",!0);return}m("#puestoEstado").style.display="";let h=await b("/api/ficha",{method:"POST",body:JSON.stringify({user:d.yo,domain:t,role:a,objetivo:{title:u("#g_title"),signal:u("#g_signal"),command:u("#g_command"),manual:u("#g_manual"),baseline:u("#g_baseline"),target:u("#g_target"),by:u("#g_by")||"this quarter"}})});await k(),g(h.ok?`Card written \u2014 you are ${h.user??d.yo}`:h.error??"It could not write",!h.ok)}}T.barrios=()=>{let e=d.unidades.map(c=>({...c})),n=new Set(d.lab),t={};for(let c of d.parcelas)(t[c.repo]=t[c.repo]??[]).push({ruta:c.ruta,unidad:c.unidad,nombre:c.nombre});let a=c=>c.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,20),i=()=>{let c=new Set([...e.map(o=>o.id),"none"]);m("#lienzo").innerHTML=`<div><span class="sub">${s("districts & houses")}</span>
348
+ <input type="text" id="g_by" value="${l(p.by??"this quarter")}"></div>`;let u=h=>m(h).value.trim();m("#guardaPuesto").onclick=async()=>{if(!a){g("Pick a role \u2014 it is the name your seat answers to",!0);return}m("#puestoEstado").style.display="";let h=await y("/api/ficha",{method:"POST",body:JSON.stringify({user:c.yo,domain:t,role:a,objetivo:{title:u("#g_title"),signal:u("#g_signal"),command:u("#g_command"),manual:u("#g_manual"),baseline:u("#g_baseline"),target:u("#g_target"),by:u("#g_by")||"this quarter"}})});await k(),g(h.ok?`Card written \u2014 you are ${h.user??c.yo}`:h.error??"It could not write",!h.ok)}}T.barrios=()=>{let e=c.unidades.map(d=>({...d})),n=new Set(c.lab),t={};for(let d of c.parcelas)(t[d.repo]=t[d.repo]??[]).push({ruta:d.ruta,unidad:d.unidad,nombre:d.nombre});let a=d=>d.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,20),i=()=>{let d=new Set([...e.map(o=>o.id),"none"]);m("#lienzo").innerHTML=`<div><span class="sub">${s("districts & houses")}</span>
345
349
  <h1 style="margin-top:6px">${s("The modelling no tool can do for you")}</h1>
346
350
  <p class="prosa" style="margin-top:8px">${s(`A house is <b>not a repo</b> \u2014 it is a
347
351
  parcel, a slice of one serving a single business unit. Split the interesting
@@ -363,7 +367,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
363
367
  <div class="tr">
364
368
  <input type="text" value="${l(o.name)}" data-i="${r}" style="flex:1">
365
369
  <span class="id">${l(o.id)}</span>
366
- <span class="pips" data-i="${r}">${d.paleta.map(p=>`
370
+ <span class="pips" data-i="${r}">${c.paleta.map(p=>`
367
371
  <button class="pip ${p.hex===o.color?"on":""}" data-c="${p.hex}"
368
372
  style="background:#${p.hex}" title="${l(p.nombre)}"></button>`).join("")}</span>
369
373
  <button class="x" data-x="${r}">\xD7</button></div>`).join("")||'<div class="tr" style="color:var(--tinta3);font-size:12.5px">No districts yet \u2014 everything sits in \u201Cno unit\u201D.</div>',m("#parcelas").innerHTML=Object.keys(t).sort().map(o=>`
@@ -379,16 +383,16 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
379
383
  placeholder="glob inside the repo \u2014 empty = the whole repo">
380
384
  <select data-k="unidad" data-r="${l(o)}" data-j="${p}">
381
385
  ${[...e.map(u=>u.id),"none"].map(u=>`
382
- <option value="${l(u)}" ${(c.has(r.unidad)?r.unidad:"none")===u?"selected":""}>${l(u)}</option>`).join("")}
386
+ <option value="${l(u)}" ${(d.has(r.unidad)?r.unidad:"none")===u?"selected":""}>${l(u)}</option>`).join("")}
383
387
  </select>
384
388
  ${(t[o]??[]).length>1?`<button class="x" data-quita="${l(o)}" data-j="${p}">\xD7</button>`:"<span></span>"}
385
389
  </div>`).join("")}
386
- </div>`).join("")||'<p class="prosa">No houses yet. Pick your folders in <b>My seat</b> and each becomes one.</p>',f("#unidades input[data-i]").forEach(o=>{o.oninput=()=>{let r=e[Number(o.dataset.i)];if(!r)return;r.name=o.value,r.id=a(o.value)||r.id;let p=o.closest(".tr")?.querySelector(".id");p&&(p.textContent=r.id)}}),f("#unidades .pip").forEach(o=>{o.onclick=()=>{let r=o.closest(".pips"),p=e[Number(r?.dataset.i)];p&&(p.color=o.dataset.c??p.color,i())}}),f("#unidades .x[data-x]").forEach(o=>{o.onclick=()=>{e.splice(Number(o.dataset.x),1),i()}}),m("#masU").onclick=()=>{let o=m("#nuevaU").value.trim();if(!o)return;let r=e.map(u=>u.color),p=d.paleta.find(u=>!r.includes(u.hex))??d.paleta[0];e.push({id:a(o),name:o,color:p?p.hex:"c8b48a"}),i()},m("#nuevaU").onkeydown=o=>{o.key==="Enter"&&m("#masU").click()},f("#parcelas [data-k]").forEach(o=>{o.onchange=()=>{let r=t[o.dataset.r??""]?.[Number(o.dataset.j)];if(!r)return;let p=o.dataset.k;r[p]=o.value}}),f("#parcelas [data-split]").forEach(o=>{o.onclick=()=>{let r=o.dataset.split??"";(t[r]=t[r]??[]).push({ruta:"",unidad:"none",nombre:`${r} \xB7 new slice`}),i()}}),f("#parcelas [data-quita]").forEach(o=>{o.onclick=()=>{t[o.dataset.quita??""]?.splice(Number(o.dataset.j),1),i()}}),f("#parcelas [data-lab]").forEach(o=>{o.onchange=()=>{let r=o.dataset.lab??"";o.checked?n.add(r):n.delete(r)}}),m("#guardaBarrios").onclick=async()=>{let o=await b("/api/unidades",{method:"POST",body:JSON.stringify({unidades:e})}),r=await b("/api/parcelas",{method:"POST",body:JSON.stringify({repos:t,lab:[...n]})});await k();let p=o.error??r.error;g(p??"Districts and houses written",!!p)}};i()};T.red=()=>{let e=new Set(d.roads.map(a=>a.id)),n=d.ciudades.filter(a=>!a.actual&&a.id&&!e.has(a.id)),t=JSON.stringify(d.invitation);m("#lienzo").innerHTML=`<div><span class="sub">roads</span>
390
+ </div>`).join("")||'<p class="prosa">No houses yet. Pick your folders in <b>My seat</b> and each becomes one.</p>',f("#unidades input[data-i]").forEach(o=>{o.oninput=()=>{let r=e[Number(o.dataset.i)];if(!r)return;r.name=o.value,r.id=a(o.value)||r.id;let p=o.closest(".tr")?.querySelector(".id");p&&(p.textContent=r.id)}}),f("#unidades .pip").forEach(o=>{o.onclick=()=>{let r=o.closest(".pips"),p=e[Number(r?.dataset.i)];p&&(p.color=o.dataset.c??p.color,i())}}),f("#unidades .x[data-x]").forEach(o=>{o.onclick=()=>{e.splice(Number(o.dataset.x),1),i()}}),m("#masU").onclick=()=>{let o=m("#nuevaU").value.trim();if(!o)return;let r=e.map(u=>u.color),p=c.paleta.find(u=>!r.includes(u.hex))??c.paleta[0];e.push({id:a(o),name:o,color:p?p.hex:"c8b48a"}),i()},m("#nuevaU").onkeydown=o=>{o.key==="Enter"&&m("#masU").click()},f("#parcelas [data-k]").forEach(o=>{o.onchange=()=>{let r=t[o.dataset.r??""]?.[Number(o.dataset.j)];if(!r)return;let p=o.dataset.k;r[p]=o.value}}),f("#parcelas [data-split]").forEach(o=>{o.onclick=()=>{let r=o.dataset.split??"";(t[r]=t[r]??[]).push({ruta:"",unidad:"none",nombre:`${r} \xB7 new slice`}),i()}}),f("#parcelas [data-quita]").forEach(o=>{o.onclick=()=>{t[o.dataset.quita??""]?.splice(Number(o.dataset.j),1),i()}}),f("#parcelas [data-lab]").forEach(o=>{o.onchange=()=>{let r=o.dataset.lab??"";o.checked?n.add(r):n.delete(r)}}),m("#guardaBarrios").onclick=async()=>{let o=await y("/api/unidades",{method:"POST",body:JSON.stringify({unidades:e})}),r=await y("/api/parcelas",{method:"POST",body:JSON.stringify({repos:t,lab:[...n]})});await k();let p=o.error??r.error;g(p??"Districts and houses written",!!p)}};i()};T.red=()=>{let e=new Set(c.roads.map(a=>a.id)),n=c.ciudades.filter(a=>!a.actual&&a.id&&!e.has(a.id)),t=JSON.stringify(c.invitation);m("#lienzo").innerHTML=`<div><span class="sub">roads</span>
387
391
  <h1 style="margin-top:6px">${s("Cities this one may reach")}</h1>
388
392
  <p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
389
393
  this machine or continue over the remote bus; the city sees one explicit
390
394
  connection either way.</p></div>
391
- <div class="gente">${d.roads.map(a=>`<div class="persona"><h3>${l(a.name)}</h3>
395
+ <div class="gente">${c.roads.map(a=>`<div class="persona"><h3>${l(a.name)}</h3>
392
396
  <span class="ag">${l(a.address)}</span>
393
397
  <span class="rp">${a.local?"local road":"remote road"}</span>
394
398
  <button class="bt mini" data-road-close="${l(a.id)}">close</button></div>`).join("")||`<p class="prosa">${s("No roads yet. This city is isolated on purpose.")}</p>`}</div>
@@ -399,7 +403,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
399
403
  <div class="orden"><span class="et2">${s("remote invitation \xB7 public, no token")}</span>
400
404
  <code>${l(t)}</code>
401
405
  <button class="bt mini" data-copia="${l(t)}">copy</button>
402
- </div>`,re(),f("[data-road-open]").forEach(a=>{a.onclick=async()=>{let i=await b("/api/roads",{method:"POST",body:JSON.stringify({action:"connect",target:a.dataset.roadOpen})});if(i.error)return g(i.error,!0);await k(),g("Road open at both local cities")}}),f("[data-road-close]").forEach(a=>{a.onclick=async()=>{let i=await b("/api/roads",{method:"POST",body:JSON.stringify({action:"disconnect",target:a.dataset.roadClose})});if(i.error)return g(i.error,!0);await k(),g("Road closed")}})};T.recepcion=()=>{m("#lienzo").innerHTML=`<div><span class="sub">${s("reception")}</span>
406
+ </div>`,re(),f("[data-road-open]").forEach(a=>{a.onclick=async()=>{let i=await y("/api/roads",{method:"POST",body:JSON.stringify({action:"connect",target:a.dataset.roadOpen})});if(i.error)return g(i.error,!0);await k(),g("Road open at both local cities")}}),f("[data-road-close]").forEach(a=>{a.onclick=async()=>{let i=await y("/api/roads",{method:"POST",body:JSON.stringify({action:"disconnect",target:a.dataset.roadClose})});if(i.error)return g(i.error,!0);await k(),g("Road closed")}})};T.recepcion=()=>{m("#lienzo").innerHTML=`<div><span class="sub">${s("reception")}</span>
403
407
  <h1 style="margin-top:6px">${s("Messages wait for you, not your agents")}</h1>
404
408
  <p class="prosa" style="margin-top:8px">${s("Remote text stops here as inert text. Read it, reject it with a reason, or choose the cities that should receive it. Until then no model can read it.")}</p></div>
405
409
  <div class="recModo" id="recModo">
@@ -408,7 +412,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
408
412
  <button class="bt mini" type="button" data-rec-open-config>${s("Configure Auto")}</button>
409
413
  </div>
410
414
  <div id="recConfig" hidden></div>
411
- <div id="recLista"><p class="prosa">${s("Reading your reception\u2026")}</p></div>`,W()};async function W(){let e=m("#recLista"),n;try{n=await b("/api/reception")}catch{e.innerHTML=`<div class="recError">${s("Could not read your reception")}</div>`;return}if(n.error){e.innerHTML=`<div class="recError">${l(n.error)}</div>`;return}d.reception={pending:n.summary.pending,pendingBytes:n.summary.pendingBytes,routingMode:n.settings.routingMode,reviewPolicy:n.settings.reviewPolicy,routerProfile:n.settings.routerProfile,autoAvailable:n.settings.autoAvailable};let t=new Map(n.settings.autoRules.map(u=>[u.cityId,u.keywords])),a=m("#recModo");a.innerHTML=`<div><span class="et2">${s("routing mode")}</span>
415
+ <div id="recLista"><p class="prosa">${s("Reading your reception\u2026")}</p></div>`,W()};async function W(){let e=m("#recLista"),n;try{n=await y("/api/reception")}catch{e.innerHTML=`<div class="recError">${s("Could not read your reception")}</div>`;return}if(n.error){e.innerHTML=`<div class="recError">${l(n.error)}</div>`;return}c.reception={pending:n.summary.pending,pendingBytes:n.summary.pendingBytes,routingMode:n.settings.routingMode,reviewPolicy:n.settings.reviewPolicy,routerProfile:n.settings.routerProfile,autoAvailable:n.settings.autoAvailable};let t=new Map(n.settings.autoRules.map(u=>[u.cityId,u.keywords])),a=m("#recModo");a.innerHTML=`<div><span class="et2">${s("routing mode")}</span>
412
416
  <b>${n.settings.routingMode==="auto"?s("Automatic routing"):s("Manual review")}</b>
413
417
  <p>${n.settings.routingMode==="auto"?s("Only one clear, low-risk rule match can leave the human queue automatically."):s("Every message needs a person before it reaches a city.")}</p></div>
414
418
  <button class="bt mini" type="button" data-rec-open-config>${s("Configure Auto")}</button>`;let i=m("#recConfig");i.innerHTML=`<form class="recAutoConfig" data-rec-config>
@@ -425,7 +429,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
425
429
  placeholder="${s("e.g. contract, privacy, legal review")}"></label>`).join("")}</div>
426
430
  <p class="recAutoAviso">${s("Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.")}</p>
427
431
  <button class="bt ppal" type="submit">${s("Save routing policy")}</button>
428
- </form>`,m("[data-rec-open-config]",a).onclick=()=>{i.hidden=!i.hidden},ze();let c=n.connections.length?`<section class="recConexiones"><div class="recSeccion"><b>${s("New encrypted message")}</b>
432
+ </form>`,m("[data-rec-open-config]",a).onclick=()=>{i.hidden=!i.hidden},ze();let d=n.connections.length?`<section class="recConexiones"><div class="recSeccion"><b>${s("New encrypted message")}</b>
429
433
  <span>${n.outbox.queued?x(n.outbox.queued,"message queued","messages queued"):s("Messages leave from this computer with end-to-end encryption.")}</span></div>
430
434
  <form class="recCompose" data-rec-send>
431
435
  <label class="recComposePersona" for="rec-send-connection"><span>${s("Write to")}</span>
@@ -466,7 +470,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
466
470
  </form>
467
471
  </details>
468
472
  </div>`}
469
- </article>`).join("")}</div>`:"";e.innerHTML=`${o}${p}${c}`,at()}function at(){m("[data-rec-config]").onsubmit=async e=>{e.preventDefault();let n=e.currentTarget,t=m("button[type=submit]",n),a=m("input[name=routing_mode]:checked",n).value,i=f("[data-rule-city]",n).map(o=>({city_id:o.dataset.ruleCity,keywords:o.value.split(",").map(r=>r.trim()).filter(Boolean)})).filter(o=>o.keywords.length);t.disabled=!0;let c=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"configure",routing_mode:a,rules:i})});if(!c.ok){t.disabled=!1,g(c.error??s("Could not save the routing policy"),!0);return}g(s("Routing policy saved.")),await W()},f("[data-rec-send]").forEach(e=>{e.onsubmit=async n=>{n.preventDefault();let t=m("button[type=submit]",e),a=m("textarea[name=text]",e),i=m("select[name=connection]",e).value;if(!i||!a.value.trim())return;t.disabled=!0;let c=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"send",connection_id:i,text:a.value})});if(!c.ok){t.disabled=!1,g(c.error??s("Could not queue the message"),!0);return}a.value="",g(s("Message queued on this computer.")),await W()}}),f("[data-rec-dismiss]").forEach(e=>{e.onclick=async()=>{e.disabled=!0;let n=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"reject",message_id:e.dataset.recDismiss,reason:"Dismissed response"})});if(!n.ok){e.disabled=!1,g(n.error??s("Could not dismiss the response"),!0);return}await W()}}),f("[data-rec-route]").forEach(e=>{let n=m("button[type=submit]",e),t=()=>{n.disabled=!e.querySelector("input:checked")};e.onchange=t,e.onsubmit=async a=>{a.preventDefault();let i=f("input[name=destination]:checked",e).map(o=>o.value);if(!i.length)return;n.disabled=!0;let c=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"route",message_id:e.dataset.recRoute,destinations:i})});if(!c.ok){n.disabled=!1,g(c.error??s("Could not route the message"),!0);return}g(s("Message routed. Only the selected cities can now read it.")),await W()}}),f("[data-rec-reject]").forEach(e=>{e.onsubmit=async n=>{n.preventDefault();let t=m("button[type=submit]",e),a=m("input[name=reason]",e).value.trim();if(!a)return;t.disabled=!0;let i=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"reject",message_id:e.dataset.recReject,reason:a})});if(!i.ok){t.disabled=!1,g(i.error??s("Could not reject the message"),!0);return}g(s("Message rejected. Your reason is queued for encrypted delivery.")),await W()}})}function st(e){let n=new Date(e);return Number.isNaN(n.getTime())?e:n.toLocaleString(D()==="es"?"es-ES":"en-GB",{dateStyle:"medium",timeStyle:"short"})}function ot(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}T.committee=()=>{m("#lienzo").innerHTML=`<div><span class="sub">committee</span>
473
+ </article>`).join("")}</div>`:"";e.innerHTML=`${o}${p}${d}`,at()}function at(){m("[data-rec-config]").onsubmit=async e=>{e.preventDefault();let n=e.currentTarget,t=m("button[type=submit]",n),a=m("input[name=routing_mode]:checked",n).value,i=f("[data-rule-city]",n).map(o=>({city_id:o.dataset.ruleCity,keywords:o.value.split(",").map(r=>r.trim()).filter(Boolean)})).filter(o=>o.keywords.length);t.disabled=!0;let d=await y("/api/reception",{method:"POST",body:JSON.stringify({action:"configure",routing_mode:a,rules:i})});if(!d.ok){t.disabled=!1,g(d.error??s("Could not save the routing policy"),!0);return}g(s("Routing policy saved.")),await W()},f("[data-rec-send]").forEach(e=>{e.onsubmit=async n=>{n.preventDefault();let t=m("button[type=submit]",e),a=m("textarea[name=text]",e),i=m("select[name=connection]",e).value;if(!i||!a.value.trim())return;t.disabled=!0;let d=await y("/api/reception",{method:"POST",body:JSON.stringify({action:"send",connection_id:i,text:a.value})});if(!d.ok){t.disabled=!1,g(d.error??s("Could not queue the message"),!0);return}a.value="",g(s("Message queued on this computer.")),await W()}}),f("[data-rec-dismiss]").forEach(e=>{e.onclick=async()=>{e.disabled=!0;let n=await y("/api/reception",{method:"POST",body:JSON.stringify({action:"reject",message_id:e.dataset.recDismiss,reason:"Dismissed response"})});if(!n.ok){e.disabled=!1,g(n.error??s("Could not dismiss the response"),!0);return}await W()}}),f("[data-rec-route]").forEach(e=>{let n=m("button[type=submit]",e),t=()=>{n.disabled=!e.querySelector("input:checked")};e.onchange=t,e.onsubmit=async a=>{a.preventDefault();let i=f("input[name=destination]:checked",e).map(o=>o.value);if(!i.length)return;n.disabled=!0;let d=await y("/api/reception",{method:"POST",body:JSON.stringify({action:"route",message_id:e.dataset.recRoute,destinations:i})});if(!d.ok){n.disabled=!1,g(d.error??s("Could not route the message"),!0);return}g(s("Message routed. Only the selected cities can now read it.")),await W()}}),f("[data-rec-reject]").forEach(e=>{e.onsubmit=async n=>{n.preventDefault();let t=m("button[type=submit]",e),a=m("input[name=reason]",e).value.trim();if(!a)return;t.disabled=!0;let i=await y("/api/reception",{method:"POST",body:JSON.stringify({action:"reject",message_id:e.dataset.recReject,reason:a})});if(!i.ok){t.disabled=!1,g(i.error??s("Could not reject the message"),!0);return}g(s("Message rejected. Your reason is queued for encrypted delivery.")),await W()}})}function st(e){let n=new Date(e);return Number.isNaN(n.getTime())?e:n.toLocaleString(D()==="es"?"es-ES":"en-GB",{dateStyle:"medium",timeStyle:"short"})}function ot(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}T.committee=()=>{m("#lienzo").innerHTML=`<div><span class="sub">committee</span>
470
474
  <h1 style="margin-top:6px">${s("Decisions with a visible chain of custody")}</h1>
471
475
  <p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
472
476
  gathers isolated positions, controls the floor, decides and assigns an
@@ -476,7 +480,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
476
480
  <code>${s("agents-city committee schema open")}</code>
477
481
  <button class="bt mini" data-copia="agents-city committee schema open">copy</button>
478
482
  </div>
479
- <div class="gente">${d.deliberations.map(e=>`<div class="persona">
483
+ <div class="gente">${c.deliberations.map(e=>`<div class="persona">
480
484
  <h3>${l(e.question)}</h3>
481
485
  <span class="ag">${l(e.status)} \xB7 revision ${e.revision} \xB7
482
486
  ${e.received}/${e.total} positions</span>
@@ -485,13 +489,13 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
485
489
  ${e.contributors.length?` \xB7 decisive ${l(e.contributors.join(", "))}`:""}
486
490
  ${e.verifier?` \xB7 verifier ${l(e.verifier)} ${l(e.verification)}`:""}</span>
487
491
  <code class="mono">${l(N(e.act))}</code>
488
- </div>`).join("")||`<p class="prosa">${s("No committee acts yet. Open one only when the seat needs specialised evidence.")}</p>`}</div>`,re()};function ge(e,n){return z(e,n,l)}function Ae(e){return e?.startsWith("data:image/svg+xml;base64,")?`<img class="rpgCara" src="${l(e)}" alt="">`:""}function ve(e,n=""){let t=Math.round(Math.max(0,Math.min(1,e))*100);return`<span class="barra${n?" "+n:""}"><span class="llena" style="width:${t}%"></span></span>`}T.gente=()=>{let e=d.tarjetas.find(o=>o.user===d.yo),n=d.agents??[],t=o=>{let r=Me(o.model),p=de[o.effort]??0,u=o.growth,h=Math.min(1,Math.log2(u.floors+1)/8),v=d.skills[o.name]?.skills??[],w=J(o.runtime),y=ge([""].concat(w.modelos),o.model)+`<option value="__otro__">${s("custom\u2026")}</option>`,$=ge([""].concat(Q),w.esfuerzo?o.effort:""),le=ge(G,o.runtime),R=o.mounts??[];return`
492
+ </div>`).join("")||`<p class="prosa">${s("No committee acts yet. Open one only when the seat needs specialised evidence.")}</p>`}</div>`,re()};function ge(e,n){return z(e,n,l)}function je(e){return e?.startsWith("data:image/svg+xml;base64,")?`<img class="rpgCara" src="${l(e)}" alt="">`:""}function ve(e,n=""){let t=Math.round(Math.max(0,Math.min(1,e))*100);return`<span class="barra${n?" "+n:""}"><span class="llena" style="width:${t}%"></span></span>`}T.gente=()=>{let e=c.tarjetas.find(o=>o.user===c.yo),n=c.agents??[],t=o=>{let r=Me(o.model),p=ce[o.effort]??0,u=o.growth,h=Math.min(1,Math.log2(u.floors+1)/8),v=c.skills[o.name]?.skills??[],w=J(o.runtime),b=ge([""].concat(w.modelos),o.model)+`<option value="__otro__">${s("custom\u2026")}</option>`,$=ge([""].concat(Q),w.esfuerzo?o.effort:""),le=ge(G,o.runtime),q=o.mounts??[];return`
489
493
  <div class="fichaRPG">
490
- <div class="rpgCab">${Ae(o.avatar)}
494
+ <div class="rpgCab">${je(o.avatar)}
491
495
  <div><h3>${l(o.name)}</h3>
492
496
  <span class="rpgTags"><span class="kindChip ${l(o.kind)}">${l(o.kind)}</span>
493
497
  <span class="rpgRol">${l(o.role)}</span>
494
- <span class="rpgHogar">${o.legacy?"repo":`workspace \xB7 ${R.length} ${R.length===1?"mount":"mounts"}`}</span></span></div>
498
+ <span class="rpgHogar">${o.legacy?"repo":`workspace \xB7 ${q.length} ${q.length===1?"mount":"mounts"}`}</span></span></div>
495
499
  <span class="rpgBotones">
496
500
  <button class="rpgMini rpgDado" type="button" data-agente="${l(o.slug)}"
497
501
  title="${s("Reroll this agent's face \u2014 deterministic, persisted on the card")}">\u{1F3B2}</button>
@@ -504,7 +508,7 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
504
508
  <div class="rpgFila"><label>${s("engine")}</label>
505
509
  ${ve(r.ancho,r.defecto?"defecto":"")}
506
510
  <select class="rpgSel" data-agente="${l(o.slug)}" data-campo="model">
507
- ${y}</select></div>
511
+ ${b}</select></div>
508
512
  <div class="rpgFila"><label>${s("effort")}</label>
509
513
  ${ve(p/5,p?"":"defecto")}
510
514
  <select class="rpgSel" data-agente="${l(o.slug)}" data-campo="effort"
@@ -516,8 +520,8 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
516
520
  title="${s("Run the engine for real: --version, and the login state on Claude")}">test</button></span>
517
521
  <select class="rpgSel" data-agente="${l(o.slug)}" data-campo="runtime">
518
522
  ${le}</select></div>
519
- <div class="rpgSkills"><label>${s("works on")} \xB7 ${R.length}</label>
520
- ${R.length?R.map(M=>`<code class="mono" title="${l(M.target)}">${l(M.label)}${M.fixed?"":`<button class="rpgQuitar rpgDesmonta" type="button"
523
+ <div class="rpgSkills"><label>${s("works on")} \xB7 ${q.length}</label>
524
+ ${q.length?q.map(M=>`<code class="mono" title="${l(M.target)}">${l(M.label)}${M.fixed?"":`<button class="rpgQuitar rpgDesmonta" type="button"
521
525
  data-agente="${l(o.slug)}" data-mount="${l(M.label)}"
522
526
  title="Stop this agent working on ${l(M.target)}">\xD7</button>`}</code>`).join(" "):'<span class="rpgDato">nothing mounted yet</span>'}
523
527
  ${o.legacy?`<span class="rpgDato">${s("a legacy repo agent works on its own repo")}</span>`:`<button class="rpgMini rpgMonta" type="button" data-agente="${l(o.slug)}"
@@ -531,13 +535,13 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
531
535
  <label class="rpgMini rpgSubir"
532
536
  title="${s("Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them")}">
533
537
  + zip<input type="file" accept=".zip" data-agente="${l(o.slug)}"></label></div>
534
- </div>`};m("#lienzo").innerHTML=`<div><span class="sub">the houses of ${l(d.city_name)}</span>
535
- <h1 style="margin-top:6px">${s("Who lives in {city}",{city:l(d.city_name)})}</h1>
538
+ </div>`};m("#lienzo").innerHTML=`<div><span class="sub">the houses of ${l(c.city_name)}</span>
539
+ <h1 style="margin-top:6px">${s("Who lives in {city}",{city:l(c.city_name)})}</h1>
536
540
  <p class="prosa" style="margin-top:8px">${s(`<b>A house is where an agent lives and works</b>,
537
541
  and many houses are a city \u2014 it is the same thing the map draws, growing with what that
538
542
  agent actually does. The card and the CLI call them <code class="mono">agents</code>;
539
543
  here you see their houses.`)}</p>
540
- <p class="prosa">${s("They belong to <b>this</b> city and only to it: each one\u2019s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.",{donde:`<code class="mono">${l(N(d.datos))}/agents/</code>`})} Every agent is whole here: the kind of work
544
+ <p class="prosa">${s("They belong to <b>this</b> city and only to it: each one\u2019s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.",{donde:`<code class="mono">${l(N(c.datos))}/agents/</code>`})} Every agent is whole here: the kind of work
541
545
  it does, its role, everything it works on \u2014 any number of repositories and document
542
546
  folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
543
547
  <p class="prosa">Every number is real: growth from what the agent actually produced,
@@ -546,13 +550,13 @@ var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t
546
550
  <button class="rpgAlta" type="button" id="altaAgente">${s("+ Build a house")}</button></div>
547
551
  <div class="fichasRPG">
548
552
  <div class="fichaRPG rpgSeat">
549
- <div class="rpgCab">${Ae(d.avatars?.seat)}
553
+ <div class="rpgCab">${je(c.avatars?.seat)}
550
554
  <div><h3>seat</h3>
551
555
  <span class="rpgTags"><span class="kindChip coordinator">chair</span>
552
556
  <span class="rpgRol">${l(e?.role??"not configured")}</span></span></div>
553
557
  </div>
554
- <div class="rpgFila"><span class="rpgDato">${l(d.address)}</span></div>
558
+ <div class="rpgFila"><span class="rpgDato">${l(c.address)}</span></div>
555
559
  <div class="rpgFila"><span class="rpgDato">${l(e?.objetivo?.title??"No goal yet")}</span></div>
556
560
  </div>
557
561
  ${n.map(t).join("")}
558
- </div>`;let a=async(o,r)=>{let p;try{p=await b("/api/agente",{method:"POST",body:JSON.stringify(o)})}catch(u){g(String(u),!0),C();return}if(!p.ok||!p.agent){g(p.error||"Could not save",!0),C();return}d.agents=(d.agents??[]).map(u=>u.slug===p.agent.slug?p.agent:u),d.avatars&&(d.avatars[p.agent.name]=p.agent.avatar),g(r),C()};f(".rpgSel").forEach(o=>{o.onchange=async()=>{let r=o.value;if(r==="__otro__"){let u=(await I({titulo:s("Which engine?"),cuerpo:[s("An alias the CLI resolves when the window opens. Anything it accepts works here.")],campos:[{id:"alias",etiqueta:s("Model alias"),pista:"claude-opus-5",requerido:!0}],aceptar:s("Use it")}))?.alias??null;if(u===null){C();return}r=u.trim().toLowerCase()}await a({agent:o.dataset.agente??"",[o.dataset.campo??"model"]:r},"Saved \u2014 applies next session")}}),f(".rpgDado").forEach(o=>{o.onclick=()=>a({agent:o.dataset.agente??"",avatar:Math.random().toString(36).slice(2,8)},"New look \u2014 same identity, everywhere, forever")}),f(".rpgTest").forEach(o=>{o.onclick=async()=>{o.disabled=!0;try{let r=await b("/api/motor",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??""})});if(r.error){g(r.error,!0);return}let p=[r.version,r.detail].filter(Boolean).join(" \xB7 ");g(r.ok?`${r.binary} works \u2014 ${p}`:`${r.binary}: ${r.detail||"failed"}`,!r.ok)}catch(r){g(String(r),!0)}finally{o.disabled=!1}}}),f(".rpgQuitar").forEach(o=>{o.onclick=async()=>{let r=o.dataset.skill??"";if(!await Z(s("Remove the skill {skill}?",{skill:r}),[s("It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.")],{aceptar:s("Remove it"),peligro:!0}))return;let u=await b("/api/skill",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??"",remove:r})});if(!u.ok){g(u.error||"Could not remove",!0);return}g(`Skill ${r} removed`),k()}});let i=document.getElementById("altaAgente");i&&(i.onclick=async()=>{let o=new _({api:b,esc:l,aviso:g,yo:d.yo}),r=null;await I({titulo:s("Build a house"),cuerpo:[s("One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.")],contenido:'<div id="altaCasa"></div>',aceptar:s("Build it"),ancho:640,enlaza:(p,u)=>{let h=p.querySelector("#altaCasa");h&&o.monta(h);let v=p.querySelector('[data-dlg="si"]');v&&(v.onclick=async()=>{v.disabled=!0,r=await o.guarda(),v.disabled=!1,r&&u({})})}}),r&&(g(s("{name} has a house now",{name:r.nombre})),k())});let c=async(o,r)=>{try{let p=await b("/api/montaje",{method:"POST",body:JSON.stringify(o)});if(!p.ok){g(p.error||"Could not change the mounts",!0);return}g(r),k()}catch(p){g(String(p),!0)}};f(".rpgMonta").forEach(o=>{o.onclick=async()=>{let r=[],p=new B({api:b,esc:l},y=>{let $=r.indexOf(y);$>=0?r.splice($,1):r.push(y)},()=>r);if(await I({titulo:s("What else does it work on?"),cuerpo:[s("A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.")],contenido:'<div id="montaExp"></div>',aceptar:s("Mount it"),ancho:620,enlaza:y=>{let $=y.querySelector("#montaExp");$&&p.monta($)}})===null||!r.length)return;let h=o.dataset.agente??"",w=(await Promise.all(r.map(y=>b("/api/montaje",{method:"POST",body:JSON.stringify({agent:h,add:y})}).then($=>$.ok?"":`${y}: ${$.error??"could not mount"}`).catch($=>`${y}: ${String($)}`)))).filter(Boolean);g(w.length?w.join("; "):s("Mounted"),w.length>0),k()}}),f(".rpgDesmonta").forEach(o=>{o.onclick=async()=>{let r=o.dataset.mount??"";await Z(s("Stop this agent working on {what}?",{what:r}),[s("The link goes. The folder it points at stays exactly where it is.")],{aceptar:s("Unmount it")})&&c({agent:o.dataset.agente??"",remove:r},`${r} unmounted \u2014 the folder itself is untouched`)}}),f(".rpgIns").forEach(o=>{o.onclick=()=>void it(o.dataset.agente??"",o.dataset.file??"")}),f(".rpgSubir input").forEach(o=>{o.onchange=async()=>{let r=o.files?.[0];if(r){o.disabled=!0;try{let p=await nt(r),u=r.name.replace(/\.zip$/i,"").toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,""),h=await b("/api/skill",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??"",name:u,zip:p})});if(!h.ok){g(h.error||"Could not install",!0);return}g(`Skill ${h.skill} installed \u2014 the Claude runtime reads it next session`),k()}catch(p){g(String(p),!0)}finally{o.disabled=!1,o.value=""}}}})};function nt(e){return new Promise((n,t)=>{let a=new FileReader;a.onerror=()=>t(a.error),a.onload=()=>n(String(a.result).split(",",2)[1]??""),a.readAsDataURL(e)})}var V=null;async function it(e,n){let t=await b(`/api/instrucciones?agent=${encodeURIComponent(e)}&file=${encodeURIComponent(n)}`);if(t.error){g(t.error,!0);return}V={agent:e,file:n},m("#edTitulo").textContent=`${e} \xB7 ${n}`,m("#edLector").textContent=`read by ${t.reader??"?"}${t.exists?"":" \xB7 new file"}`,m("#edRuta").textContent=t.home?`${t.home}/${n}`:"",m("#edTexto").value=t.content??"",m("#editorIns").hidden=!1,m("#edTexto").focus()}document.getElementById("edCerrar")?.addEventListener("click",()=>{m("#editorIns").hidden=!0,V=null});document.getElementById("edGuardar")?.addEventListener("click",async()=>{if(!V)return;let e=await b("/api/instrucciones",{method:"POST",body:JSON.stringify({...V,content:m("#edTexto").value})});if(!e.ok){g(e.error||"Could not save",!0);return}g("Saved"),m("#editorIns").hidden=!0,V=null});function re(){f("[data-copia]").forEach(e=>{e.onclick=async()=>{try{await navigator.clipboard.writeText(e.dataset.copia??""),g("Copied")}catch{g("Could not copy \u2014 select it by hand",!0)}}}),f("[data-ir]").forEach(e=>{e.onclick=()=>{S=e.dataset.ir??"mapa",C()}})}function rt(){let e=document.getElementById("app"),n=document.getElementById("liveResize"),t=document.getElementById("livePanel");if(!e||!n||!t)return;let a="hall-live-width",i=o=>{o===null?e.style.removeProperty("--live-ancho"):e.style.setProperty("--live-ancho",`${Math.round(o)}px`)},c=o=>Math.max(340,Math.min(o,Math.max(420,window.innerWidth*.6)));try{let o=Number(localStorage.getItem(a));o>0&&i(c(o))}catch{}n.addEventListener("pointerdown",o=>{o.preventDefault();let r=o.clientX,p=t.getBoundingClientRect().width;e.classList.add("liveResizing"),n.setPointerCapture(o.pointerId);let u=v=>i(c(p+(r-v.clientX))),h=()=>{e.classList.remove("liveResizing"),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",h),n.removeEventListener("pointercancel",h);try{localStorage.setItem(a,String(Math.round(t.getBoundingClientRect().width)))}catch{}};n.addEventListener("pointermove",u),n.addEventListener("pointerup",h),n.addEventListener("pointercancel",h)}),n.addEventListener("dblclick",()=>{i(null);try{localStorage.removeItem(a)}catch{}})}window.addEventListener("message",e=>{if(!Le(e.data))return;let n=document.querySelector("#cityMapFrame");if(!n||e.source!==n.contentWindow||e.origin!==new URL(n.src,location.href).origin)return;let t=e.data.view;t!=="committee"&&t!=="red"||(S=t,C())});function lt(){let e=document.documentElement.getAttribute("data-tema");return e?e==="claro"?"light":"dark":"light"}function ct(){let e=document.getElementById("temaBoton"),n=(()=>{try{return localStorage.getItem("hall-tema")??""}catch{return""}})(),t=!1,a=i=>{let c=document.documentElement;i?c.setAttribute("data-tema",i):c.removeAttribute("data-tema");let o=i?i==="claro":!t;e&&(e.textContent=o?"\u263E Night":"\u2600 Day");let r=document.querySelector("#cityMapFrame");r?.contentWindow?.postMessage({type:"agents-city-map-theme/1",theme:o?"light":"dark"},new URL(r.src,location.href).origin)};a(n),e&&(e.onclick=()=>{let c=(document.documentElement.getAttribute("data-tema")?document.documentElement.getAttribute("data-tema")==="claro":!t)?"oscuro":"claro";try{localStorage.setItem("hall-tema",c)}catch{}a(c)})}function dt(){let e=document.getElementById("idiomaBoton");if(!e)return;let n=()=>{document.documentElement.lang=D(),e.textContent=D()==="es"?"EN":"ES",f("[data-i18n]").forEach(t=>{t.dataset.en||(t.dataset.en=(t.textContent??"").trim()),t.textContent=s(t.dataset.en)})};n(),e.onclick=()=>{Te(D()==="es"?"en":"es"),n(),k()}}window.addEventListener("error",e=>ne("uncaught error",{mensaje:e.message,fichero:e.filename,linea:e.lineno}));window.addEventListener("unhandledrejection",e=>ne("unhandled rejection",String(e.reason)));rt();dt();ct();k();
562
+ </div>`;let a=async(o,r)=>{let p;try{p=await y("/api/agente",{method:"POST",body:JSON.stringify(o)})}catch(u){g(String(u),!0),C();return}if(!p.ok||!p.agent){g(p.error||"Could not save",!0),C();return}c.agents=(c.agents??[]).map(u=>u.slug===p.agent.slug?p.agent:u),c.avatars&&(c.avatars[p.agent.name]=p.agent.avatar),g(r),C()};f(".rpgSel").forEach(o=>{o.onchange=async()=>{let r=o.value;if(r==="__otro__"){let u=(await P({titulo:s("Which engine?"),cuerpo:[s("An alias the CLI resolves when the window opens. Anything it accepts works here.")],campos:[{id:"alias",etiqueta:s("Model alias"),pista:"claude-opus-5",requerido:!0}],aceptar:s("Use it")}))?.alias??null;if(u===null){C();return}r=u.trim().toLowerCase()}await a({agent:o.dataset.agente??"",[o.dataset.campo??"model"]:r},"Saved \u2014 applies next session")}}),f(".rpgDado").forEach(o=>{o.onclick=()=>a({agent:o.dataset.agente??"",avatar:Math.random().toString(36).slice(2,8)},"New look \u2014 same identity, everywhere, forever")}),f(".rpgTest").forEach(o=>{o.onclick=async()=>{o.disabled=!0;try{let r=await y("/api/motor",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??""})});if(r.error){g(r.error,!0);return}let p=[r.version,r.detail].filter(Boolean).join(" \xB7 ");g(r.ok?`${r.binary} works \u2014 ${p}`:`${r.binary}: ${r.detail||"failed"}`,!r.ok)}catch(r){g(String(r),!0)}finally{o.disabled=!1}}}),f(".rpgQuitar").forEach(o=>{o.onclick=async()=>{let r=o.dataset.skill??"";if(!await Z(s("Remove the skill {skill}?",{skill:r}),[s("It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.")],{aceptar:s("Remove it"),peligro:!0}))return;let u=await y("/api/skill",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??"",remove:r})});if(!u.ok){g(u.error||"Could not remove",!0);return}g(`Skill ${r} removed`),k()}});let i=document.getElementById("altaAgente");i&&(i.onclick=async()=>{let o=new _({api:y,esc:l,aviso:g,yo:c.yo}),r=null;await P({titulo:s("Build a house"),cuerpo:[s("One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.")],contenido:'<div id="altaCasa"></div>',aceptar:s("Build it"),ancho:640,enlaza:(p,u)=>{let h=p.querySelector("#altaCasa");h&&o.monta(h);let v=p.querySelector('[data-dlg="si"]');v&&(v.onclick=async()=>{v.disabled=!0,r=await o.guarda(),v.disabled=!1,r&&u({})})}}),r&&(g(s("{name} has a house now",{name:r.nombre})),k())});let d=async(o,r)=>{try{let p=await y("/api/montaje",{method:"POST",body:JSON.stringify(o)});if(!p.ok){g(p.error||"Could not change the mounts",!0);return}g(r),k()}catch(p){g(String(p),!0)}};f(".rpgMonta").forEach(o=>{o.onclick=async()=>{let r=[],p=new B({api:y,esc:l},b=>{let $=r.indexOf(b);$>=0?r.splice($,1):r.push(b)},()=>r);if(await P({titulo:s("What else does it work on?"),cuerpo:[s("A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.")],contenido:'<div id="montaExp"></div>',aceptar:s("Mount it"),ancho:620,enlaza:b=>{let $=b.querySelector("#montaExp");$&&p.monta($)}})===null||!r.length)return;let h=o.dataset.agente??"",w=(await Promise.all(r.map(b=>y("/api/montaje",{method:"POST",body:JSON.stringify({agent:h,add:b})}).then($=>$.ok?"":`${b}: ${$.error??"could not mount"}`).catch($=>`${b}: ${String($)}`)))).filter(Boolean);g(w.length?w.join("; "):s("Mounted"),w.length>0),k()}}),f(".rpgDesmonta").forEach(o=>{o.onclick=async()=>{let r=o.dataset.mount??"";await Z(s("Stop this agent working on {what}?",{what:r}),[s("The link goes. The folder it points at stays exactly where it is.")],{aceptar:s("Unmount it")})&&d({agent:o.dataset.agente??"",remove:r},`${r} unmounted \u2014 the folder itself is untouched`)}}),f(".rpgIns").forEach(o=>{o.onclick=()=>void it(o.dataset.agente??"",o.dataset.file??"")}),f(".rpgSubir input").forEach(o=>{o.onchange=async()=>{let r=o.files?.[0];if(r){o.disabled=!0;try{let p=await nt(r),u=r.name.replace(/\.zip$/i,"").toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,""),h=await y("/api/skill",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??"",name:u,zip:p})});if(!h.ok){g(h.error||"Could not install",!0);return}g(`Skill ${h.skill} installed \u2014 the Claude runtime reads it next session`),k()}catch(p){g(String(p),!0)}finally{o.disabled=!1,o.value=""}}}})};function nt(e){return new Promise((n,t)=>{let a=new FileReader;a.onerror=()=>t(a.error),a.onload=()=>n(String(a.result).split(",",2)[1]??""),a.readAsDataURL(e)})}var V=null;async function it(e,n){let t=await y(`/api/instrucciones?agent=${encodeURIComponent(e)}&file=${encodeURIComponent(n)}`);if(t.error){g(t.error,!0);return}V={agent:e,file:n},m("#edTitulo").textContent=`${e} \xB7 ${n}`,m("#edLector").textContent=`read by ${t.reader??"?"}${t.exists?"":" \xB7 new file"}`,m("#edRuta").textContent=t.home?`${t.home}/${n}`:"",m("#edTexto").value=t.content??"",m("#editorIns").hidden=!1,m("#edTexto").focus()}document.getElementById("edCerrar")?.addEventListener("click",()=>{m("#editorIns").hidden=!0,V=null});document.getElementById("edGuardar")?.addEventListener("click",async()=>{if(!V)return;let e=await y("/api/instrucciones",{method:"POST",body:JSON.stringify({...V,content:m("#edTexto").value})});if(!e.ok){g(e.error||"Could not save",!0);return}g("Saved"),m("#editorIns").hidden=!0,V=null});function re(){f("[data-copia]").forEach(e=>{e.onclick=async()=>{try{await navigator.clipboard.writeText(e.dataset.copia??""),g("Copied")}catch{g("Could not copy \u2014 select it by hand",!0)}}}),f("[data-ir]").forEach(e=>{e.onclick=()=>{S=e.dataset.ir??"mapa",C()}})}function rt(){let e=document.getElementById("app"),n=document.getElementById("liveResize"),t=document.getElementById("livePanel");if(!e||!n||!t)return;let a="hall-live-width",i=o=>{o===null?e.style.removeProperty("--live-ancho"):e.style.setProperty("--live-ancho",`${Math.round(o)}px`)},d=o=>Math.max(340,Math.min(o,Math.max(420,window.innerWidth*.6)));try{let o=Number(localStorage.getItem(a));o>0&&i(d(o))}catch{}n.addEventListener("pointerdown",o=>{o.preventDefault();let r=o.clientX,p=t.getBoundingClientRect().width;e.classList.add("liveResizing"),n.setPointerCapture(o.pointerId);let u=v=>i(d(p+(r-v.clientX))),h=()=>{e.classList.remove("liveResizing"),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",h),n.removeEventListener("pointercancel",h);try{localStorage.setItem(a,String(Math.round(t.getBoundingClientRect().width)))}catch{}};n.addEventListener("pointermove",u),n.addEventListener("pointerup",h),n.addEventListener("pointercancel",h)}),n.addEventListener("dblclick",()=>{i(null);try{localStorage.removeItem(a)}catch{}})}window.addEventListener("message",e=>{if(!Le(e.data))return;let n=document.querySelector("#cityMapFrame");if(!n||e.source!==n.contentWindow||e.origin!==new URL(n.src,location.href).origin)return;let t=e.data.view;t!=="committee"&&t!=="red"||(S=t,C())});function lt(){let e=document.documentElement.getAttribute("data-tema");return e?e==="claro"?"light":"dark":"light"}function dt(){let e=document.getElementById("temaBoton"),n=(()=>{try{return localStorage.getItem("hall-tema")??""}catch{return""}})(),t=!1,a=i=>{let d=document.documentElement;i?d.setAttribute("data-tema",i):d.removeAttribute("data-tema");let o=i?i==="claro":!t;e&&(e.textContent=o?"\u263E Night":"\u2600 Day");let r=document.querySelector("#cityMapFrame");r?.contentWindow?.postMessage({type:"agents-city-map-theme/1",theme:o?"light":"dark"},new URL(r.src,location.href).origin)};a(n),e&&(e.onclick=()=>{let d=(document.documentElement.getAttribute("data-tema")?document.documentElement.getAttribute("data-tema")==="claro":!t)?"oscuro":"claro";try{localStorage.setItem("hall-tema",d)}catch{}a(d)})}function ct(){let e=document.getElementById("idiomaBoton");if(!e)return;let n=()=>{document.documentElement.lang=D(),e.textContent=D()==="es"?"EN":"ES",f("[data-i18n]").forEach(t=>{t.dataset.en||(t.dataset.en=(t.textContent??"").trim()),t.textContent=s(t.dataset.en)})};n(),e.onclick=()=>{Te(D()==="es"?"en":"es"),n(),k()}}window.addEventListener("error",e=>ne("uncaught error",{mensaje:e.message,fichero:e.filename,linea:e.lineno}));window.addEventListener("unhandledrejection",e=>ne("unhandled rejection",String(e.reason)));rt();ct();dt();k();
@@ -248,9 +248,17 @@ export class Bienvenida {
248
248
  this.roster.length ? 'Add another house' : 'Build the first house',
249
249
  )}</button>
250
250
  <button class="bt" data-bv="siguiente">${_(
251
- this.roster.length ? 'That is everyone' : 'Skipjust me for now',
251
+ this.roster.length ? 'That is everyone' : 'Nobody yet I answer alone',
252
252
  )}</button>
253
253
  </div>
254
+ ${
255
+ this.roster.length
256
+ ? ''
257
+ : `<p class="prosa apunte">${_(`A city with no houses does not delegate: the seat
258
+ answers, and it is the only one who can. That is a real choice for a role whose
259
+ work is other people's cities rather than folders — and it is not the usual one.
260
+ Houses can be added later, from here or with <b>agents-city seat --agents</b>.`)}</p>`
261
+ }
254
262
  </div>`;
255
263
  }
256
264
 
@@ -118,7 +118,9 @@ anota({
118
118
  'Build the first house': 'Construye la primera casa',
119
119
  'Add another house': 'Añade otra casa',
120
120
  'That is everyone': 'Ya están todos',
121
- 'Skipjust me for now': 'Sáltalode momento solo yo',
121
+ 'Nobody yet I answer alone': 'Nadie todavía contesto yo solo',
122
+ "A city with no houses does not delegate: the seat answers, and it is the only one who can. That is a real choice for a role whose work is other people's cities rather than folders — and it is not the usual one. Houses can be added later, from here or with <b>agents-city seat --agents</b>.":
123
+ 'Una ciudad sin casas no delega: contesta el asiento, y es el único que puede. Es una elección de verdad para un rol cuyo trabajo son las ciudades de otros y no las carpetas — y no es la habitual. Las casas se pueden añadir luego, desde aquí o con <b>agents-city seat --agents</b>.',
122
124
  'a new house': 'una casa nueva',
123
125
  'Who lives in it?': '¿Quién vive en ella?',
124
126
  'What do you call it?': '¿Cómo la llamas?',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-city",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Run autonomous agent cities: one chair seat, role-aware repo agents, repo-owned skills, and explicit roads.",
5
5
  "bin": {
6
6
  "agents-city": "bin/agents-city.js"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "city",
3
3
  "displayName": "Agents City",
4
- "version": "0.5.6",
4
+ "version": "0.5.7",
5
5
  "description": "One autonomous city seat with a work domain, role, goal, repo support agents, recognised skills and explicit roads to other cities.",
6
6
  "author": {
7
7
  "name": "jlcases",
@@ -476,8 +476,17 @@ python3 "$(dirname "$0")/trust-repos.py" "$EQUIPO" ${RUTAS[@]+"${RUTAS[@]}"}
476
476
 
477
477
  # ── The seat window: this city's identity on its roads ─────────────────────
478
478
  "$RUNTIME" ensure >/dev/null
479
- comodidades
480
479
  if [ "$SESION_YA" -eq 0 ]; then
480
+ # Only on a city being opened. These are a dozen GLOBAL tmux options, and a
481
+ # session already running has whatever its owner has set since — a mouse
482
+ # toggle, a status bar, a style. Re-applying them from underneath a
483
+ # full-screen app is not a fresh start, it is a change of terrain mid-step:
484
+ # flipping mouse reporting while Claude Code is drawing sends the raw SGR
485
+ # sequences (`^[[<0;40;51M`) into the prompt as text.
486
+ #
487
+ # It could not happen before reconciling existed, because an open session
488
+ # exec'd `attach` and never got here.
489
+ comodidades
481
490
  tmux new-session -d -s "$SESSION" -n seat -c "$EQUIPO"
482
491
  elif ! existe_ventana seat; then
483
492
  # The session outlived its own chair — somebody closed that one window. The
@@ -114,8 +114,6 @@ def contexto(datos, plugin=""):
114
114
  """The whole note, or '' when there is nobody to name."""
115
115
  agentes = agentes_de(datos)
116
116
  caminos = carreteras(datos, plugin)
117
- if not agentes and not caminos:
118
- return ""
119
117
  partes = [
120
118
  "Before you answer this, decide who it concerns. You are the chair of "
121
119
  "this city: the answer that leaves here should be the city's, and a city "
@@ -123,11 +121,25 @@ def contexto(datos, plugin=""):
123
121
  ]
124
122
  if agentes:
125
123
  partes.append("In this city:\n" + "\n".join(_linea_de_agente(a) for a in agentes))
124
+ else:
125
+ # Said rather than left silent. A city with no houses cannot delegate, so
126
+ # nothing is refused here and the answer is yours alone — which is a fact
127
+ # about this city worth knowing before you give it, not a state to
128
+ # discover afterwards.
129
+ partes.append(
130
+ "This city has no houses, so there is nobody here to ask and nothing "
131
+ "is being withheld from you: whatever you answer is yours alone. "
132
+ "`agents-city seat --agents` changes that."
133
+ )
126
134
  if caminos:
127
135
  partes.append(
128
136
  "On your roads — other cities, each with its own owner and its own "
129
137
  "seat:\n" + "\n".join(_linea_de_carretera(c) for c in caminos)
130
138
  )
139
+ # With nobody in the city and no road out, the two routes below are a form
140
+ # to fill in with nothing. Say what is true and stop.
141
+ if not agentes and not caminos:
142
+ return "\n\n".join(partes)
131
143
  partes.append(
132
144
  "Ask the ones it concerns, and only those:\n"
133
145
  " · an agent here — `agents-city committee open --question … --member <agent> "