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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +12 -3
- package/README.md +10 -2
- package/bin/hall.html +24 -22
- package/bin/navegador.mjs +33 -5
- package/bin/serve.py +66 -0
- package/bin/test-cities.py +45 -0
- package/bin/test-seat.py +1 -1
- package/bin/test-serve.py +8 -3
- package/city/web/dist-hall/hall.js +115 -43
- package/city/web/src/bienvenida.ts +22 -18
- package/city/web/src/hall.ts +142 -43
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/reset.py +50 -14
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
|
|
1061
|
+
Reinicia ciudades gestionadas conservando su identidad estable y sus repos.
|
|
1062
1062
|
|
|
1063
1063
|
```bash
|
|
1064
|
-
agents-city reset
|
|
1065
|
-
agents-city reset
|
|
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
|
|
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.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/* Two skins, one set of names. DAY is the default — the 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
|
-
/*
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 => /
|
|
181
|
-
if (!ir) return { falta: 'no
|
|
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
|
|
198
|
+
comprueba('the houses view renders its sheets', (abierto?.fichas ?? 0) > 0, JSON.stringify(abierto));
|
|
199
199
|
for (const [control, etiqueta] of [
|
|
200
|
-
['alta', '
|
|
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'} —
|
|
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 "")
|
package/bin/test-cities.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
and
|
|
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
|
|
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
|
|
84
|
-
<p class="prosa">You take the chair.
|
|
85
|
-
window, its own role and its own corner of your
|
|
86
|
-
a folder of documents, whatever it actually
|
|
87
|
-
|
|
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
|
|
183
|
-
<p class="prosa">
|
|
184
|
-
and its own corner of your disk.
|
|
185
|
-
for three services and a folder of documents at once, and
|
|
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
|
|
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
|
|
212
|
-
<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 ? "
|
|
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>
|
|
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
|
|
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
|
|
459
|
+
["barrios", "Districts"],
|
|
458
460
|
["red", "Roads"],
|
|
459
461
|
["committee", "Committee"],
|
|
460
|
-
["gente", "
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
869
|
-
|
|
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 ? "
|
|
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) ? "
|
|
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">
|
|
1049
|
-
<p class="prosa" style="margin-top:8px">
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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">
|
|
1519
|
-
<h1 style="margin-top:6px">
|
|
1520
|
-
<p class="prosa" style="margin-top:8px">A
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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()}
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
148
|
-
<p class="prosa">You take the chair.
|
|
149
|
-
window, its own role and its own corner of your
|
|
150
|
-
a folder of documents, whatever it actually
|
|
151
|
-
|
|
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
|
|
271
|
-
<p class="prosa">
|
|
272
|
-
and its own corner of your disk.
|
|
273
|
-
for three services and a folder of documents at once, and
|
|
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
|
|
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
|
|
312
|
-
<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 ? '
|
|
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>
|
|
355
|
-
|
|
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
|
|
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>
|
package/city/web/src/hall.ts
CHANGED
|
@@ -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
|
|
279
|
+
['barrios', 'Districts'],
|
|
268
280
|
['red', 'Roads'],
|
|
269
281
|
['committee', 'Committee'],
|
|
270
|
-
['gente', '
|
|
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
|
-
//
|
|
286
|
-
//
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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,
|
|
833
|
-
|
|
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
|
-
? '
|
|
903
|
-
: `<b>${agentes.length}</b>
|
|
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
|
-
? '
|
|
912
|
-
: 'Give
|
|
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">
|
|
1058
|
-
<p class="prosa" style="margin-top:8px"
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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">
|
|
1658
|
-
<h1 style="margin-top:6px">
|
|
1659
|
-
<p class="prosa" style="margin-top:8px">A
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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()}
|
|
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
|
|
2056
|
-
|
|
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 =
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "city",
|
|
3
3
|
"displayName": "Agents City",
|
|
4
|
-
"version": "0.2.
|
|
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",
|
package/plugin/channel/bus.js
CHANGED
|
@@ -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.
|
|
20103
|
+
{ name: "agents-city-bus", version: "0.2.1" },
|
|
20104
20104
|
{
|
|
20105
20105
|
capabilities: {
|
|
20106
20106
|
...channelEnabled ? { experimental: { "claude/channel": {} } } : {},
|
package/plugin/channel/bus.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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");
|
package/plugin/scripts/reset.py
CHANGED
|
@@ -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
|
|
184
|
+
description="Reset cities to onboarding; repos stay untouched and data is backed up"
|
|
157
185
|
)
|
|
158
|
-
ap.add_argument("city",
|
|
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
|
-
|
|
163
|
-
if not
|
|
164
|
-
|
|
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
|
-
|
|
174
|
-
print(f"
|
|
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
|
|