agents-city 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "city",
11
11
  "source": "./plugin",
12
- "version": "0.2.0",
12
+ "version": "0.2.1",
13
13
  "description": "Operate one autonomous city seat, its domain, role, repo support agents, goal, recognised skills and explicit roads to other cities."
14
14
  }
15
15
  ]
package/README.es.md CHANGED
@@ -1058,13 +1058,22 @@ demuestra por sí solo mayor calidad de respuesta ni una afirmación SOTA.
1058
1058
 
1059
1059
  ### `agents-city reset`
1060
1060
 
1061
- Reinicia **una** ciudad gestionada conservando su identidad estable y sus repos.
1061
+ Reinicia ciudades gestionadas conservando su identidad estable y sus repos.
1062
1062
 
1063
1063
  ```bash
1064
- agents-city reset producto --dry-run
1065
- agents-city reset producto
1064
+ agents-city reset product --dry-run # muestra cada efecto, no cambia nada
1065
+ agents-city reset product
1066
+ agents-city reset product cliente-a # varias, separadas por espacios
1067
+ agents-city reset all # todas las de este propietario
1066
1068
  ```
1067
1069
 
1070
+ Un nombre desconocido aborta la ejecución **entera** antes de tocar nada:
1071
+ reiniciar tres ciudades y pararse en una errata es el peor final posible para un
1072
+ comando destructivo. El Hall tiene lo mismo como botón, en **Cities** — primero
1073
+ enseña qué desaparece, qué sobrevive y dónde queda la copia, y te pide escribir
1074
+ el nombre de la ciudad.
1075
+
1076
+
1068
1077
  El plan de reset:
1069
1078
 
1070
1079
  1. valida que el destino sea una ciudad gestionada, no una ruta arbitraria;
package/README.md CHANGED
@@ -1045,13 +1045,21 @@ benchmark; by itself it proves neither higher answer quality nor a SOTA claim.
1045
1045
 
1046
1046
  ### `agents-city reset`
1047
1047
 
1048
- Resets **one** managed city while preserving its stable identity and repositories.
1048
+ Resets managed cities while preserving their stable identity and repositories.
1049
1049
 
1050
1050
  ```bash
1051
- agents-city reset product --dry-run
1051
+ agents-city reset product --dry-run # show every effect, change nothing
1052
1052
  agents-city reset product
1053
+ agents-city reset product client-a # several, space separated
1054
+ agents-city reset all # every city this owner has
1053
1055
  ```
1054
1056
 
1057
+ One unknown name aborts the **whole** run before anything is touched: resetting
1058
+ three cities and then stopping on a typo is the worst outcome a destructive
1059
+ command can have. The Hall has the same thing as a button, in **Cities** — it
1060
+ first shows what disappears, what survives and where the copy lands, and asks
1061
+ you to type the city's name.
1062
+
1055
1063
  The reset plan:
1056
1064
 
1057
1065
  1. verifies the target is a managed city, not an arbitrary path;
package/bin/hall.html CHANGED
@@ -11,10 +11,10 @@
11
11
  <style>
12
12
  /* The map's palette and its three typefaces. The hall manages the city the map
13
13
  draws; they must not look like two products. Single visual world on purpose. */
14
- /* Two skins, one set of names. Night is the city's own light an isometric map
15
- at 3am — and stays the default. Day borrows the palette of agentscity.net, so
16
- the local tool and the public site are visibly the same product: its paper,
17
- its ink, its violet. Nothing below this block names a colour directly. */
14
+ /* Two skins, one set of names. DAY is the defaultthe palette of
15
+ agentscity.net, so the local tool and the public site are visibly one
16
+ product. Night is the map's own light, chosen with the switch and
17
+ remembered. Nothing below this block names a colour directly. */
18
18
  :root{
19
19
  /* Day is the default: the palette of agentscity.net, so the tool on your
20
20
  machine and the public site are one product from the first second. Night
@@ -46,8 +46,7 @@
46
46
  --noche-tono-verificacion:#50ccb1;
47
47
  --noche-sombra:none;
48
48
  }
49
- /* One skin's worth of mapping, used by the explicit choice and by the system
50
- preference alike. An explicit choice always outranks the machine's. */
49
+ /* Night, mapped from the tokens declared once above. */
51
50
  :root[data-tema="oscuro"]{
52
51
  --suelo:var(--noche-suelo); --suelo2:var(--noche-suelo2); --suelo3:var(--noche-suelo3);
53
52
  --linea:var(--noche-linea); --linea2:var(--noche-linea2);
@@ -62,22 +61,10 @@
62
61
  --tono-verificacion:var(--noche-tono-verificacion);
63
62
  --sombra:var(--noche-sombra);
64
63
  }
65
- @media(prefers-color-scheme:dark){
66
- :root:not([data-tema="claro"]){
67
- --suelo:var(--noche-suelo); --suelo2:var(--noche-suelo2); --suelo3:var(--noche-suelo3);
68
- --linea:var(--noche-linea); --linea2:var(--noche-linea2);
69
- --tinta:var(--noche-tinta); --tinta2:var(--noche-tinta2); --tinta3:var(--noche-tinta3);
70
- --lampara:var(--noche-lampara); --carta:var(--noche-carta); --verde:var(--noche-verde);
71
- --panel:var(--noche-panel); --burbuja:var(--noche-burbuja);
72
- --burbuja-mia:var(--noche-burbuja-mia);
73
- --borde-acento:var(--noche-borde-acento); --sobre-lampara:var(--noche-sobre-lampara);
74
- --lampara-viva:var(--noche-lampara-viva);
75
- --chip-luz:var(--noche-chip-luz); --chip-tinta:var(--noche-chip-tinta);
76
- --tono-palabra:var(--noche-tono-palabra); --tono-evidencia:var(--noche-tono-evidencia);
77
- --tono-verificacion:var(--noche-tono-verificacion);
78
- --sombra:var(--noche-sombra);
79
- }
80
- }
64
+ /* No `prefers-color-scheme` rule on purpose: daylight is THE default, not a
65
+ default that a machine set to dark quietly overrides. Night is a choice made
66
+ here, with the switch, and remembered — which is why an owner whose whole
67
+ desktop is dark still opens a paper-coloured hall. */
81
68
  *{box-sizing:border-box}
82
69
  html,body{height:100%}
83
70
  body{margin:0;background:var(--suelo);color:var(--tinta);font-family:var(--texto);
@@ -105,6 +92,13 @@ nav{background:var(--suelo2);border-right:1px solid var(--linea);padding:20px 0
105
92
  .badge::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--lampara)}
106
93
  .marca h2{font-size:19px;margin:9px 0 0}
107
94
  .marca p{margin:4px 0 0;font-size:12px;color:var(--tinta3);line-height:1.45}
95
+ .railCiudad{margin:0 18px;padding:10px 12px;border-radius:7px;background:var(--suelo3);
96
+ border:1px solid var(--linea);display:flex;flex-direction:column;gap:2px}
97
+ .railCiudad .railEtiqueta{font:600 9px var(--hud);letter-spacing:.16em;text-transform:uppercase;
98
+ color:var(--tinta3)}
99
+ .railCiudad b{font:600 15px var(--hud);color:var(--tinta)}
100
+ .railCiudad .railDe{font:10.5px var(--mono);color:var(--tinta3)}
101
+ .railOtras{margin-top:6px;padding-top:6px;border-top:1px solid var(--linea)}
108
102
  nav ol{list-style:none;margin:0;padding:0 10px;display:flex;flex-direction:column;gap:2px}
109
103
  nav li{display:flex;gap:10px;align-items:baseline;padding:9px 10px;border-radius:3px;
110
104
  font-family:var(--hud);font-size:13.5px;color:var(--tinta2);cursor:pointer;white-space:nowrap}
@@ -114,6 +108,12 @@ nav li .n{font-family:var(--mono);font-size:10px;color:var(--tinta3);margin-left
114
108
  nav .pie{margin-top:auto;padding:14px 18px 0;border-top:1px solid var(--linea);
115
109
  display:flex;flex-direction:column;gap:8px}
116
110
  nav .pie a{font-family:var(--hud);font-size:12px;color:var(--tinta3);text-decoration:none}
111
+ .peligro{border:1px solid var(--carta);border-radius:8px;padding:16px 18px;
112
+ background:color-mix(in srgb,var(--carta) 7%,var(--suelo2));display:flex;
113
+ flex-direction:column;gap:10px;align-items:flex-start}
114
+ .peligro h3{font:600 13px var(--hud);color:var(--carta)}
115
+ .bt.malo{border-color:var(--carta);color:var(--carta)}
116
+ .bt.malo:hover{background:var(--carta);color:var(--suelo2)}
117
117
  .avisoVersion{margin:0;font:11px/1.45 var(--mono);color:var(--lampara);
118
118
  border:1px solid var(--borde-acento);background:var(--burbuja-mia);
119
119
  padding:7px 9px;border-radius:4px}
@@ -495,7 +495,9 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
495
495
  <button type="button" id="temaBoton" class="temaBoton"
496
496
  title="Daylight is the default; night is the map's own light">☾ Night</button>
497
497
  </div>
498
+ <div id="railCiudad" class="railCiudad"></div>
498
499
  <ol id="rail"></ol>
500
+ <ol id="railOtras" class="railOtras"></ol>
499
501
  <div id="ciudades" style="padding:0 18px"></div>
500
502
  <div class="pie">
501
503
  <p id="avisoVersion" class="avisoVersion" hidden></p>
package/bin/navegador.mjs CHANGED
@@ -177,8 +177,8 @@ async function main() {
177
177
 
178
178
  // The agents view: this is where dead controls shipped once.
179
179
  const abierto = await cdp.evalua(`(async () => {
180
- const ir = [...document.querySelectorAll('nav li')].find(l => /agents/i.test(l.textContent));
181
- if (!ir) return { falta: 'no agents entry' };
180
+ const ir = [...document.querySelectorAll('nav li')].find(l => /houses/i.test(l.textContent));
181
+ if (!ir) return { falta: 'no houses entry' };
182
182
  ir.click();
183
183
  for (let i = 0; i < 40; i++) {
184
184
  if (document.querySelector('.fichaRPG')) break;
@@ -195,9 +195,9 @@ async function main() {
195
195
  test: boton('.rpgTest'),
196
196
  };
197
197
  })()`);
198
- comprueba('the agents view renders its sheets', (abierto?.fichas ?? 0) > 0, JSON.stringify(abierto));
198
+ comprueba('the houses view renders its sheets', (abierto?.fichas ?? 0) > 0, JSON.stringify(abierto));
199
199
  for (const [control, etiqueta] of [
200
- ['alta', '+ Add an agent'],
200
+ ['alta', 'the build-a-house button'],
201
201
  ['instrucciones', 'the CLAUDE.md / AGENTS.md editors'],
202
202
  ['subir', 'the skill zip upload'],
203
203
  ['monta', 'the + folder mount button'],
@@ -280,6 +280,34 @@ async function main() {
280
280
  filas: document.querySelectorAll('.lista .fila').length,
281
281
  };
282
282
  })()`);
283
+ const reinicio = await cdp.evalua(`(async () => {
284
+ const b = document.getElementById('reiniciaCiudad');
285
+ if (!b) return { falta: true };
286
+ // Nothing is clicked here: this asks the server what a reset WOULD do,
287
+ // which is the same call the button makes before it dares ask anything.
288
+ const r = await fetch('/api/ciudad-reinicia?PASE=' + window.PASE, {
289
+ method: 'POST',
290
+ headers: { 'X-City-Pase': window.PASE, 'Content-Type': 'application/json' },
291
+ body: '{}',
292
+ }).then((x) => x.json());
293
+ return { cableado: !!b.onclick, ok: r.ok, previa: r.preview, error: r.error };
294
+ })()`);
295
+ comprueba(
296
+ 'the danger button exists and is wired',
297
+ reinicio?.cableado === true,
298
+ JSON.stringify(reinicio),
299
+ );
300
+ comprueba(
301
+ // Two acceptable answers, one forbidden: a preview of what would happen,
302
+ // or a refusal (this fixture's city lives outside the owner's folder and
303
+ // reset declines to touch such a thing). What must NEVER come back from a
304
+ // call with no typed name is ok:true — that would be a reset on one click.
305
+ 'without the typed name it previews or refuses, but never resets',
306
+ reinicio?.ok !== true &&
307
+ (Array.isArray(reinicio?.previa?.loses) || typeof reinicio?.error === 'string'),
308
+ JSON.stringify(reinicio),
309
+ );
310
+
283
311
  comprueba(
284
312
  'the Cities section can start a new city',
285
313
  ciudades?.crear === true && ciudades?.campo === true,
@@ -291,7 +319,7 @@ async function main() {
291
319
  } finally {
292
320
  proceso.kill('SIGKILL');
293
321
  }
294
- console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 14 checks\n`);
322
+ console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 16 checks\n`);
295
323
  process.exit(fallos ? 1 : 0);
296
324
  }
297
325
 
package/bin/serve.py CHANGED
@@ -48,6 +48,7 @@ import avatar # noqa: E402
48
48
  import workspace # noqa: E402
49
49
  import crecimiento # noqa: E402
50
50
  import runtime_processes # noqa: E402
51
+ import reset as reinicio # noqa: E402
51
52
  import actualiza # noqa: E402
52
53
  import importlib.machinery as _mach
53
54
  import importlib.util as _iu # noqa: E402
@@ -175,6 +176,16 @@ def skills_de_ciudad(datos):
175
176
  return valor
176
177
 
177
178
 
179
+ def estado_seguro_agentes(datos):
180
+ """How many agents this city has, or none when the card cannot be read."""
181
+ owner = cities.lee_clave(datos, "owner") or seat.quien_soy()
182
+ try:
183
+ texto = card.lee(os.path.join(datos, f"{owner}.md")).get("texto") or ""
184
+ return workspace.agentes(texto, datos)
185
+ except (OSError, ValueError):
186
+ return []
187
+
188
+
178
189
  def olvida_skills():
179
190
  """Forget the discovery memo: a skill was installed or removed, and that
180
191
  changes the answer without touching the card."""
@@ -426,10 +437,20 @@ def lee_clave(fichero, clave, defecto=""):
426
437
 
427
438
 
428
439
  def lista_con(datos):
440
+ """Every city this owner has, with the one being looked at marked — and how
441
+ many agents live in each.
442
+
443
+ The count is not decoration: an agent belongs to ONE city because its
444
+ workspace and its mounts live inside that city's folder, and a menu that
445
+ lists "Cities" and "Agents" as two unrelated things hides exactly that. One
446
+ card read per city, which is cheaper than the page that draws it.
447
+ """
429
448
  real = os.path.realpath(datos)
430
449
  fuera = [dict(c, actual=(c["ruta"] == real)) for c in cities.lista(seat.quien_soy())]
431
450
  if not any(c["actual"] for c in fuera):
432
451
  fuera.insert(0, {"ruta": real, "nombre": cities.nombre(real), "actual": True})
452
+ for c in fuera:
453
+ c["agentes"] = len(estado_seguro_agentes(c["ruta"]))
433
454
  return fuera
434
455
 
435
456
 
@@ -680,6 +701,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
680
701
  "/api/sesion": "p_sesion",
681
702
  "/api/ciudades": "p_ciudades",
682
703
  "/api/ciudad-archiva": "p_archiva_ciudad",
704
+ "/api/ciudad-reinicia": "p_reinicia_ciudad",
683
705
  "/api/roads": "p_roads",
684
706
  "/api/agente": "p_agente",
685
707
  "/api/agentes": "p_agentes",
@@ -1070,6 +1092,50 @@ class Manejador(http.server.BaseHTTPRequestHandler):
1070
1092
  {"ok": True, "backup": copia, "ciudades": lista_con(cities.actual(usuario))}
1071
1093
  )
1072
1094
 
1095
+ def p_reinicia_ciudad(self, q, cuerpo):
1096
+ """Take a city back to its first day — after showing exactly what that
1097
+ means, and only when the person types its name.
1098
+
1099
+ Two calls, on purpose. Without `confirm` it answers with the effects and
1100
+ changes nothing; with `confirm` matching the city's own name it does the
1101
+ work. A dangerous button that fires on one click will eventually be
1102
+ clicked by an elbow, and the answer "are you sure?" is not information —
1103
+ the list below is.
1104
+ """
1105
+ datos = self.ciudad(q)
1106
+ usuario = cities.lee_clave(datos, "owner") or seat.quien_soy()
1107
+ nombre = cities.nombre(datos)
1108
+ try:
1109
+ copia = reinicio.reinicia(datos, usuario, dry_run=True)
1110
+ except (OSError, ValueError) as e:
1111
+ return self.responde({"error": str(e)}, codigo=409)
1112
+ agentes = len(estado_seguro_agentes(datos))
1113
+ efectos = {
1114
+ "city": nombre,
1115
+ "backup": copia,
1116
+ "agents": agentes,
1117
+ "roads": len(roads.lee(datos)),
1118
+ "deliberations": len(deliberations.lista(datos)),
1119
+ "keeps": [
1120
+ "every repository and folder your agents mount — untouched",
1121
+ f"a full copy of this city at {copia}",
1122
+ ],
1123
+ "loses": [
1124
+ "your seat card: role, goal and the whole roster",
1125
+ "each agent's workspace, its mounts and the skills installed in it",
1126
+ "the committee history and every recorded decision",
1127
+ "the map's districts and houses",
1128
+ ],
1129
+ }
1130
+ if str(cuerpo.get("confirm") or "") != nombre:
1131
+ return self.responde({"ok": False, "preview": efectos})
1132
+ try:
1133
+ copia = reinicio.reinicia(datos, usuario)
1134
+ except (OSError, ValueError) as e:
1135
+ return self.responde({"error": str(e)}, codigo=409)
1136
+ olvida_skills()
1137
+ return self.responde({"ok": True, "backup": copia, "preview": efectos})
1138
+
1073
1139
  def p_roads(self, q, cuerpo):
1074
1140
  origen = self.ciudad(q)
1075
1141
  accion = str(cuerpo.get("action") or "")
@@ -438,6 +438,50 @@ def skills_transparentes():
438
438
  comprueba("· there is no stale capability cache", nombres, ["safe-deploy", "audit"])
439
439
 
440
440
 
441
+ def reset_de_varias():
442
+ """`reset` takes several cities, or `all` — and stops before touching any
443
+ of them when one name is wrong.
444
+
445
+ Resetting three of four cities and then failing on a typo is the worst
446
+ possible outcome of a destructive command, so resolution happens first and
447
+ completely.
448
+ """
449
+ print(" resetting more than one city")
450
+ base = tempfile.mkdtemp()
451
+ previo = {k: os.environ.get(k) for k in ("AGENTS_CITY_HOME", "AGENTS_CITY_USER")}
452
+ os.environ.update(AGENTS_CITY_HOME=base, AGENTS_CITY_USER="ana")
453
+ registro = cities.REGISTRO
454
+ cities.REGISTRO = os.path.join(base, "reg")
455
+ try:
456
+ for n in ("home", "producto", "cliente"):
457
+ cities.crea("ana", n)
458
+ entorno = dict(os.environ, AGENTS_CITY_HOME=base, AGENTS_CITY_USER="ana")
459
+ guion = os.path.join(RAIZ, "plugin", "scripts", "reset.py")
460
+
461
+ r = subprocess.run(["python3", guion, "all", "--dry-run"],
462
+ capture_output=True, text=True, env=entorno)
463
+ comprueba("· `all` covers every city this owner has",
464
+ sum(1 for ln in r.stdout.splitlines() if "Would reset" in ln), 3)
465
+ r = subprocess.run(["python3", guion, "home", "cliente", "--dry-run"],
466
+ capture_output=True, text=True, env=entorno)
467
+ afirma("· several names, space separated, cover exactly those",
468
+ "`home`" in r.stdout and "`cliente`" in r.stdout and "`producto`" not in r.stdout,
469
+ r.stdout)
470
+ r = subprocess.run(["python3", guion, "home", "inventada", "--dry-run"],
471
+ capture_output=True, text=True, env=entorno)
472
+ afirma("· one unknown name aborts the whole run, before any city is touched",
473
+ r.returncode == 1 and "Nothing was reset" in r.stderr, r.stderr)
474
+ comprueba("· so nothing was reset", len(cities.lista("ana")), 3)
475
+ finally:
476
+ cities.REGISTRO = registro
477
+ for k, v in previo.items():
478
+ if v is None:
479
+ os.environ.pop(k, None)
480
+ else:
481
+ os.environ[k] = v
482
+ shutil.rmtree(base, ignore_errors=True)
483
+
484
+
441
485
  def main():
442
486
  print()
443
487
  identidad_y_catalogo()
@@ -446,6 +490,7 @@ def main():
446
490
  carreteras()
447
491
  reinicio_aislado()
448
492
  skills_transparentes()
493
+ reset_de_varias()
449
494
  return resumen("cities")
450
495
 
451
496
 
package/bin/test-seat.py CHANGED
@@ -518,7 +518,7 @@ def maquinas_hostiles():
518
518
  "· an old cached Claude plugin is updated through Claude's own CLI",
519
519
  llamadas
520
520
  and llamadas[0][:4] == ["claude", "plugin", "update", "city@agents-city"]
521
- and "0.2.0" in salida.getvalue(),
521
+ and "0.2.1" in salida.getvalue(),
522
522
  f"calls={llamadas!r} output={salida.getvalue()!r}",
523
523
  )
524
524
 
package/bin/test-serve.py CHANGED
@@ -189,10 +189,15 @@ def main():
189
189
  cabecera[-500:],
190
190
  )
191
191
  afirma(
192
- an explicit choice outranks the machine's preference",
192
+ # Daylight is THE default, not a default a dark desktop overrides:
193
+ # the owner asked for light and a machine setting does not outvote
194
+ # them. Night is reachable only through the switch, and remembered.
195
+ "· night is a choice made here, never one the machine makes",
193
196
  '[data-tema="oscuro"]' in cabecera
194
- and "prefers-color-scheme:dark" in cabecera
195
- and ':root:not([data-tema="claro"])' in cabecera,
197
+ # The RULE, not the word: the palette block explains in prose
198
+ # that there deliberately is no media query, and a test that
199
+ # cannot tell a comment from a rule fails on its own docs.
200
+ and "@media(prefers-color-scheme" not in cabecera,
196
201
  cabecera[-700:],
197
202
  )
198
203
  import re as _re
@@ -12,7 +12,7 @@ var CLASES = [
12
12
  ],
13
13
  ["coordinator", "It coordinates", "Its house grows with the decisions it records."]
14
14
  ];
15
- var PASOS = ["Welcome", "The work", "Your chair", "The agents", "Ready"];
15
+ var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
16
16
  function casita(color) {
17
17
  return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
18
18
  <polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
@@ -80,11 +80,13 @@ var Bienvenida = class {
80
80
  <div class="bvArte">${casita("#7048e8")}${casita("#45b9a7")}${casita("#f2674a")}</div>
81
81
  <div>
82
82
  <span class="sub">welcome</span>
83
- <h1>A city is you, and the agents who work with you</h1>
84
- <p class="prosa">You take the chair. Each <b>agent</b> is one worker with its own
85
- window, its own role and its own corner of your disk \u2014 a repository, three of them,
86
- a folder of documents, whatever it actually works on. They never talk to each other
87
- behind your back: you chair, they answer.</p>
83
+ <h1>A city is you, and the houses around you</h1>
84
+ <p class="prosa">You take the chair. Around it, one <b>house</b> per worker \u2014 an
85
+ <b>agent</b> \u2014 each with its own window, its own role and its own corner of your
86
+ disk: a repository, three of them, a folder of documents, whatever it actually
87
+ works on. The map draws them as houses that grow with the work done in them, and
88
+ many houses are a city. They never talk to each other behind your back: you chair,
89
+ they answer.</p>
88
90
  <p class="prosa">Everything lives as plain files in
89
91
  <code class="mono">${this.p.esc(this.p.datos)}</code>. Nothing leaves this machine,
90
92
  there is no account, and you can edit any of it by hand afterwards.</p>
@@ -179,14 +181,14 @@ var Bienvenida = class {
179
181
  return `
180
182
  <div class="bvPaso">
181
183
  <span class="sub">question 3 of 4</span>
182
- <h1>${this.roster.length ? "Who else works here?" : "Who works with you?"}</h1>
183
- <p class="prosa">An <b>agent</b> is one worker: it gets its own window, its own role
184
- and its own corner of your disk. It is <b>not</b> a repository \u2014 one agent can answer
185
- for three services and a folder of documents at once, and an agent whose work is
186
- documents needs no git anywhere.</p>
184
+ <h1>${this.roster.length ? "Who else lives here?" : "Who lives in your city?"}</h1>
185
+ <p class="prosa">Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window,
186
+ its own role and its own corner of your disk. A house is <b>not</b> a repository: one
187
+ can answer for three services and a folder of documents at once, and a house whose
188
+ work is documents needs no git anywhere.</p>
187
189
  ${this.roster.length ? `<div class="bvFichas">${fichas}</div>` : ""}
188
190
  <div class="bvBotones">
189
- <button class="bt ppal" data-bv="nuevo">${this.roster.length ? "Add another agent" : "Add the first agent"}</button>
191
+ <button class="bt ppal" data-bv="nuevo">${this.roster.length ? "Add another house" : "Build the first house"}</button>
190
192
  <button class="bt" data-bv="siguiente">${this.roster.length ? "That is everyone" : "Skip \u2014 just me for now"}</button>
191
193
  </div>
192
194
  </div>`;
@@ -208,8 +210,8 @@ var Bienvenida = class {
208
210
  ).join("");
209
211
  return `
210
212
  <div class="bvPaso">
211
- <span class="sub">a new agent</span>
212
- <h1>Tell me about this one</h1>
213
+ <span class="sub">a new house</span>
214
+ <h1>Who lives in it?</h1>
213
215
  <div class="campo"><label>What do you call it?</label>
214
216
  <input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
215
217
  placeholder="urgencias, api, the handbook \u2014 whatever you would say out loud"></div>
@@ -226,7 +228,7 @@ var Bienvenida = class {
226
228
  ${a.montajes.length ? `<p class="pista">Working on: <b>${this.p.esc(a.montajes.join(", "))}</b></p>` : ""}
227
229
  <div class="bvBotones">
228
230
  <button class="bt" data-bv="cancela">Cancel</button>
229
- <button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${this.guardando ? "adding\u2026" : "Add this agent"}</button>
231
+ <button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${this.guardando ? "building\u2026" : "Build it"}</button>
230
232
  </div>
231
233
  </div>`;
232
234
  }
@@ -236,11 +238,11 @@ var Bienvenida = class {
236
238
  <div class="bvPaso">
237
239
  <span class="sub">ready</span>
238
240
  <h1>${this.p.esc(this.p.ciudad)} is alive</h1>
239
- <p class="prosa">Your seat is written and ${this.roster.length ? `<b>${this.roster.length}</b> agent${this.roster.length === 1 ? "" : "s"} live here` : "the city is waiting for its first agent"}. Everything you just answered is a plain file you can read and edit.</p>
241
+ <p class="prosa">Your seat is written and ${this.roster.length ? `<b>${this.roster.length}</b> house${this.roster.length === 1 ? "" : "s"} stand${this.roster.length === 1 ? "s" : ""} around it` : "the city is waiting for its first house"}. Everything you just answered is a plain file you can read and edit.</p>
240
242
  <p class="prosa">What people usually do next:</p>
241
243
  <div class="bvSiguientes">
242
244
  <button class="bt ppal" data-bv="sesion">Open my session</button>
243
- <button class="bt" data-bv="ir-gente">See the agents</button>
245
+ <button class="bt" data-bv="ir-gente">See the houses</button>
244
246
  <button class="bt" data-bv="ir-mapa">Draw the map</button>
245
247
  <button class="bt" data-bv="ir-puesto">Set a goal</button>
246
248
  </div>
@@ -454,10 +456,10 @@ var SECCIONES = [
454
456
  ["resumen", "Overview"],
455
457
  ["mapa", "The map"],
456
458
  ["puesto", "My seat"],
457
- ["barrios", "Districts & houses"],
459
+ ["barrios", "Districts"],
458
460
  ["red", "Roads"],
459
461
  ["committee", "Committee"],
460
- ["gente", "Agents & skills"],
462
+ ["gente", "Houses"],
461
463
  ["ciudades", "Cities"]
462
464
  ];
463
465
  var primeraCarga = true;
@@ -467,6 +469,7 @@ async function refresca() {
467
469
  primeraCarga = false;
468
470
  const mia = E.tarjetas.find((t) => t.user === E.yo);
469
471
  if (!mia || (E.agents ?? []).length === 0) SECCION = "bienvenida";
472
+ else if (!E.mapa) SECCION = "resumen";
470
473
  }
471
474
  pinta();
472
475
  pintaAvisoDeVersion();
@@ -782,12 +785,18 @@ function icono(kind) {
782
785
  return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>';
783
786
  }
784
787
  function rail() {
785
- q("#rail").innerHTML = SECCIONES.map(([id, et]) => {
788
+ const cuantos = (E.agents ?? []).length;
789
+ q("#railCiudad").innerHTML = `<span class="railEtiqueta">you are in</span>
790
+ <b>${esc(E.city_name)}</b>
791
+ <span class="railDe">${esc(E.domain)} \xB7 ${cuantos} house${cuantos === 1 ? "" : "s"}</span>`;
792
+ q("#rail").innerHTML = SECCIONES.filter(([id]) => id !== "ciudades").map(([id, et]) => {
786
793
  const n = id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
787
794
  return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
788
795
  <span>${et}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
789
796
  }).join("");
790
- todos("#rail li").forEach((li) => {
797
+ q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades">
798
+ <span>All cities</span><span class="n">${E.ciudades.length}</span></li>`;
799
+ todos("#rail li,#railOtras li").forEach((li) => {
791
800
  li.onclick = () => {
792
801
  document.body.classList.remove("enGuia");
793
802
  SECCION = li.dataset.s ?? "mapa";
@@ -854,6 +863,7 @@ VISTAS.ciudades = () => {
854
863
  (c) => `
855
864
  <div class="fila ${c.actual ? "on" : ""}">
856
865
  <span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">\xB7 open now</b>' : ""}</span>
866
+ <span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? "" : "s"} of its own</span>
857
867
  <span class="de mono">${esc(corto(c.ruta))}</span>
858
868
  <span style="margin-left:auto;display:flex;gap:6px">
859
869
  ${c.actual ? "" : `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
@@ -865,14 +875,24 @@ VISTAS.ciudades = () => {
865
875
  q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
866
876
  <h1 style="margin-top:6px">Your cities</h1>
867
877
  <p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
868
- its own identity, domain, chair, agents and roads. They share nothing unless you build a
869
- road between them. The one you have open is marked.</p></div>
878
+ its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
879
+ shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that
880
+ city's folder \u2014 so two cities can each have a <code class="mono">docs</code> house and
881
+ they are two different workers. They share nothing unless you build a road between
882
+ them.</p></div>
870
883
  <div class="lista"><div class="filas">${filas}</div></div>
871
884
  <div class="campos" style="max-width:420px">
872
885
  <div class="campo"><label>Start another city</label>
873
886
  <input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
874
887
  <button class="bt ppal" id="creaCiudad">Create it</button>
875
888
  </div>
889
+ <div class="peligro">
890
+ <h3>Start this city over</h3>
891
+ <p class="prosa">Takes <b>${esc(E.city_name)}</b> back to its first day: no seat, no
892
+ agents, no committee history, no map. Your repositories and document folders are
893
+ <b>never touched</b> \u2014 only the city that points at them.</p>
894
+ <button class="bt malo" id="reiniciaCiudad">Start over\u2026</button>
895
+ </div>
876
896
  <p class="pista">Archiving <b>moves</b> a city into
877
897
  <code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
878
898
  <code class="mono">mv</code>. The city you are standing in, and the last one you own,
@@ -896,6 +916,47 @@ VISTAS.ciudades = () => {
896
916
  toast(`${nombre} created`);
897
917
  await refresca();
898
918
  };
919
+ const reiniciar = document.getElementById("reiniciaCiudad");
920
+ if (reiniciar)
921
+ reiniciar.onclick = async () => {
922
+ const previa = await api("/api/ciudad-reinicia", {
923
+ method: "POST",
924
+ body: "{}"
925
+ });
926
+ if (!previa.preview) {
927
+ toast(previa.error ?? "Could not read what a reset would do", true);
928
+ return;
929
+ }
930
+ const p = previa.preview;
931
+ const texto = `Start over ${p.city}?
932
+
933
+ You lose:
934
+ ` + p.loses.map((l) => ` \xB7 ${l}`).join("\n") + `
935
+
936
+ Right now that is ${p.agents} agent(s), ${p.roads} road(s) and ${p.deliberations} committee act(s).
937
+
938
+ You keep:
939
+ ` + p.keeps.map((l) => ` \xB7 ${l}`).join("\n") + `
940
+
941
+ Type the city's name to confirm:`;
942
+ const escrito = window.prompt(texto, "");
943
+ if (escrito === null) return;
944
+ if (escrito.trim() !== p.city) {
945
+ toast("That is not the name \u2014 nothing was changed", true);
946
+ return;
947
+ }
948
+ const r = await api(
949
+ "/api/ciudad-reinicia",
950
+ { method: "POST", body: JSON.stringify({ confirm: p.city }) }
951
+ );
952
+ if (!r.ok) {
953
+ toast(r.error ?? "Could not reset it", true);
954
+ return;
955
+ }
956
+ toast(`${p.city} starts over \u2014 the old one is at ${r.backup ?? "its backup"}`);
957
+ primeraCarga = true;
958
+ await refresca();
959
+ };
899
960
  todos(".archiva").forEach((boton) => {
900
961
  boton.onclick = async () => {
901
962
  const nombre = boton.dataset.nombre ?? "";
@@ -926,14 +987,14 @@ VISTAS.resumen = () => {
926
987
  },
927
988
  {
928
989
  ok: agentes.length > 0,
929
- txt: agentes.length === 0 ? "Add the <b>agents</b> that work in this city \u2014 each one whole: its kind, its role, and everything it works on" : `<b>${agentes.length}</b> agent${agentes.length === 1 ? "" : "s"} in this city: ${esc(
990
+ txt: agentes.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>${agentes.length}</b> house${agentes.length === 1 ? "" : "s"} in this city: ${esc(
930
991
  agentes.map((a) => a.name).join(", ")
931
992
  )}`,
932
993
  ir: "gente"
933
994
  },
934
995
  {
935
996
  ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
936
- txt: agentes.some((a) => (a.mounts ?? []).length) ? "Your agents work on real repositories and folders" : "Give an agent something to work on \u2014 a repository, a worktree, a folder of documents",
997
+ txt: agentes.some((a) => (a.mounts ?? []).length) ? "Every house works on something real" : "Give a house something to work on \u2014 a repository, a worktree, a folder of documents",
937
998
  ir: "gente"
938
999
  },
939
1000
  {
@@ -1044,12 +1105,17 @@ VISTAS.mapa = () => {
1044
1105
  };
1045
1106
  return;
1046
1107
  }
1108
+ const cuantos = (E.agents ?? []).length;
1047
1109
  q("#lienzo").innerHTML = `<div><span class="sub">the map</span>
1048
- <h1 style="margin-top:6px">Not drawn yet</h1>
1049
- <p class="prosa" style="margin-top:8px">The map can be served independently.
1050
- Locally the hall runs it for you: first time bakes the front end
1051
- and seeds the local database, which takes about a minute.</p></div>
1052
- <div><button class="bt ppal" id="arrancaMapa">Draw my city</button></div>
1110
+ <h1 style="margin-top:6px">Your city, drawn</h1>
1111
+ <p class="prosa" style="margin-top:8px">${cuantos ? `<b>${cuantos}</b> agent${cuantos === 1 ? "" : "s"} live here, and the map shows
1112
+ them as houses that grow with the work they do \u2014 with the town hall in the middle
1113
+ when a committee is sitting, and a gate for every road out.` : `The map draws one house per agent. There are none yet, so it would be an empty
1114
+ plot \u2014 build one from <b>Houses</b> first.`}</p>
1115
+ <p class="prosa">The map is its own little server, so it is not running until you say so.
1116
+ The first time takes about a minute: it builds the front end and seeds a local database.
1117
+ After that it opens instantly, and it is yours \u2014 nothing is uploaded anywhere.</p></div>
1118
+ <div><button class="bt ppal" id="arrancaMapa">${cuantos ? "Draw my city" : "Draw it anyway"}</button></div>
1053
1119
  <p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`;
1054
1120
  q("#arrancaMapa").onclick = async () => {
1055
1121
  q("#arrancaMapa").disabled = true;
@@ -1515,16 +1581,22 @@ VISTAS.gente = () => {
1515
1581
  + zip<input type="file" accept=".zip" data-agente="${esc(a.slug)}"></label></div>
1516
1582
  </div>`;
1517
1583
  };
1518
- q("#lienzo").innerHTML = `<div><span class="sub">agents &amp; skills</span>
1519
- <h1 style="margin-top:6px">One seat, the agents behind it</h1>
1520
- <p class="prosa" style="margin-top:8px">A city is its agents, and each one is
1521
- whole here: the kind of work it does, its role, everything it works on \u2014 any
1522
- number of repositories and document folders at once \u2014 the engine and effort
1523
- that run it, and the skills in its own home. Every number is real: growth
1524
- from what the agent actually produced, skills from live read-only
1525
- recognition. Changes persist to the card and apply the next time the session
1526
- opens.</p>
1527
- <button class="rpgAlta" type="button" id="altaAgente">+ Add an agent</button></div>
1584
+ q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
1585
+ <h1 style="margin-top:6px">Who lives in ${esc(E.city_name)}</h1>
1586
+ <p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
1587
+ and many houses are a city \u2014 it is the same thing the map draws, growing with what that
1588
+ agent actually does. The card and the CLI call them <code class="mono">agents</code>;
1589
+ here you see their houses.</p>
1590
+ <p class="prosa">They belong to <b>this</b> city and only
1591
+ to it: each one's workspace and its mounts live inside
1592
+ <code class="mono">${esc(corto(E.datos))}/agents/</code>, so another city has its own
1593
+ people even if you give them the same names. Every agent is whole here: the kind of work
1594
+ it does, its role, everything it works on \u2014 any number of repositories and document
1595
+ folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
1596
+ <p class="prosa">Every number is real: growth from what the agent actually produced,
1597
+ skills from live read-only recognition. Changes persist to the card and apply the next
1598
+ time the session opens.</p>
1599
+ <button class="rpgAlta" type="button" id="altaAgente">+ Build a house</button></div>
1528
1600
  <div class="fichasRPG">
1529
1601
  <div class="fichaRPG rpgSeat">
1530
1602
  <div class="rpgCab">${caraRPG(E.avatars?.["seat"])}
@@ -1637,7 +1709,7 @@ VISTAS.gente = () => {
1637
1709
  toast(r.error || "Could not add the agent", true);
1638
1710
  return;
1639
1711
  }
1640
- toast(`${nombre.trim()} joined this city \u2014 give it something to work on`);
1712
+ toast(`${nombre.trim()} has a house now \u2014 give it something to work on`);
1641
1713
  void refresca();
1642
1714
  } catch (e) {
1643
1715
  toast(String(e), true);
@@ -1823,7 +1895,7 @@ window.addEventListener("message", (message) => {
1823
1895
  function temaActual() {
1824
1896
  const elegido = document.documentElement.getAttribute("data-tema");
1825
1897
  if (elegido) return elegido === "claro" ? "light" : "dark";
1826
- return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1898
+ return "light";
1827
1899
  }
1828
1900
  function tema() {
1829
1901
  const boton = document.getElementById("temaBoton");
@@ -1834,7 +1906,7 @@ function tema() {
1834
1906
  return "";
1835
1907
  }
1836
1908
  })();
1837
- const oscuroDelSistema = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
1909
+ const oscuroDelSistema = false;
1838
1910
  const pon = (elegido) => {
1839
1911
  const raiz = document.documentElement;
1840
1912
  if (elegido) raiz.setAttribute("data-tema", elegido);
@@ -68,7 +68,7 @@ const CLASES: Array<[string, string, string]> = [
68
68
  ['coordinator', 'It coordinates', 'Its house grows with the decisions it records.'],
69
69
  ];
70
70
 
71
- const PASOS = ['Welcome', 'The work', 'Your chair', 'The agents', 'Ready'];
71
+ const PASOS = ['Welcome', 'The work', 'Your chair', 'The houses', 'Ready'];
72
72
 
73
73
  /** A house, drawn rather than shipped: the same isometric shape the map and the
74
74
  * desktop icon use, so the first thing somebody sees is already the product. */
@@ -144,11 +144,13 @@ export class Bienvenida {
144
144
  <div class="bvArte">${casita('#7048e8')}${casita('#45b9a7')}${casita('#f2674a')}</div>
145
145
  <div>
146
146
  <span class="sub">welcome</span>
147
- <h1>A city is you, and the agents who work with you</h1>
148
- <p class="prosa">You take the chair. Each <b>agent</b> is one worker with its own
149
- window, its own role and its own corner of your disk — a repository, three of them,
150
- a folder of documents, whatever it actually works on. They never talk to each other
151
- behind your back: you chair, they answer.</p>
147
+ <h1>A city is you, and the houses around you</h1>
148
+ <p class="prosa">You take the chair. Around it, one <b>house</b> per worker an
149
+ <b>agent</b> — each with its own window, its own role and its own corner of your
150
+ disk: a repository, three of them, a folder of documents, whatever it actually
151
+ works on. The map draws them as houses that grow with the work done in them, and
152
+ many houses are a city. They never talk to each other behind your back: you chair,
153
+ they answer.</p>
152
154
  <p class="prosa">Everything lives as plain files in
153
155
  <code class="mono">${this.p.esc(this.p.datos)}</code>. Nothing leaves this machine,
154
156
  there is no account, and you can edit any of it by hand afterwards.</p>
@@ -267,15 +269,15 @@ export class Bienvenida {
267
269
  return `
268
270
  <div class="bvPaso">
269
271
  <span class="sub">question 3 of 4</span>
270
- <h1>${this.roster.length ? 'Who else works here?' : 'Who works with you?'}</h1>
271
- <p class="prosa">An <b>agent</b> is one worker: it gets its own window, its own role
272
- and its own corner of your disk. It is <b>not</b> a repository one agent can answer
273
- for three services and a folder of documents at once, and an agent whose work is
274
- documents needs no git anywhere.</p>
272
+ <h1>${this.roster.length ? 'Who else lives here?' : 'Who lives in your city?'}</h1>
273
+ <p class="prosa">Every house holds one worker — an <b>agent</b> with its own window,
274
+ its own role and its own corner of your disk. A house is <b>not</b> a repository: one
275
+ can answer for three services and a folder of documents at once, and a house whose
276
+ work is documents needs no git anywhere.</p>
275
277
  ${this.roster.length ? `<div class="bvFichas">${fichas}</div>` : ''}
276
278
  <div class="bvBotones">
277
279
  <button class="bt ppal" data-bv="nuevo">${
278
- this.roster.length ? 'Add another agent' : 'Add the first agent'
280
+ this.roster.length ? 'Add another house' : 'Build the first house'
279
281
  }</button>
280
282
  <button class="bt" data-bv="siguiente">${
281
283
  this.roster.length ? 'That is everyone' : 'Skip — just me for now'
@@ -308,8 +310,8 @@ export class Bienvenida {
308
310
  .join('');
309
311
  return `
310
312
  <div class="bvPaso">
311
- <span class="sub">a new agent</span>
312
- <h1>Tell me about this one</h1>
313
+ <span class="sub">a new house</span>
314
+ <h1>Who lives in it?</h1>
313
315
  <div class="campo"><label>What do you call it?</label>
314
316
  <input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
315
317
  placeholder="urgencias, api, the handbook — whatever you would say out loud"></div>
@@ -337,7 +339,7 @@ export class Bienvenida {
337
339
  <div class="bvBotones">
338
340
  <button class="bt" data-bv="cancela">Cancel</button>
339
341
  <button class="bt ppal" data-bv="guarda" ${this.guardando ? 'disabled' : ''}>${
340
- this.guardando ? 'adding…' : 'Add this agent'
342
+ this.guardando ? 'building…' : 'Build it'
341
343
  }</button>
342
344
  </div>
343
345
  </div>`;
@@ -351,13 +353,15 @@ export class Bienvenida {
351
353
  <h1>${this.p.esc(this.p.ciudad)} is alive</h1>
352
354
  <p class="prosa">Your seat is written and ${
353
355
  this.roster.length
354
- ? `<b>${this.roster.length}</b> agent${this.roster.length === 1 ? '' : 's'} live here`
355
- : 'the city is waiting for its first agent'
356
+ ? `<b>${this.roster.length}</b> house${this.roster.length === 1 ? '' : 's'} stand${
357
+ this.roster.length === 1 ? 's' : ''
358
+ } around it`
359
+ : 'the city is waiting for its first house'
356
360
  }. Everything you just answered is a plain file you can read and edit.</p>
357
361
  <p class="prosa">What people usually do next:</p>
358
362
  <div class="bvSiguientes">
359
363
  <button class="bt ppal" data-bv="sesion">Open my session</button>
360
- <button class="bt" data-bv="ir-gente">See the agents</button>
364
+ <button class="bt" data-bv="ir-gente">See the houses</button>
361
365
  <button class="bt" data-bv="ir-mapa">Draw the map</button>
362
366
  <button class="bt" data-bv="ir-puesto">Set a goal</button>
363
367
  </div>
@@ -64,6 +64,8 @@ interface Ciudad {
64
64
  slug?: string;
65
65
  id?: string;
66
66
  actual: boolean;
67
+ /** How many agents live in it. An agent belongs to exactly one city. */
68
+ agentes?: number;
67
69
  }
68
70
  interface Road {
69
71
  id: string;
@@ -188,6 +190,16 @@ interface DomainInfo {
188
190
  name: string;
189
191
  summary: string;
190
192
  }
193
+ /** What a reset would do, as the server computes it — never guessed here. */
194
+ interface Efectos {
195
+ city: string;
196
+ backup: string;
197
+ agents: number;
198
+ roads: number;
199
+ deliberations: number;
200
+ keeps: string[];
201
+ loses: string[];
202
+ }
191
203
  interface RepoLocal {
192
204
  nombre: string;
193
205
  ruta: string;
@@ -264,10 +276,10 @@ const SECCIONES: Array<[string, string]> = [
264
276
  ['resumen', 'Overview'],
265
277
  ['mapa', 'The map'],
266
278
  ['puesto', 'My seat'],
267
- ['barrios', 'Districts & houses'],
279
+ ['barrios', 'Districts'],
268
280
  ['red', 'Roads'],
269
281
  ['committee', 'Committee'],
270
- ['gente', 'Agents & skills'],
282
+ ['gente', 'Houses'],
271
283
  ['ciudades', 'Cities'],
272
284
  ];
273
285
 
@@ -282,9 +294,14 @@ async function refresca(): Promise<void> {
282
294
  // A city with no seat or no agents has nothing to draw, and "Not drawn
283
295
  // yet" is a dead end as a first impression. Start where the next step is.
284
296
  const mia = E.tarjetas.find((t) => t.user === E.yo);
285
- // A city with no seat and no agents has nothing to show and everything to
286
- // ask, so it opens the guide instead of a map it cannot draw.
297
+ // Never land on a dead end. Three cases, in order of what the person needs:
298
+ // nothing set up -> the guide, which asks the questions
299
+ // set up, no map yet -> the overview, which says what is left and how
300
+ // set up, map running -> the map, which is the point of the product
301
+ // Landing on the map view without a map was how "Not drawn yet" became the
302
+ // first thing an owner saw every single morning.
287
303
  if (!mia || (E.agents ?? []).length === 0) SECCION = 'bienvenida';
304
+ else if (!E.mapa) SECCION = 'resumen';
288
305
  }
289
306
  pinta();
290
307
  pintaAvisoDeVersion();
@@ -721,21 +738,34 @@ function icono(kind: string): string {
721
738
  }
722
739
 
723
740
  function rail(): void {
724
- q('#rail').innerHTML = SECCIONES.map(([id, et]) => {
725
- const n =
726
- id === 'gente'
727
- ? String(Object.keys(E.skills).length)
728
- : id === 'committee'
729
- ? String(E.deliberations.length)
730
- : id === 'red'
731
- ? String(E.roads.length)
732
- : id === 'barrios'
733
- ? String(E.parcelas.length)
734
- : '';
735
- return `<li class="${id === SECCION ? 'aqui' : ''}" data-s="${id}">
741
+ // Everything above the divider belongs to ONE city — its seat, its agents,
742
+ // its map. Saying so here is the difference between a menu and a place: an
743
+ // agent's workspace and mounts live inside this city's folder, and a flat
744
+ // list of "Cities" and "Agents" as siblings hid that completely.
745
+ const cuantos = (E.agents ?? []).length;
746
+ q('#railCiudad').innerHTML = `<span class="railEtiqueta">you are in</span>
747
+ <b>${esc(E.city_name)}</b>
748
+ <span class="railDe">${esc(E.domain)} · ${cuantos} house${cuantos === 1 ? '' : 's'}</span>`;
749
+ q('#rail').innerHTML = SECCIONES.filter(([id]) => id !== 'ciudades')
750
+ .map(([id, et]) => {
751
+ const n =
752
+ id === 'gente'
753
+ ? String(Object.keys(E.skills).length)
754
+ : id === 'committee'
755
+ ? String(E.deliberations.length)
756
+ : id === 'red'
757
+ ? String(E.roads.length)
758
+ : id === 'barrios'
759
+ ? String(E.parcelas.length)
760
+ : '';
761
+ return `<li class="${id === SECCION ? 'aqui' : ''}" data-s="${id}">
736
762
  <span>${et}</span>${n ? `<span class="n">${n}</span>` : ''}</li>`;
737
- }).join('');
738
- todos<HTMLElement>('#rail li').forEach((li) => {
763
+ })
764
+ .join('');
765
+ q('#railOtras').innerHTML =
766
+ `<li class="${SECCION === 'ciudades' ? 'aqui' : ''}" data-s="ciudades">
767
+ <span>All cities</span><span class="n">${E.ciudades.length}</span></li>`;
768
+ todos<HTMLElement>('#rail li,#railOtras li').forEach((li) => {
739
769
  li.onclick = () => {
740
770
  document.body.classList.remove('enGuia');
741
771
  SECCION = li.dataset.s ?? 'mapa';
@@ -813,6 +843,7 @@ VISTAS.ciudades = () => {
813
843
  (c) => `
814
844
  <div class="fila ${c.actual ? 'on' : ''}">
815
845
  <span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">· open now</b>' : ''}</span>
846
+ <span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? '' : 's'} of its own</span>
816
847
  <span class="de mono">${esc(corto(c.ruta))}</span>
817
848
  <span style="margin-left:auto;display:flex;gap:6px">
818
849
  ${
@@ -829,14 +860,24 @@ VISTAS.ciudades = () => {
829
860
  q('#lienzo').innerHTML = `<div><span class="sub">cities</span>
830
861
  <h1 style="margin-top:6px">Your cities</h1>
831
862
  <p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
832
- its own identity, domain, chair, agents and roads. They share nothing unless you build a
833
- road between them. The one you have open is marked.</p></div>
863
+ its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
864
+ shared it stands inside the city that owns it, with its workspace and mounts under that
865
+ city's folder — so two cities can each have a <code class="mono">docs</code> house and
866
+ they are two different workers. They share nothing unless you build a road between
867
+ them.</p></div>
834
868
  <div class="lista"><div class="filas">${filas}</div></div>
835
869
  <div class="campos" style="max-width:420px">
836
870
  <div class="campo"><label>Start another city</label>
837
871
  <input type="text" id="nuevaCiudad" placeholder="client-a, research, the book…"></div>
838
872
  <button class="bt ppal" id="creaCiudad">Create it</button>
839
873
  </div>
874
+ <div class="peligro">
875
+ <h3>Start this city over</h3>
876
+ <p class="prosa">Takes <b>${esc(E.city_name)}</b> back to its first day: no seat, no
877
+ agents, no committee history, no map. Your repositories and document folders are
878
+ <b>never touched</b> — only the city that points at them.</p>
879
+ <button class="bt malo" id="reiniciaCiudad">Start over…</button>
880
+ </div>
840
881
  <p class="pista">Archiving <b>moves</b> a city into
841
882
  <code class="mono">.backups/</code> — nothing is deleted, and you can bring it back with
842
883
  <code class="mono">mv</code>. The city you are standing in, and the last one you own,
@@ -860,6 +901,46 @@ VISTAS.ciudades = () => {
860
901
  toast(`${nombre} created`);
861
902
  await refresca();
862
903
  };
904
+ const reiniciar = document.getElementById('reiniciaCiudad');
905
+ if (reiniciar)
906
+ reiniciar.onclick = async () => {
907
+ // Two round-trips on purpose: the first asks the server what this would
908
+ // actually do, and the person reads it before typing anything. "Are you
909
+ // sure?" is not information; a list of what disappears is.
910
+ const previa = await api<{ preview?: Efectos; error?: string }>('/api/ciudad-reinicia', {
911
+ method: 'POST',
912
+ body: '{}',
913
+ });
914
+ if (!previa.preview) {
915
+ toast(previa.error ?? 'Could not read what a reset would do', true);
916
+ return;
917
+ }
918
+ const p = previa.preview;
919
+ const texto =
920
+ `Start over ${p.city}?\n\nYou lose:\n` +
921
+ p.loses.map((l) => ` · ${l}`).join('\n') +
922
+ `\n\nRight now that is ${p.agents} agent(s), ${p.roads} road(s) and ` +
923
+ `${p.deliberations} committee act(s).\n\nYou keep:\n` +
924
+ p.keeps.map((l) => ` · ${l}`).join('\n') +
925
+ `\n\nType the city's name to confirm:`;
926
+ const escrito = window.prompt(texto, '');
927
+ if (escrito === null) return;
928
+ if (escrito.trim() !== p.city) {
929
+ toast('That is not the name — nothing was changed', true);
930
+ return;
931
+ }
932
+ const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
933
+ '/api/ciudad-reinicia',
934
+ { method: 'POST', body: JSON.stringify({ confirm: p.city }) },
935
+ );
936
+ if (!r.ok) {
937
+ toast(r.error ?? 'Could not reset it', true);
938
+ return;
939
+ }
940
+ toast(`${p.city} starts over — the old one is at ${r.backup ?? 'its backup'}`);
941
+ primeraCarga = true; // land wherever a fresh city should land: the guide
942
+ await refresca();
943
+ };
863
944
  todos<HTMLButtonElement>('.archiva').forEach((boton) => {
864
945
  boton.onclick = async () => {
865
946
  const nombre = boton.dataset.nombre ?? '';
@@ -899,8 +980,8 @@ VISTAS.resumen = () => {
899
980
  ok: agentes.length > 0,
900
981
  txt:
901
982
  agentes.length === 0
902
- ? 'Add the <b>agents</b> that work in this city — each one whole: its kind, its role, and everything it works on'
903
- : `<b>${agentes.length}</b> agent${agentes.length === 1 ? '' : 's'} in this city: ${esc(
983
+ ? 'Build the <b>houses</b> of this city — one per worker, each whole: its kind, its role, and everything it works on'
984
+ : `<b>${agentes.length}</b> house${agentes.length === 1 ? '' : 's'} in this city: ${esc(
904
985
  agentes.map((a) => a.name).join(', '),
905
986
  )}`,
906
987
  ir: 'gente',
@@ -908,8 +989,8 @@ VISTAS.resumen = () => {
908
989
  {
909
990
  ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
910
991
  txt: agentes.some((a) => (a.mounts ?? []).length)
911
- ? 'Your agents work on real repositories and folders'
912
- : 'Give an agent something to work on — a repository, a worktree, a folder of documents',
992
+ ? 'Every house works on something real'
993
+ : 'Give a house something to work on — a repository, a worktree, a folder of documents',
913
994
  ir: 'gente',
914
995
  },
915
996
  {
@@ -1053,12 +1134,23 @@ VISTAS.mapa = () => {
1053
1134
  };
1054
1135
  return;
1055
1136
  }
1137
+ const cuantos = (E.agents ?? []).length;
1056
1138
  q('#lienzo').innerHTML = `<div><span class="sub">the map</span>
1057
- <h1 style="margin-top:6px">Not drawn yet</h1>
1058
- <p class="prosa" style="margin-top:8px">The map can be served independently.
1059
- Locally the hall runs it for you: first time bakes the front end
1060
- and seeds the local database, which takes about a minute.</p></div>
1061
- <div><button class="bt ppal" id="arrancaMapa">Draw my city</button></div>
1139
+ <h1 style="margin-top:6px">Your city, drawn</h1>
1140
+ <p class="prosa" style="margin-top:8px">${
1141
+ cuantos
1142
+ ? `<b>${cuantos}</b> agent${cuantos === 1 ? '' : 's'} live here, and the map shows
1143
+ them as houses that grow with the work they do — with the town hall in the middle
1144
+ when a committee is sitting, and a gate for every road out.`
1145
+ : `The map draws one house per agent. There are none yet, so it would be an empty
1146
+ plot — build one from <b>Houses</b> first.`
1147
+ }</p>
1148
+ <p class="prosa">The map is its own little server, so it is not running until you say so.
1149
+ The first time takes about a minute: it builds the front end and seeds a local database.
1150
+ After that it opens instantly, and it is yours — nothing is uploaded anywhere.</p></div>
1151
+ <div><button class="bt ppal" id="arrancaMapa">${
1152
+ cuantos ? 'Draw my city' : 'Draw it anyway'
1153
+ }</button></div>
1062
1154
  <p class="cargando" id="mapaEspera" style="display:none">baking the map — first time takes a minute</p>`;
1063
1155
  q<HTMLButtonElement>('#arrancaMapa').onclick = async () => {
1064
1156
  q<HTMLButtonElement>('#arrancaMapa').disabled = true;
@@ -1654,16 +1746,22 @@ VISTAS.gente = () => {
1654
1746
  </div>`;
1655
1747
  };
1656
1748
 
1657
- q('#lienzo').innerHTML = `<div><span class="sub">agents &amp; skills</span>
1658
- <h1 style="margin-top:6px">One seat, the agents behind it</h1>
1659
- <p class="prosa" style="margin-top:8px">A city is its agents, and each one is
1660
- whole here: the kind of work it does, its role, everything it works on any
1661
- number of repositories and document folders at once the engine and effort
1662
- that run it, and the skills in its own home. Every number is real: growth
1663
- from what the agent actually produced, skills from live read-only
1664
- recognition. Changes persist to the card and apply the next time the session
1665
- opens.</p>
1666
- <button class="rpgAlta" type="button" id="altaAgente">+ Add an agent</button></div>
1749
+ q('#lienzo').innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
1750
+ <h1 style="margin-top:6px">Who lives in ${esc(E.city_name)}</h1>
1751
+ <p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
1752
+ and many houses are a city it is the same thing the map draws, growing with what that
1753
+ agent actually does. The card and the CLI call them <code class="mono">agents</code>;
1754
+ here you see their houses.</p>
1755
+ <p class="prosa">They belong to <b>this</b> city and only
1756
+ to it: each one's workspace and its mounts live inside
1757
+ <code class="mono">${esc(corto(E.datos))}/agents/</code>, so another city has its own
1758
+ people even if you give them the same names. Every agent is whole here: the kind of work
1759
+ it does, its role, everything it works on — any number of repositories and document
1760
+ folders at once — the engine and effort that run it, and the skills in its own home.</p>
1761
+ <p class="prosa">Every number is real: growth from what the agent actually produced,
1762
+ skills from live read-only recognition. Changes persist to the card and apply the next
1763
+ time the session opens.</p>
1764
+ <button class="rpgAlta" type="button" id="altaAgente">+ Build a house</button></div>
1667
1765
  <div class="fichasRPG">
1668
1766
  <div class="fichaRPG rpgSeat">
1669
1767
  <div class="rpgCab">${caraRPG(E.avatars?.['seat'])}
@@ -1805,7 +1903,7 @@ VISTAS.gente = () => {
1805
1903
  toast(r.error || 'Could not add the agent', true);
1806
1904
  return;
1807
1905
  }
1808
- toast(`${nombre.trim()} joined this city — give it something to work on`);
1906
+ toast(`${nombre.trim()} has a house now — give it something to work on`);
1809
1907
  void refresca();
1810
1908
  } catch (e) {
1811
1909
  toast(String(e), true);
@@ -2052,8 +2150,9 @@ window.addEventListener('message', (message) => {
2052
2150
  function temaActual(): 'light' | 'dark' {
2053
2151
  const elegido = document.documentElement.getAttribute('data-tema');
2054
2152
  if (elegido) return elegido === 'claro' ? 'light' : 'dark';
2055
- // Day is the default now, so only a machine that asks for dark gets it.
2056
- return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
2153
+ // Day is THE default, full stop: a desktop set to dark does not decide this
2154
+ // for the owner. Only the switch does, and it is remembered.
2155
+ return 'light';
2057
2156
  }
2058
2157
 
2059
2158
  function tema(): void {
@@ -2065,7 +2164,7 @@ function tema(): void {
2065
2164
  return ''; // a browser with site data blocked still gets a working page
2066
2165
  }
2067
2166
  })();
2068
- const oscuroDelSistema = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
2167
+ const oscuroDelSistema = false; // daylight is the default; only the switch changes it
2069
2168
 
2070
2169
  const pon = (elegido: string): void => {
2071
2170
  const raiz = document.documentElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-city",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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.2.0",
4
+ "version": "0.2.1",
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",
@@ -20100,7 +20100,7 @@ try {
20100
20100
  }
20101
20101
  var chair = Boolean(context && context.actors[actor]?.role === "chair" && actor === "seat");
20102
20102
  var server = new Server(
20103
- { name: "agents-city-bus", version: "0.2.0" },
20103
+ { name: "agents-city-bus", version: "0.2.1" },
20104
20104
  {
20105
20105
  capabilities: {
20106
20106
  ...channelEnabled ? { experimental: { "claude/channel": {} } } : {},
@@ -24,7 +24,7 @@ try {
24
24
 
25
25
  const chair = Boolean(context && context.actors[actor]?.role === 'chair' && actor === 'seat');
26
26
  const server = new Server(
27
- { name: 'agents-city-bus', version: '0.2.0' },
27
+ { name: 'agents-city-bus', version: '0.2.1' },
28
28
  {
29
29
  capabilities: {
30
30
  ...(channelEnabled ? { experimental: { 'claude/channel': {} } } : {}),
@@ -67,7 +67,7 @@ export class CodexConnector implements RuntimeConnector {
67
67
  async (method, params) => this.providerRequest(method, params),
68
68
  );
69
69
  await this.rpc.request('initialize', {
70
- clientInfo: { name: 'agents-city', title: 'Agents City', version: '0.2.0' },
70
+ clientInfo: { name: 'agents-city', title: 'Agents City', version: '0.2.1' },
71
71
  capabilities: { experimentalApi: true },
72
72
  });
73
73
  await this.rpc.notify('initialized');
@@ -4894,7 +4894,7 @@ var CodexConnector = class {
4894
4894
  async (method, params) => this.providerRequest(method, params)
4895
4895
  );
4896
4896
  await this.rpc.request("initialize", {
4897
- clientInfo: { name: "agents-city", title: "Agents City", version: "0.2.0" },
4897
+ clientInfo: { name: "agents-city", title: "Agents City", version: "0.2.1" },
4898
4898
  capabilities: { experimentalApi: true }
4899
4899
  });
4900
4900
  await this.rpc.notify("initialized");
@@ -151,28 +151,64 @@ def reinicia(datos, usuario="", dry_run=False):
151
151
  return ctx["backup"]
152
152
 
153
153
 
154
+ def _resuelve_todas(pedidas, usuario):
155
+ """Every requested city as a path, or (None, complaint).
156
+
157
+ `all` means every managed city this owner has. Names resolve one by one and
158
+ a single unknown name aborts the WHOLE run: resetting three of four cities
159
+ and then stopping on a typo is the worst possible outcome of a destructive
160
+ command, so nothing starts until every name is known.
161
+ """
162
+ if len(pedidas) == 1 and pedidas[0].lower() == "all":
163
+ todas = [c["ruta"] for c in cities.lista(usuario)]
164
+ if not todas:
165
+ return None, "there are no cities here to reset"
166
+ return todas, ""
167
+ fuera, desconocidas = [], []
168
+ for nombre in pedidas:
169
+ datos = cities.resuelve(nombre, usuario)
170
+ if datos:
171
+ if datos not in fuera:
172
+ fuera.append(datos)
173
+ else:
174
+ desconocidas.append(nombre)
175
+ if desconocidas:
176
+ conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
177
+ return None, (f"no city called {', '.join(repr(d) for d in desconocidas)}. "
178
+ f"Known here: {conocidas}. Nothing was reset.")
179
+ return fuera, ""
180
+
181
+
154
182
  def main():
155
183
  ap = argparse.ArgumentParser(
156
- description="Reset one city to onboarding; repos stay untouched and data is backed up"
184
+ description="Reset cities to onboarding; repos stay untouched and data is backed up"
157
185
  )
158
- ap.add_argument("city", help="city name or path (required)")
186
+ ap.add_argument("city", nargs="+",
187
+ help="one or more city names or paths, space separated — or `all`")
159
188
  ap.add_argument("--dry-run", action="store_true", help="show every effect, change nothing")
160
189
  a = ap.parse_args()
161
190
  usuario = cities.usuario_actual()
162
- datos = cities.resuelve(a.city, usuario)
163
- if not datos:
164
- conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
165
- print(f" No city called {a.city!r}. Known here: {conocidas}.", file=sys.stderr)
166
- return 1
167
- try:
168
- backup = reinicia(datos, usuario, a.dry_run)
169
- except ValueError as e:
170
- print(f" {e}", file=sys.stderr)
191
+ objetivo, queja = _resuelve_todas(a.city, usuario)
192
+ if not objetivo:
193
+ print(f" {queja}", file=sys.stderr)
171
194
  return 1
195
+ # More than one city at once is a bigger gesture than the command's name
196
+ # suggests, so it says out loud what it is about to do before doing it.
197
+ if len(objetivo) > 1 and not a.dry_run:
198
+ print(f" Resetting {len(objetivo)} cities: "
199
+ f"{', '.join(cities.slug_ciudad(d) for d in objetivo)}")
200
+ for datos in objetivo:
201
+ try:
202
+ backup = reinicia(datos, usuario, a.dry_run)
203
+ except ValueError as e:
204
+ print(f" {cities.slug_ciudad(datos)}: {e}", file=sys.stderr)
205
+ return 1
206
+ if not a.dry_run:
207
+ corto = backup.replace(os.path.expanduser("~"), "~")
208
+ print(f" City `{cities.slug_ciudad(datos)}` reset. Recovery copy: {corto}")
172
209
  if not a.dry_run:
173
- corto = backup.replace(os.path.expanduser("~"), "~")
174
- print(f" City `{cities.slug_ciudad(datos)}` reset. Recovery copy: {corto}")
175
- print(f" Run `agents-city seat --city {cities.slug_ciudad(datos)}` for onboarding.")
210
+ primera = cities.slug_ciudad(objetivo[0])
211
+ print(f" Run `agents-city seat --city {primera}` for onboarding.")
176
212
  return 0
177
213
 
178
214