agents-city 0.1.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.
@@ -1,3 +1,395 @@
1
+ // city/web/src/bienvenida.ts
2
+ var CLASES = [
3
+ [
4
+ "code",
5
+ "It writes code",
6
+ "Its house grows with the pull requests it merges. Mounts repositories and worktrees."
7
+ ],
8
+ [
9
+ "knowledge",
10
+ "It keeps knowledge",
11
+ "Its house grows with the documents it writes. Needs no git at all."
12
+ ],
13
+ ["coordinator", "It coordinates", "Its house grows with the decisions it records."]
14
+ ];
15
+ var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
16
+ function casita(color) {
17
+ return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
18
+ <polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
19
+ <polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
20
+ <polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
21
+ </svg>`;
22
+ }
23
+ var Bienvenida = class {
24
+ constructor(p) {
25
+ this.p = p;
26
+ }
27
+ paso = 0;
28
+ dominios = [];
29
+ roles = [];
30
+ rolesDeAgente = [];
31
+ repos = [];
32
+ dominio = "";
33
+ rol = "";
34
+ roster = [];
35
+ enCurso = null;
36
+ guardando = false;
37
+ buscandoRepos = false;
38
+ reposListos = false;
39
+ lienzo = null;
40
+ /** Draw the current step into `lienzo`. The only entry point. */
41
+ async pinta(lienzo) {
42
+ this.lienzo = lienzo;
43
+ if (!this.dominios.length) {
44
+ lienzo.innerHTML = '<p class="cargando">reading the domain packs</p>';
45
+ const [d, ra] = await Promise.all([
46
+ this.p.api("/api/domains"),
47
+ this.p.api("/api/roles?scope=agent")
48
+ ]);
49
+ this.dominios = d.domains;
50
+ this.rolesDeAgente = ra.roles;
51
+ }
52
+ lienzo.innerHTML = `<div class="bv">${this.cabecera()}${await this.cuerpo()}</div>`;
53
+ this.enlaza(lienzo);
54
+ }
55
+ cabecera() {
56
+ const puntos = PASOS.map(
57
+ (nombre, i) => `<span class="bvPunto ${i === this.paso ? "aqui" : i < this.paso ? "hecho" : ""}"
58
+ title="${this.p.esc(nombre)}"><i></i>${this.p.esc(nombre)}</span>`
59
+ ).join("");
60
+ return `<div class="bvPasos">${puntos}</div>`;
61
+ }
62
+ async cuerpo() {
63
+ switch (this.paso) {
64
+ case 0:
65
+ return this.pantallaHola();
66
+ case 1:
67
+ return this.pantallaDominio();
68
+ case 2:
69
+ return await this.pantallaRol();
70
+ case 3:
71
+ return this.pantallaAgentes();
72
+ default:
73
+ return this.pantallaFinal();
74
+ }
75
+ }
76
+ // ── 0. what this is ────────────────────────────────────────────────────────
77
+ pantallaHola() {
78
+ return `
79
+ <div class="bvHola">
80
+ <div class="bvArte">${casita("#7048e8")}${casita("#45b9a7")}${casita("#f2674a")}</div>
81
+ <div>
82
+ <span class="sub">welcome</span>
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>
90
+ <p class="prosa">Everything lives as plain files in
91
+ <code class="mono">${this.p.esc(this.p.datos)}</code>. Nothing leaves this machine,
92
+ there is no account, and you can edit any of it by hand afterwards.</p>
93
+ <p class="prosa"><b>Four questions and you are working.</b> You can skip any of them
94
+ and change everything later.</p>
95
+ <div class="bvBotones">
96
+ <button class="bt ppal" data-bv="siguiente">Let\u2019s build it</button>
97
+ <button class="bt" data-bv="salir">I\u2019ll set it up myself</button>
98
+ </div>
99
+ </div>
100
+ </div>`;
101
+ }
102
+ // ── 1. the domain ─────────────────────────────────────────────────────────
103
+ pantallaDominio() {
104
+ const tarjetas = this.dominios.map(
105
+ (d) => `
106
+ <button class="bvOpcion ${this.dominio === d.id ? "elegida" : ""}" data-bv="dominio"
107
+ data-id="${this.p.esc(d.id)}">
108
+ <b>${this.p.esc(d.name)}</b><span>${this.p.esc(d.summary)}</span></button>`
109
+ ).join("");
110
+ return `
111
+ <div class="bvPaso">
112
+ <span class="sub">question 1 of 4</span>
113
+ <h1>What kind of work happens here?</h1>
114
+ <p class="prosa">It decides the vocabulary, the roles you will be offered and what
115
+ counts as evidence in a decision. A clinic does not ship pull requests, and a law
116
+ firm does not measure story points. Pick the closest one \u2014 you can change it later.</p>
117
+ <div class="bvRejilla">${tarjetas}</div>
118
+ <div class="bvBotones">
119
+ <button class="bt" data-bv="atras">Back</button>
120
+ <button class="bt ppal" data-bv="siguiente" ${this.dominio ? "" : "disabled"}>Next</button>
121
+ </div>
122
+ </div>`;
123
+ }
124
+ // ── 2. your chair ─────────────────────────────────────────────────────────
125
+ async pantallaRol() {
126
+ if (!this.roles.length || this.roles[0]?.id === void 0) {
127
+ const r = await this.p.api(
128
+ "/api/roles?domain=" + encodeURIComponent(this.dominio)
129
+ );
130
+ this.roles = r.roles;
131
+ }
132
+ const tarjetas = this.roles.map(
133
+ (r) => `
134
+ <button class="bvOpcion ${this.rol === r.id ? "elegida" : ""}" data-bv="rol"
135
+ data-id="${this.p.esc(r.id)}">
136
+ <b>${this.p.esc(r.name)}</b><span>${this.p.esc(r.summary || r.trade)}</span></button>`
137
+ ).join("");
138
+ return `
139
+ <div class="bvPaso">
140
+ <span class="sub">question 2 of 4</span>
141
+ <h1>And what are you, here?</h1>
142
+ <p class="prosa">You chair this city whatever you answer \u2014 this is your
143
+ <b>speciality</b>, not your authority. It shapes the perspective you bring to a
144
+ decision and the knowledge files the city writes for you. <b>Blank</b> is a real
145
+ answer: it means no preset knowledge.</p>
146
+ <div class="bvRejilla">${tarjetas}</div>
147
+ <div class="bvBotones">
148
+ <button class="bt" data-bv="atras">Back</button>
149
+ <button class="bt ppal" data-bv="siguiente" ${this.rol ? "" : "disabled"}>Next</button>
150
+ </div>
151
+ </div>`;
152
+ }
153
+ // ── 3. the agents ─────────────────────────────────────────────────────────
154
+ pantallaAgentes() {
155
+ this.buscaReposUnaVez();
156
+ if (!this.enCurso) return this.listaDeAgentes();
157
+ return this.formularioDeAgente(this.enCurso);
158
+ }
159
+ buscaReposUnaVez() {
160
+ if (this.reposListos || this.buscandoRepos) return;
161
+ this.buscandoRepos = true;
162
+ void this.p.api("/api/misrepos?user=" + encodeURIComponent(this.p.yo)).then((r) => {
163
+ this.repos = (r.repos ?? []).slice(0, 200);
164
+ }).catch(() => {
165
+ this.repos = [];
166
+ }).finally(() => {
167
+ this.buscandoRepos = false;
168
+ this.reposListos = true;
169
+ if (this.lienzo && this.paso === 3) void this.pinta(this.lienzo);
170
+ });
171
+ }
172
+ listaDeAgentes() {
173
+ const fichas = this.roster.map(
174
+ (a) => `
175
+ <div class="bvFicha">
176
+ <b>${this.p.esc(a.nombre)}</b>
177
+ <span>${this.p.esc(a.clase)} \xB7 ${this.p.esc(a.rol)}</span>
178
+ <span class="de">${a.montajes.length ? this.p.esc(a.montajes.map((m) => m.split("/").pop()).join(", ")) : "nothing mounted yet"}</span>
179
+ </div>`
180
+ ).join("");
181
+ return `
182
+ <div class="bvPaso">
183
+ <span class="sub">question 3 of 4</span>
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>
189
+ ${this.roster.length ? `<div class="bvFichas">${fichas}</div>` : ""}
190
+ <div class="bvBotones">
191
+ <button class="bt ppal" data-bv="nuevo">${this.roster.length ? "Add another house" : "Build the first house"}</button>
192
+ <button class="bt" data-bv="siguiente">${this.roster.length ? "That is everyone" : "Skip \u2014 just me for now"}</button>
193
+ </div>
194
+ </div>`;
195
+ }
196
+ formularioDeAgente(a) {
197
+ const clases = CLASES.map(
198
+ ([id, titulo, porque]) => `
199
+ <button class="bvOpcion ${a.clase === id ? "elegida" : ""}" data-bv="clase" data-id="${id}">
200
+ <b>${titulo}</b><span>${porque}</span></button>`
201
+ ).join("");
202
+ const roles = this.rolesDeAgente.map(
203
+ (r) => `<option value="${this.p.esc(r.id)}"${r.id === a.rol ? " selected" : ""}>${this.p.esc(
204
+ r.name
205
+ )}</option>`
206
+ ).join("");
207
+ const sugeridos = this.repos.filter((r) => r.mio).slice(0, 12).map(
208
+ (r) => `<button class="bvChip ${a.montajes.includes(r.ruta) ? "elegida" : ""}"
209
+ data-bv="monta" data-ruta="${this.p.esc(r.ruta)}">${this.p.esc(r.nombre)}</button>`
210
+ ).join("");
211
+ return `
212
+ <div class="bvPaso">
213
+ <span class="sub">a new house</span>
214
+ <h1>Who lives in it?</h1>
215
+ <div class="campo"><label>What do you call it?</label>
216
+ <input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
217
+ placeholder="urgencias, api, the handbook \u2014 whatever you would say out loud"></div>
218
+ <label class="bvEtiqueta">What kind of work does it do?</label>
219
+ <div class="bvRejilla bvTres">${clases}</div>
220
+ <div class="campo"><label>Its role \u2014 its speciality, never authority</label>
221
+ <select id="bvRol">${roles}</select></div>
222
+ <label class="bvEtiqueta">What does it work on?</label>
223
+ <p class="pista">Pick any of your repositories, and add folders by hand. As many as
224
+ you like \u2014 this is the part that makes it an agent and not a repo.</p>
225
+ ${sugeridos ? `<div class="bvChips">${sugeridos}</div>` : this.buscandoRepos ? '<p class="cargando">looking through your disk for repositories\u2026</p>' : '<p class="pista">No repositories found on this disk \u2014 add folders by path below.</p>'}
226
+ <div class="campo"><label>\u2026or a path, and Enter</label>
227
+ <input type="text" id="bvRuta" placeholder="~/documents/handbook"></div>
228
+ ${a.montajes.length ? `<p class="pista">Working on: <b>${this.p.esc(a.montajes.join(", "))}</b></p>` : ""}
229
+ <div class="bvBotones">
230
+ <button class="bt" data-bv="cancela">Cancel</button>
231
+ <button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${this.guardando ? "building\u2026" : "Build it"}</button>
232
+ </div>
233
+ </div>`;
234
+ }
235
+ // ── 4. done ───────────────────────────────────────────────────────────────
236
+ pantallaFinal() {
237
+ return `
238
+ <div class="bvPaso">
239
+ <span class="sub">ready</span>
240
+ <h1>${this.p.esc(this.p.ciudad)} is alive</h1>
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>
242
+ <p class="prosa">What people usually do next:</p>
243
+ <div class="bvSiguientes">
244
+ <button class="bt ppal" data-bv="sesion">Open my session</button>
245
+ <button class="bt" data-bv="ir-gente">See the houses</button>
246
+ <button class="bt" data-bv="ir-mapa">Draw the map</button>
247
+ <button class="bt" data-bv="ir-puesto">Set a goal</button>
248
+ </div>
249
+ <p class="pista">A goal is optional, and the city works without one \u2014 but a round with
250
+ no goal is a status report, because there is nothing to argue against.</p>
251
+ </div>`;
252
+ }
253
+ // ── wiring ────────────────────────────────────────────────────────────────
254
+ enlaza(lienzo) {
255
+ const repinta = () => void this.pinta(lienzo);
256
+ lienzo.querySelectorAll("[data-bv]").forEach((el) => {
257
+ el.onclick = async (evento) => {
258
+ evento.preventDefault();
259
+ const que = el.dataset.bv;
260
+ if (que === "salir") return this.p.vete("resumen");
261
+ if (que === "atras") {
262
+ this.paso = Math.max(0, this.paso - 1);
263
+ return repinta();
264
+ }
265
+ if (que === "dominio") {
266
+ this.dominio = el.dataset.id ?? "";
267
+ this.roles = [];
268
+ this.rol = "";
269
+ return repinta();
270
+ }
271
+ if (que === "rol") {
272
+ this.rol = el.dataset.id ?? "";
273
+ return repinta();
274
+ }
275
+ if (que === "nuevo") {
276
+ this.enCurso = { nombre: "", clase: "code", rol: "blank", montajes: [] };
277
+ return repinta();
278
+ }
279
+ if (que === "clase" && this.enCurso) {
280
+ this.enCurso.clase = el.dataset.id ?? "code";
281
+ this.recogeFormulario(lienzo);
282
+ return repinta();
283
+ }
284
+ if (que === "monta" && this.enCurso) {
285
+ const ruta2 = el.dataset.ruta ?? "";
286
+ const ya = this.enCurso.montajes.indexOf(ruta2);
287
+ if (ya >= 0) this.enCurso.montajes.splice(ya, 1);
288
+ else this.enCurso.montajes.push(ruta2);
289
+ this.recogeFormulario(lienzo);
290
+ return repinta();
291
+ }
292
+ if (que === "cancela") {
293
+ this.enCurso = null;
294
+ return repinta();
295
+ }
296
+ if (que === "guarda") return void await this.guardaAgente(lienzo);
297
+ if (que === "siguiente") return void await this.avanza(lienzo);
298
+ if (que === "sesion") return void await this.abreSesion();
299
+ if (que === "ir-gente") return this.p.vete("gente");
300
+ if (que === "ir-mapa") return this.p.vete("mapa");
301
+ if (que === "ir-puesto") return this.p.vete("puesto");
302
+ };
303
+ });
304
+ const ruta = lienzo.querySelector("#bvRuta");
305
+ if (ruta)
306
+ ruta.onkeydown = (e) => {
307
+ if (e.key !== "Enter" || !this.enCurso) return;
308
+ e.preventDefault();
309
+ const valor = ruta.value.trim();
310
+ if (valor) this.enCurso.montajes.push(valor);
311
+ this.recogeFormulario(lienzo);
312
+ void this.pinta(lienzo);
313
+ };
314
+ lienzo.querySelector("#bvNombre")?.focus();
315
+ }
316
+ /** Keep what is typed when a click forces a repaint. */
317
+ recogeFormulario(lienzo) {
318
+ if (!this.enCurso) return;
319
+ const nombre = lienzo.querySelector("#bvNombre");
320
+ const rol = lienzo.querySelector("#bvRol");
321
+ if (nombre) this.enCurso.nombre = nombre.value;
322
+ if (rol) this.enCurso.rol = rol.value;
323
+ }
324
+ async guardaAgente(lienzo) {
325
+ this.recogeFormulario(lienzo);
326
+ const a = this.enCurso;
327
+ if (!a) return;
328
+ if (!a.nombre.trim()) {
329
+ this.p.aviso("Give it a name \u2014 it is how you will call it in its window", true);
330
+ return;
331
+ }
332
+ this.guardando = true;
333
+ await this.pinta(lienzo);
334
+ try {
335
+ await this.aseguraFicha();
336
+ const r = await this.p.api("/api/agentes", {
337
+ method: "POST",
338
+ body: JSON.stringify({ name: a.nombre.trim(), kind: a.clase, role: a.rol })
339
+ });
340
+ if (!r.ok || !r.agent) {
341
+ this.p.aviso(r.error || "Could not add that agent", true);
342
+ return;
343
+ }
344
+ for (const ruta of a.montajes) {
345
+ const m = await this.p.api("/api/montaje", {
346
+ method: "POST",
347
+ body: JSON.stringify({ agent: r.agent, add: ruta })
348
+ });
349
+ if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? "could not mount"}`, true);
350
+ }
351
+ this.roster.push({ ...a, nombre: a.nombre.trim() });
352
+ this.enCurso = null;
353
+ this.p.aviso(`${a.nombre.trim()} joined the city`);
354
+ await this.p.refresca();
355
+ } catch (e) {
356
+ this.p.aviso(String(e), true);
357
+ } finally {
358
+ this.guardando = false;
359
+ await this.pinta(lienzo);
360
+ }
361
+ }
362
+ async aseguraFicha() {
363
+ await this.p.api("/api/ficha", {
364
+ method: "POST",
365
+ body: JSON.stringify({ domain: this.dominio || "software", role: this.rol || "blank" })
366
+ });
367
+ }
368
+ async avanza(lienzo) {
369
+ const escribeLaFicha = this.paso === 2;
370
+ this.paso = Math.min(PASOS.length - 1, this.paso + 1);
371
+ await this.pinta(lienzo);
372
+ if (escribeLaFicha) {
373
+ await this.aseguraFicha();
374
+ await this.p.refresca();
375
+ }
376
+ }
377
+ async abreSesion() {
378
+ try {
379
+ const r = await this.p.api("/api/sesion", {
380
+ method: "POST",
381
+ body: JSON.stringify({ user: this.p.yo })
382
+ });
383
+ this.p.aviso(
384
+ r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : r.error ?? "Could not",
385
+ !r.ok
386
+ );
387
+ } catch (e) {
388
+ this.p.aviso(String(e), true);
389
+ }
390
+ }
391
+ };
392
+
1
393
  // city/web/src/activity.ts
2
394
  var MAP_ACTIVITY_PROTOCOL = "agents-city-map-activity/1";
3
395
  function isMapNavMessage(value) {
@@ -64,13 +456,21 @@ var SECCIONES = [
64
456
  ["resumen", "Overview"],
65
457
  ["mapa", "The map"],
66
458
  ["puesto", "My seat"],
67
- ["barrios", "Districts & houses"],
459
+ ["barrios", "Districts"],
68
460
  ["red", "Roads"],
69
461
  ["committee", "Committee"],
70
- ["gente", "Agents & skills"]
462
+ ["gente", "Houses"],
463
+ ["ciudades", "Cities"]
71
464
  ];
465
+ var primeraCarga = true;
72
466
  async function refresca() {
73
467
  E = await api("/api/estado");
468
+ if (primeraCarga) {
469
+ primeraCarga = false;
470
+ const mia = E.tarjetas.find((t) => t.user === E.yo);
471
+ if (!mia || (E.agents ?? []).length === 0) SECCION = "bienvenida";
472
+ else if (!E.mapa) SECCION = "resumen";
473
+ }
74
474
  pinta();
75
475
  pintaAvisoDeVersion();
76
476
  pintaDemoControles();
@@ -385,13 +785,20 @@ function icono(kind) {
385
785
  return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>';
386
786
  }
387
787
  function rail() {
388
- 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]) => {
389
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) : "";
390
794
  return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
391
795
  <span>${et}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
392
796
  }).join("");
393
- 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) => {
394
800
  li.onclick = () => {
801
+ document.body.classList.remove("enGuia");
395
802
  SECCION = li.dataset.s ?? "mapa";
396
803
  pinta();
397
804
  };
@@ -432,33 +839,188 @@ function pinta() {
432
839
  q(".cuerpo").scrollTop = 0;
433
840
  }
434
841
  var VISTAS = {};
842
+ var guia = null;
843
+ VISTAS.bienvenida = () => {
844
+ document.body.classList.add("enGuia");
845
+ guia ??= new Bienvenida({
846
+ api,
847
+ esc,
848
+ aviso: toast,
849
+ refresca,
850
+ vete: (seccion) => {
851
+ document.body.classList.remove("enGuia");
852
+ SECCION = seccion;
853
+ pinta();
854
+ },
855
+ ciudad: E.city_name,
856
+ yo: E.yo,
857
+ datos: corto(E.datos)
858
+ });
859
+ void guia.pinta(q("#lienzo"));
860
+ };
861
+ VISTAS.ciudades = () => {
862
+ const filas = E.ciudades.map(
863
+ (c) => `
864
+ <div class="fila ${c.actual ? "on" : ""}">
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>
867
+ <span class="de mono">${esc(corto(c.ruta))}</span>
868
+ <span style="margin-left:auto;display:flex;gap:6px">
869
+ ${c.actual ? "" : `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
870
+ <button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
871
+ data-nombre="${esc(c.nombre)}">archive</button>`}
872
+ </span>
873
+ </div>`
874
+ ).join("");
875
+ q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
876
+ <h1 style="margin-top:6px">Your cities</h1>
877
+ <p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
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>
883
+ <div class="lista"><div class="filas">${filas}</div></div>
884
+ <div class="campos" style="max-width:420px">
885
+ <div class="campo"><label>Start another city</label>
886
+ <input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
887
+ <button class="bt ppal" id="creaCiudad">Create it</button>
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>
896
+ <p class="pista">Archiving <b>moves</b> a city into
897
+ <code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
898
+ <code class="mono">mv</code>. The city you are standing in, and the last one you own,
899
+ cannot be archived.</p>`;
900
+ enlaza();
901
+ const campo = q("#nuevaCiudad");
902
+ q("#creaCiudad").onclick = async () => {
903
+ const nombre = campo.value.trim();
904
+ if (!nombre) {
905
+ toast("Give it a name", true);
906
+ return;
907
+ }
908
+ const r = await api("/api/ciudades", {
909
+ method: "POST",
910
+ body: JSON.stringify({ name: nombre })
911
+ });
912
+ if (!r.ok) {
913
+ toast(r.error ?? "Could not create it", true);
914
+ return;
915
+ }
916
+ toast(`${nombre} created`);
917
+ await refresca();
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
+ };
960
+ todos(".archiva").forEach((boton) => {
961
+ boton.onclick = async () => {
962
+ const nombre = boton.dataset.nombre ?? "";
963
+ if (!window.confirm(`Archive ${nombre}? It moves into .backups \u2014 nothing is deleted.`))
964
+ return;
965
+ const r = await api(
966
+ "/api/ciudad-archiva",
967
+ { method: "POST", body: JSON.stringify({ city: boton.dataset.ciudad ?? "" }) }
968
+ );
969
+ if (!r.ok) {
970
+ toast(r.error ?? "Could not archive it", true);
971
+ return;
972
+ }
973
+ toast(`${nombre} archived \u2014 it is in ${r.backup ?? ".backups"}`);
974
+ await refresca();
975
+ };
976
+ });
977
+ };
435
978
  VISTAS.resumen = () => {
436
979
  const mia = E.tarjetas.find((t) => t.user === E.yo);
437
980
  const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
981
+ const agentes = E.agents ?? [];
438
982
  const tareas = [
439
983
  {
440
984
  ok: !!mia,
441
- txt: mia ? `Your card exists \u2014 you are <b>${esc(mia.agent)}</b>` : "Take your seat: role, folders, one goal",
985
+ txt: mia ? `Your seat is taken \u2014 you are <b>${esc(mia.agent)}</b> in <b>${esc(E.domain)}</b>` : "Take your seat: the work domain and your role in it",
442
986
  ir: "puesto"
443
987
  },
988
+ {
989
+ ok: agentes.length > 0,
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(
991
+ agentes.map((a) => a.name).join(", ")
992
+ )}`,
993
+ ir: "gente"
994
+ },
995
+ {
996
+ ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
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",
998
+ ir: "gente"
999
+ },
444
1000
  {
445
1001
  ok: !!(mia && mia.goals_defined),
446
1002
  txt: mia && mia.goals_defined ? `Your goal: <b>${esc(mia.objetivo ? mia.objetivo.title : "")}</b>` : "Set <b>one goal</b>, with the command that measures it",
447
1003
  ir: "puesto"
448
1004
  },
449
1005
  {
450
- ok: E.parcelas.length > 0 && sinUnidad === 0,
451
- txt: E.parcelas.length === 0 ? "No houses yet \u2014 pick your folders and they appear" : sinUnidad ? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district \u2014 assign them` : "Every house has its district",
1006
+ ok: E.parcelas.length === 0 || sinUnidad === 0,
1007
+ txt: E.parcelas.length === 0 ? "Districts appear on the map once agents have houses" : sinUnidad ? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district \u2014 assign them` : "Every house has its district",
452
1008
  ir: "barrios"
453
1009
  }
454
1010
  ];
1011
+ const empezando = !mia || agentes.length === 0;
455
1012
  const sesionArriba = E.tmux.includes(E.sesion);
456
1013
  q("#lienzo").innerHTML = `
457
- <div><span class="sub">your city</span>
458
- <h1 style="margin-top:6px">${esc(E.city_name)}</h1>
1014
+ <div><span class="sub">${empezando ? "welcome" : "your city"}</span>
1015
+ <h1 style="margin-top:6px">${empezando ? "Let\u2019s build " + esc(E.city_name) : esc(E.city_name)}</h1>
459
1016
  <p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
460
- <p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
461
- counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p></div>
1017
+ ${empezando ? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
1018
+ it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
1019
+ it \u2014 then add the agents, each one asked for whole: what kind of work it does,
1020
+ its role, everything it works on, the engine that runs it and the skills it
1021
+ starts with. Everything below is a plain file in
1022
+ <code class="mono">${esc(corto(E.datos))}</code> you can also edit by hand.</p>` : `<p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
1023
+ counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p>`}</div>
462
1024
  <div class="cifras">
463
1025
  <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
464
1026
  <div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
@@ -543,12 +1105,17 @@ VISTAS.mapa = () => {
543
1105
  };
544
1106
  return;
545
1107
  }
1108
+ const cuantos = (E.agents ?? []).length;
546
1109
  q("#lienzo").innerHTML = `<div><span class="sub">the map</span>
547
- <h1 style="margin-top:6px">Not drawn yet</h1>
548
- <p class="prosa" style="margin-top:8px">The map can be served independently.
549
- Locally the hall runs it for you: first time bakes the front end
550
- and seeds the local database, which takes about a minute.</p></div>
551
- <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>
552
1119
  <p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`;
553
1120
  q("#arrancaMapa").onclick = async () => {
554
1121
  q("#arrancaMapa").disabled = true;
@@ -1014,16 +1581,22 @@ VISTAS.gente = () => {
1014
1581
  + zip<input type="file" accept=".zip" data-agente="${esc(a.slug)}"></label></div>
1015
1582
  </div>`;
1016
1583
  };
1017
- q("#lienzo").innerHTML = `<div><span class="sub">agents &amp; skills</span>
1018
- <h1 style="margin-top:6px">One seat, the agents behind it</h1>
1019
- <p class="prosa" style="margin-top:8px">A city is its agents, and each one is
1020
- whole here: the kind of work it does, its role, everything it works on \u2014 any
1021
- number of repositories and document folders at once \u2014 the engine and effort
1022
- that run it, and the skills in its own home. Every number is real: growth
1023
- from what the agent actually produced, skills from live read-only
1024
- recognition. Changes persist to the card and apply the next time the session
1025
- opens.</p>
1026
- <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>
1027
1600
  <div class="fichasRPG">
1028
1601
  <div class="fichaRPG rpgSeat">
1029
1602
  <div class="rpgCab">${caraRPG(E.avatars?.["seat"])}
@@ -1136,7 +1709,7 @@ VISTAS.gente = () => {
1136
1709
  toast(r.error || "Could not add the agent", true);
1137
1710
  return;
1138
1711
  }
1139
- 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`);
1140
1713
  void refresca();
1141
1714
  } catch (e) {
1142
1715
  toast(String(e), true);
@@ -1322,7 +1895,7 @@ window.addEventListener("message", (message) => {
1322
1895
  function temaActual() {
1323
1896
  const elegido = document.documentElement.getAttribute("data-tema");
1324
1897
  if (elegido) return elegido === "claro" ? "light" : "dark";
1325
- return window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark";
1898
+ return "light";
1326
1899
  }
1327
1900
  function tema() {
1328
1901
  const boton = document.getElementById("temaBoton");
@@ -1333,13 +1906,13 @@ function tema() {
1333
1906
  return "";
1334
1907
  }
1335
1908
  })();
1336
- const claroDelSistema = window.matchMedia?.("(prefers-color-scheme: light)").matches ?? false;
1909
+ const oscuroDelSistema = false;
1337
1910
  const pon = (elegido) => {
1338
1911
  const raiz = document.documentElement;
1339
1912
  if (elegido) raiz.setAttribute("data-tema", elegido);
1340
1913
  else raiz.removeAttribute("data-tema");
1341
- const claro = elegido ? elegido === "claro" : claroDelSistema;
1342
- if (boton) boton.textContent = claro ? "\u2600 Day" : "\u263E Night";
1914
+ const claro = elegido ? elegido === "claro" : !oscuroDelSistema;
1915
+ if (boton) boton.textContent = claro ? "\u263E Night" : "\u2600 Day";
1343
1916
  const frame = document.querySelector("#cityMapFrame");
1344
1917
  frame?.contentWindow?.postMessage(
1345
1918
  { type: "agents-city-map-theme/1", theme: claro ? "light" : "dark" },
@@ -1349,7 +1922,7 @@ function tema() {
1349
1922
  pon(guardado);
1350
1923
  if (boton)
1351
1924
  boton.onclick = () => {
1352
- const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : claroDelSistema;
1925
+ const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : !oscuroDelSistema;
1353
1926
  const elegido = claroAhora ? "oscuro" : "claro";
1354
1927
  try {
1355
1928
  localStorage.setItem("hall-tema", elegido);