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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +12 -3
- package/README.md +10 -2
- package/bin/hall.html +113 -62
- package/bin/navegador.mjs +91 -5
- package/bin/serve.py +87 -0
- package/bin/test-cities.py +45 -0
- package/bin/test-navegador.py +8 -1
- package/bin/test-seat.py +1 -1
- package/bin/test-serve.py +21 -10
- package/city/web/dist-hall/hall.js +605 -32
- package/city/web/src/bienvenida.ts +524 -0
- package/city/web/src/hall.ts +290 -45
- 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/cities.py +34 -0
- 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,69 +11,60 @@
|
|
|
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
|
-
/*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
19
|
+
/* Day is the default: the palette of agentscity.net, so the tool on your
|
|
20
|
+
machine and the public site are one product from the first second. Night
|
|
21
|
+
is one click away and remembered — it is the map's own light, not the
|
|
22
|
+
app's. */
|
|
23
|
+
--suelo:#f3f0e8; --suelo2:#fbfaf6; --suelo3:#ffffff;
|
|
24
|
+
--linea:#e2ddd0; --linea2:#cdc6b6;
|
|
25
|
+
--tinta:#1d2229; --tinta2:#515861; --tinta3:#7b8187;
|
|
26
|
+
--lampara:#7048e8; --carta:#dd5d3e; --verde:#5f8f2b;
|
|
27
|
+
--panel:#fbfaf6; --burbuja:#ffffff; --burbuja-mia:#f4efff;
|
|
28
|
+
--borde-acento:#c9b8ff; --sobre-lampara:#ffffff; --lampara-viva:#5331bd;
|
|
29
|
+
--chip-luz:52% 86%; --chip-tinta:#1d2229;
|
|
30
|
+
--tono-palabra:#6b46f0; --tono-evidencia:#2b7ab8; --tono-verificacion:#1c8d78;
|
|
27
31
|
--hud:"Chakra Petch",system-ui,sans-serif; --texto:"Newsreader",Georgia,serif;
|
|
28
32
|
--mono:"IBM Plex Mono",ui-monospace,monospace;
|
|
29
|
-
--sombra:
|
|
33
|
+
--sombra:0 18px 40px -30px #1d222945;
|
|
30
34
|
|
|
31
|
-
/*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
-
--
|
|
35
|
+
/* Night, declared once: the city's own light, an isometric map at 3am. The
|
|
36
|
+
two rules below only MAP these; a palette written twice drifts twice. */
|
|
37
|
+
--noche-suelo:#131a24; --noche-suelo2:#1b2431; --noche-suelo3:#222d3c;
|
|
38
|
+
--noche-linea:#26313f; --noche-linea2:#38495f;
|
|
39
|
+
--noche-tinta:#e8e2d4; --noche-tinta2:#9aa7b8; --noche-tinta3:#6b7889;
|
|
40
|
+
--noche-lampara:#f5c451; --noche-carta:#e2604f; --noche-verde:#4ec98a;
|
|
41
|
+
--noche-panel:#101720; --noche-burbuja:#171f2a; --noche-burbuja-mia:#27291f;
|
|
42
|
+
--noche-borde-acento:#65552d; --noche-sobre-lampara:#1a1408;
|
|
43
|
+
--noche-lampara-viva:#ffd469;
|
|
44
|
+
--noche-chip-luz:36% 30%; --noche-chip-tinta:#f3eee4;
|
|
45
|
+
--noche-tono-palabra:#aa93ff; --noche-tono-evidencia:#61ace9;
|
|
46
|
+
--noche-tono-verificacion:#50ccb1;
|
|
47
|
+
--noche-sombra:none;
|
|
44
48
|
}
|
|
45
|
-
/*
|
|
46
|
-
|
|
47
|
-
:
|
|
48
|
-
--
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
52
|
-
--
|
|
53
|
-
--borde-acento:var(--
|
|
54
|
-
--lampara-viva:var(--
|
|
55
|
-
--chip-luz:var(--
|
|
56
|
-
--tono-palabra:var(--
|
|
57
|
-
--tono-verificacion:var(--
|
|
58
|
-
--sombra:var(--
|
|
59
|
-
}
|
|
60
|
-
@media(prefers-color-scheme:light){
|
|
61
|
-
:root:not([data-tema="oscuro"]){
|
|
62
|
-
--suelo:var(--dia-suelo); --suelo2:var(--dia-suelo2); --suelo3:var(--dia-suelo3);
|
|
63
|
-
--linea:var(--dia-linea); --linea2:var(--dia-linea2);
|
|
64
|
-
--tinta:var(--dia-tinta); --tinta2:var(--dia-tinta2); --tinta3:var(--dia-tinta3);
|
|
65
|
-
--lampara:var(--dia-lampara); --carta:var(--dia-carta); --verde:var(--dia-verde);
|
|
66
|
-
--panel:var(--dia-panel); --burbuja:var(--dia-burbuja); --burbuja-mia:var(--dia-burbuja-mia);
|
|
67
|
-
--borde-acento:var(--dia-borde-acento); --sobre-lampara:var(--dia-sobre-lampara);
|
|
68
|
-
--lampara-viva:var(--dia-lampara-viva);
|
|
69
|
-
--chip-luz:var(--dia-chip-luz); --chip-tinta:var(--dia-chip-tinta);
|
|
70
|
-
--tono-palabra:var(--dia-tono-palabra); --tono-evidencia:var(--dia-tono-evidencia);
|
|
71
|
-
--tono-verificacion:var(--dia-tono-verificacion);
|
|
72
|
-
--tono-palabra:var(--dia-tono-palabra); --tono-evidencia:var(--dia-tono-evidencia);
|
|
73
|
-
--tono-verificacion:var(--dia-tono-verificacion);
|
|
74
|
-
--sombra:var(--dia-sombra);
|
|
75
|
-
}
|
|
49
|
+
/* Night, mapped from the tokens declared once above. */
|
|
50
|
+
:root[data-tema="oscuro"]{
|
|
51
|
+
--suelo:var(--noche-suelo); --suelo2:var(--noche-suelo2); --suelo3:var(--noche-suelo3);
|
|
52
|
+
--linea:var(--noche-linea); --linea2:var(--noche-linea2);
|
|
53
|
+
--tinta:var(--noche-tinta); --tinta2:var(--noche-tinta2); --tinta3:var(--noche-tinta3);
|
|
54
|
+
--lampara:var(--noche-lampara); --carta:var(--noche-carta); --verde:var(--noche-verde);
|
|
55
|
+
--panel:var(--noche-panel); --burbuja:var(--noche-burbuja);
|
|
56
|
+
--burbuja-mia:var(--noche-burbuja-mia);
|
|
57
|
+
--borde-acento:var(--noche-borde-acento); --sobre-lampara:var(--noche-sobre-lampara);
|
|
58
|
+
--lampara-viva:var(--noche-lampara-viva);
|
|
59
|
+
--chip-luz:var(--noche-chip-luz); --chip-tinta:var(--noche-chip-tinta);
|
|
60
|
+
--tono-palabra:var(--noche-tono-palabra); --tono-evidencia:var(--noche-tono-evidencia);
|
|
61
|
+
--tono-verificacion:var(--noche-tono-verificacion);
|
|
62
|
+
--sombra:var(--noche-sombra);
|
|
76
63
|
}
|
|
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. */
|
|
77
68
|
*{box-sizing:border-box}
|
|
78
69
|
html,body{height:100%}
|
|
79
70
|
body{margin:0;background:var(--suelo);color:var(--tinta);font-family:var(--texto);
|
|
@@ -101,6 +92,13 @@ nav{background:var(--suelo2);border-right:1px solid var(--linea);padding:20px 0
|
|
|
101
92
|
.badge::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--lampara)}
|
|
102
93
|
.marca h2{font-size:19px;margin:9px 0 0}
|
|
103
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)}
|
|
104
102
|
nav ol{list-style:none;margin:0;padding:0 10px;display:flex;flex-direction:column;gap:2px}
|
|
105
103
|
nav li{display:flex;gap:10px;align-items:baseline;padding:9px 10px;border-radius:3px;
|
|
106
104
|
font-family:var(--hud);font-size:13.5px;color:var(--tinta2);cursor:pointer;white-space:nowrap}
|
|
@@ -110,12 +108,19 @@ nav li .n{font-family:var(--mono);font-size:10px;color:var(--tinta3);margin-left
|
|
|
110
108
|
nav .pie{margin-top:auto;padding:14px 18px 0;border-top:1px solid var(--linea);
|
|
111
109
|
display:flex;flex-direction:column;gap:8px}
|
|
112
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)}
|
|
113
117
|
.avisoVersion{margin:0;font:11px/1.45 var(--mono);color:var(--lampara);
|
|
114
118
|
border:1px solid var(--borde-acento);background:var(--burbuja-mia);
|
|
115
119
|
padding:7px 9px;border-radius:4px}
|
|
116
120
|
.avisoVersion[hidden]{display:none}
|
|
117
|
-
.temaBoton{
|
|
118
|
-
color:var(--tinta2);font:11px var(--mono);padding:
|
|
121
|
+
.temaBoton{margin-top:12px;border:1px solid var(--linea2);background:var(--suelo3);
|
|
122
|
+
color:var(--tinta2);font:11px var(--mono);padding:5px 11px;border-radius:999px;
|
|
123
|
+
cursor:pointer;box-shadow:var(--sombra)}
|
|
119
124
|
.temaBoton:hover{color:var(--lampara);border-color:var(--lampara)}
|
|
120
125
|
nav .pie a:hover{color:var(--lampara)}
|
|
121
126
|
#ciudades select{width:100%;background:var(--suelo);border:1px solid var(--linea2);
|
|
@@ -305,6 +310,51 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
|
|
|
305
310
|
color:var(--lampara);font:12px var(--mono);padding:7px 14px;border-radius:5px;
|
|
306
311
|
cursor:pointer}
|
|
307
312
|
.rpgAlta:hover{background:var(--suelo);border-color:var(--lampara)}
|
|
313
|
+
|
|
314
|
+
/* ── the guided first run ────────────────────────────────────────────────── */
|
|
315
|
+
.bv{display:flex;flex-direction:column;gap:26px;max-width:min(760px,100%)}
|
|
316
|
+
.bv .prosa{max-width:100%}
|
|
317
|
+
.bv code.mono{word-break:break-all}
|
|
318
|
+
/* The live rail has nothing to show before a city exists, and it costs half
|
|
319
|
+
the screen exactly when the guide needs it. */
|
|
320
|
+
body.enGuia #livePanel,body.enGuia #liveResize{display:none}
|
|
321
|
+
body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
|
|
322
|
+
.bvPasos{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
|
323
|
+
.bvPunto{display:flex;align-items:center;gap:6px;font:10px var(--hud);letter-spacing:.1em;
|
|
324
|
+
text-transform:uppercase;color:var(--tinta3)}
|
|
325
|
+
.bvPunto i{width:8px;height:8px;border-radius:50%;background:var(--linea2);display:block}
|
|
326
|
+
.bvPunto+.bvPunto{margin-left:10px}
|
|
327
|
+
.bvPunto.aqui{color:var(--lampara)}.bvPunto.aqui i{background:var(--lampara)}
|
|
328
|
+
.bvPunto.hecho i{background:var(--verde)}
|
|
329
|
+
.bvHola{display:grid;grid-template-columns:auto 1fr;gap:26px;align-items:start}
|
|
330
|
+
@media(max-width:820px){.bvHola{grid-template-columns:1fr}}
|
|
331
|
+
.bvArte{display:flex;flex-direction:column;gap:2px;padding-top:6px}
|
|
332
|
+
.bvCasa{width:92px;height:83px;margin-bottom:-26px;filter:drop-shadow(var(--sombra))}
|
|
333
|
+
.bvPaso{display:flex;flex-direction:column;gap:14px}
|
|
334
|
+
.bvPaso h1{font-size:26px;line-height:1.15}
|
|
335
|
+
.bvEtiqueta{font:600 10px var(--hud);letter-spacing:.14em;text-transform:uppercase;
|
|
336
|
+
color:var(--tinta3)}
|
|
337
|
+
.bvRejilla{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px}
|
|
338
|
+
.bvRejilla.bvTres{grid-template-columns:repeat(auto-fit,minmax(210px,1fr))}
|
|
339
|
+
.bvOpcion{text-align:left;display:flex;flex-direction:column;gap:4px;padding:12px 13px;
|
|
340
|
+
border:1px solid var(--linea2);border-radius:7px;background:var(--suelo2);cursor:pointer}
|
|
341
|
+
.bvOpcion b{font-family:var(--hud);font-size:13px;color:var(--tinta)}
|
|
342
|
+
.bvOpcion span{font-size:12px;line-height:1.45;color:var(--tinta3)}
|
|
343
|
+
.bvOpcion:hover{border-color:var(--lampara)}
|
|
344
|
+
.bvOpcion.elegida{border-color:var(--lampara);background:var(--burbuja-mia);
|
|
345
|
+
box-shadow:inset 3px 0 0 var(--lampara)}
|
|
346
|
+
.bvChips{display:flex;flex-wrap:wrap;gap:6px}
|
|
347
|
+
.bvChip{border:1px solid var(--linea2);background:var(--suelo2);color:var(--tinta2);
|
|
348
|
+
font:11px var(--mono);padding:5px 10px;border-radius:999px;cursor:pointer}
|
|
349
|
+
.bvChip:hover{border-color:var(--lampara)}
|
|
350
|
+
.bvChip.elegida{border-color:var(--lampara);background:var(--burbuja-mia);color:var(--tinta)}
|
|
351
|
+
.bvFichas{display:flex;flex-direction:column;gap:6px}
|
|
352
|
+
.bvFicha{display:flex;gap:10px;align-items:baseline;padding:9px 12px;border-radius:6px;
|
|
353
|
+
border:1px solid var(--linea);background:var(--suelo2)}
|
|
354
|
+
.bvFicha b{font-family:var(--hud);font-size:13px}
|
|
355
|
+
.bvFicha span{font:11px var(--mono);color:var(--tinta3)}
|
|
356
|
+
.bvBotones{display:flex;gap:9px;flex-wrap:wrap;margin-top:4px}
|
|
357
|
+
.bvSiguientes{display:flex;gap:9px;flex-wrap:wrap}
|
|
308
358
|
/* The instruction editor: one file, one reader, saved where the agent lives. */
|
|
309
359
|
#editorIns{position:fixed;inset:0;background:rgba(6,10,15,.72);z-index:60;
|
|
310
360
|
display:flex;align-items:center;justify-content:center}
|
|
@@ -442,15 +492,16 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
|
|
|
442
492
|
<span class="badge">town hall</span>
|
|
443
493
|
<h2>Agents City</h2>
|
|
444
494
|
<p>Manage one autonomous city: its seat, repo agents, roads and map. Everything here is a plain file you can also inspect by hand.</p>
|
|
495
|
+
<button type="button" id="temaBoton" class="temaBoton"
|
|
496
|
+
title="Daylight is the default; night is the map's own light">☾ Night</button>
|
|
445
497
|
</div>
|
|
498
|
+
<div id="railCiudad" class="railCiudad"></div>
|
|
446
499
|
<ol id="rail"></ol>
|
|
500
|
+
<ol id="railOtras" class="railOtras"></ol>
|
|
447
501
|
<div id="ciudades" style="padding:0 18px"></div>
|
|
448
502
|
<div class="pie">
|
|
449
503
|
<p id="avisoVersion" class="avisoVersion" hidden></p>
|
|
450
504
|
<a href="#" id="alWizard">Create another city →</a>
|
|
451
|
-
<button type="button" id="temaBoton" class="temaBoton"
|
|
452
|
-
title="Switch between the night city and the daylight palette of agentscity.net">
|
|
453
|
-
☾ Night</button>
|
|
454
505
|
<span class="dato" id="dondeDatos"></span>
|
|
455
506
|
</div>
|
|
456
507
|
</nav>
|
package/bin/navegador.mjs
CHANGED
|
@@ -108,6 +108,7 @@ function comprueba(texto, bien, detalle = '') {
|
|
|
108
108
|
|
|
109
109
|
async function main() {
|
|
110
110
|
const url = process.argv[2];
|
|
111
|
+
const ciudadVacia = process.argv[3] || '';
|
|
111
112
|
if (!url) {
|
|
112
113
|
console.error('usage: navegador.mjs <hall url>');
|
|
113
114
|
process.exit(2);
|
|
@@ -176,8 +177,8 @@ async function main() {
|
|
|
176
177
|
|
|
177
178
|
// The agents view: this is where dead controls shipped once.
|
|
178
179
|
const abierto = await cdp.evalua(`(async () => {
|
|
179
|
-
const ir = [...document.querySelectorAll('nav li')].find(l => /
|
|
180
|
-
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' };
|
|
181
182
|
ir.click();
|
|
182
183
|
for (let i = 0; i < 40; i++) {
|
|
183
184
|
if (document.querySelector('.fichaRPG')) break;
|
|
@@ -194,9 +195,9 @@ async function main() {
|
|
|
194
195
|
test: boton('.rpgTest'),
|
|
195
196
|
};
|
|
196
197
|
})()`);
|
|
197
|
-
comprueba('the
|
|
198
|
+
comprueba('the houses view renders its sheets', (abierto?.fichas ?? 0) > 0, JSON.stringify(abierto));
|
|
198
199
|
for (const [control, etiqueta] of [
|
|
199
|
-
['alta', '
|
|
200
|
+
['alta', 'the build-a-house button'],
|
|
200
201
|
['instrucciones', 'the CLAUDE.md / AGENTS.md editors'],
|
|
201
202
|
['subir', 'the skill zip upload'],
|
|
202
203
|
['monta', 'the + folder mount button'],
|
|
@@ -228,12 +229,97 @@ async function main() {
|
|
|
228
229
|
JSON.stringify(modal),
|
|
229
230
|
);
|
|
230
231
|
|
|
232
|
+
// The guided first run, checked against a city with nobody in it: that is
|
|
233
|
+
// the only state where it appears, and it is the screen that used to say
|
|
234
|
+
// "Not drawn yet" to somebody arriving for the first time.
|
|
235
|
+
if (ciudadVacia) {
|
|
236
|
+
await cdp.manda('Page.navigate', {
|
|
237
|
+
url: url + '&city=' + encodeURIComponent(ciudadVacia),
|
|
238
|
+
});
|
|
239
|
+
for (let i = 0; i < 60; i += 1) {
|
|
240
|
+
const listo = await cdp.evalua('!!document.getElementById("rail") && !!window.PASE');
|
|
241
|
+
if (listo) break;
|
|
242
|
+
await dormir(250);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const guia = await cdp.evalua(`(async () => {
|
|
246
|
+
for (let i = 0; i < 40; i++) {
|
|
247
|
+
if (document.querySelector('.bvPasos')) break;
|
|
248
|
+
await new Promise(r => setTimeout(r, 250));
|
|
249
|
+
}
|
|
250
|
+
const pasos = [...document.querySelectorAll('.bvPunto')].map(p => p.textContent.trim());
|
|
251
|
+
const b = document.querySelector('[data-bv="siguiente"]');
|
|
252
|
+
if (b) b.click();
|
|
253
|
+
await new Promise(r => setTimeout(r, 900));
|
|
254
|
+
return {
|
|
255
|
+
pasos,
|
|
256
|
+
titulo: document.querySelector('.bvPaso h1')?.textContent?.trim() ?? '',
|
|
257
|
+
opciones: document.querySelectorAll('.bvOpcion').length,
|
|
258
|
+
};
|
|
259
|
+
})()`);
|
|
260
|
+
comprueba(
|
|
261
|
+
'a city with no agents opens the guide, not an empty map',
|
|
262
|
+
(guia?.pasos?.length ?? 0) >= 4,
|
|
263
|
+
JSON.stringify(guia),
|
|
264
|
+
);
|
|
265
|
+
comprueba(
|
|
266
|
+
'and its first question offers the work domains to choose from',
|
|
267
|
+
/work happens here/i.test(guia?.titulo ?? '') && (guia?.opciones ?? 0) > 3,
|
|
268
|
+
JSON.stringify(guia),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Cities: a person must be able to start and retire one without a terminal.
|
|
272
|
+
const ciudades = await cdp.evalua(`(async () => {
|
|
273
|
+
const ir = [...document.querySelectorAll('nav li')].find(l => /cities/i.test(l.textContent));
|
|
274
|
+
if (!ir) return { falta: true };
|
|
275
|
+
ir.click();
|
|
276
|
+
await new Promise(r => setTimeout(r, 700));
|
|
277
|
+
return {
|
|
278
|
+
crear: !!document.getElementById('creaCiudad'),
|
|
279
|
+
campo: !!document.getElementById('nuevaCiudad'),
|
|
280
|
+
filas: document.querySelectorAll('.lista .fila').length,
|
|
281
|
+
};
|
|
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
|
+
|
|
311
|
+
comprueba(
|
|
312
|
+
'the Cities section can start a new city',
|
|
313
|
+
ciudades?.crear === true && ciudades?.campo === true,
|
|
314
|
+
JSON.stringify(ciudades),
|
|
315
|
+
);
|
|
316
|
+
|
|
231
317
|
const errores = await cdp.evalua('window.__erroresDePagina ? window.__erroresDePagina.length : 0');
|
|
232
318
|
comprueba('the page raised no uncaught errors while we drove it', errores === 0, String(errores));
|
|
233
319
|
} finally {
|
|
234
320
|
proceso.kill('SIGKILL');
|
|
235
321
|
}
|
|
236
|
-
console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} —
|
|
322
|
+
console.log(`\n ${fallos ? `${fallos} failed` : 'browser ok'} — 16 checks\n`);
|
|
237
323
|
process.exit(fallos ? 1 : 0);
|
|
238
324
|
}
|
|
239
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
|
|
|
@@ -679,6 +700,8 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
679
700
|
"/api/mapa": "p_mapa",
|
|
680
701
|
"/api/sesion": "p_sesion",
|
|
681
702
|
"/api/ciudades": "p_ciudades",
|
|
703
|
+
"/api/ciudad-archiva": "p_archiva_ciudad",
|
|
704
|
+
"/api/ciudad-reinicia": "p_reinicia_ciudad",
|
|
682
705
|
"/api/roads": "p_roads",
|
|
683
706
|
"/api/agente": "p_agente",
|
|
684
707
|
"/api/agentes": "p_agentes",
|
|
@@ -1049,6 +1072,70 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1049
1072
|
{"ok": True, "city": datos, "address": cities.direccion(usuario, datos)}
|
|
1050
1073
|
)
|
|
1051
1074
|
|
|
1075
|
+
def p_archiva_ciudad(self, q, cuerpo):
|
|
1076
|
+
"""Take one city out of use — recoverably, and never the last one.
|
|
1077
|
+
|
|
1078
|
+
Not a delete: `cities.archiva` MOVES the folder into the owner's
|
|
1079
|
+
backups. A city is somebody's cards, deliberations and map, and a
|
|
1080
|
+
product that erases that on one click will erase the wrong one.
|
|
1081
|
+
"""
|
|
1082
|
+
pedida = str(cuerpo.get("city") or "")
|
|
1083
|
+
usuario = seat.quien_soy()
|
|
1084
|
+
datos = cities.resuelve(pedida, usuario) if pedida else ""
|
|
1085
|
+
if not datos:
|
|
1086
|
+
return self.responde({"error": f"no city called {pedida!r}"}, codigo=404)
|
|
1087
|
+
try:
|
|
1088
|
+
copia = cities.archiva(datos, usuario)
|
|
1089
|
+
except (OSError, ValueError) as e:
|
|
1090
|
+
return self.responde({"error": str(e)}, codigo=409)
|
|
1091
|
+
return self.responde(
|
|
1092
|
+
{"ok": True, "backup": copia, "ciudades": lista_con(cities.actual(usuario))}
|
|
1093
|
+
)
|
|
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
|
+
|
|
1052
1139
|
def p_roads(self, q, cuerpo):
|
|
1053
1140
|
origen = self.ciudad(q)
|
|
1054
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-navegador.py
CHANGED
|
@@ -95,13 +95,20 @@ def main():
|
|
|
95
95
|
)
|
|
96
96
|
ciudad_de_prueba(datos)
|
|
97
97
|
|
|
98
|
+
# A SECOND city, deliberately empty: the guide only appears where there is
|
|
99
|
+
# nothing yet, and the first fixture has an agent so the sheets can render.
|
|
100
|
+
try:
|
|
101
|
+
vacia = serve.cities.crea("navtest", "sin-nadie", usar=False)
|
|
102
|
+
except (OSError, ValueError):
|
|
103
|
+
vacia = ""
|
|
104
|
+
|
|
98
105
|
servidor = serve.Servidor(("127.0.0.1", 0), serve.Manejador)
|
|
99
106
|
hilo = threading.Thread(target=servidor.serve_forever, daemon=True)
|
|
100
107
|
hilo.start()
|
|
101
108
|
url = f"http://127.0.0.1:{servidor.server_port}/?PASE={serve.PASE}"
|
|
102
109
|
try:
|
|
103
110
|
salida = subprocess.run(
|
|
104
|
-
["node", os.path.join(AQUI, "navegador.mjs"), url],
|
|
111
|
+
["node", os.path.join(AQUI, "navegador.mjs"), url, vacia],
|
|
105
112
|
capture_output=True,
|
|
106
113
|
text=True,
|
|
107
114
|
timeout=300,
|
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.1
|
|
521
|
+
and "0.2.1" in salida.getvalue(),
|
|
522
522
|
f"calls={llamadas!r} output={salida.getvalue()!r}",
|
|
523
523
|
)
|
|
524
524
|
|