agents-city 0.5.10 → 0.5.12
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 +15 -13
- package/README.md +15 -13
- package/benchmarks/committee/run.py +2 -2
- package/benchmarks/latency/live.py +8 -4
- package/benchmarks/stress/run.py +10 -5
- package/bin/agents-city.js +181 -56
- package/bin/hall +2 -1
- package/bin/serve.py +29 -28
- package/bin/setup.py +9 -7
- package/bin/test +6 -3
- package/bin/test-actualiza.py +16 -6
- package/bin/test-alcance.py +9 -7
- package/bin/test-arnes.py +7 -5
- package/bin/test-atajos.py +73 -9
- package/bin/test-broker.py +4 -4
- package/bin/test-cage.py +36 -8
- package/bin/test-card.py +11 -10
- package/bin/test-cities.py +38 -21
- package/bin/test-contracts.py +351 -150
- package/bin/test-doctor.py +5 -5
- package/bin/test-domains.py +4 -4
- package/bin/test-e2e.py +2 -2
- package/bin/test-exit.py +1 -1
- package/bin/test-hooks.py +227 -0
- package/bin/test-launch.py +110 -6
- package/bin/test-live-feed.py +3 -2
- package/bin/test-multiplexor.py +262 -0
- package/bin/test-navegador.py +59 -1
- package/bin/test-seat.py +257 -239
- package/bin/test-security.py +18 -10
- package/bin/test-serve.py +41 -5
- package/bin/testlib.py +11 -0
- package/bus/scripts/test-hub.ts +34 -16
- package/city/oven/collect.py +11 -4
- package/city/scripts/history.py +1 -1
- package/city/scripts/seed.py +2 -2
- package/city/web/dist-hall/hall.js +5 -5
- package/city/web/sella.py +4 -1
- package/city/web/src/hall.ts +6 -8
- package/docs/security.md +1 -1
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter.js +64 -34
- package/plugin/channel/multiplexor.ts +93 -0
- package/plugin/channel/runtime/multiplexores.json +206 -0
- package/plugin/channel/terminal-delivery.ts +14 -33
- package/plugin/hooks/hook.py +557 -0
- package/plugin/hooks/hooks.json +12 -12
- package/plugin/scripts/apaga.py +8 -8
- package/plugin/scripts/atajos.py +79 -0
- package/plugin/scripts/cage.py +1 -1
- package/plugin/scripts/city-env.sh +12 -86
- package/plugin/scripts/city-runtime.sh +4 -86
- package/plugin/scripts/city-session.sh +8 -818
- package/plugin/scripts/city_env.py +168 -6
- package/plugin/scripts/doctor.py +5 -3
- package/plugin/scripts/launch.py +238 -34
- package/plugin/scripts/multiplexor.py +354 -0
- package/plugin/scripts/mux_herdr.py +275 -0
- package/plugin/scripts/report.py +1 -1
- package/plugin/scripts/roles.py +1 -1
- package/plugin/scripts/runtime_processes.py +15 -0
- package/plugin/scripts/runtimes.py +120 -0
- package/plugin/scripts/seat.py +23 -15
- package/plugin/scripts/sesion.py +870 -0
- package/plugin/scripts/tokens.py +3 -3
- package/plugin/scripts/ui.py +14 -3
- package/plugin/scripts/workspace.py +122 -16
- package/plugin/hooks/activity.sh +0 -6
- package/plugin/hooks/ask-the-house.sh +0 -30
- package/plugin/hooks/digging.sh +0 -68
- package/plugin/hooks/growth.sh +0 -42
- package/plugin/hooks/notice-on-pr.sh +0 -35
- package/plugin/hooks/notice-on-stop.sh +0 -86
- package/plugin/hooks/notice-pending.sh +0 -58
- package/plugin/hooks/solo-en-ciudad.sh +0 -23
- package/plugin/hooks/tokens.sh +0 -47
- package/plugin/hooks/who-does-this-concern.sh +0 -22
package/README.es.md
CHANGED
|
@@ -165,10 +165,9 @@ agents-city --version
|
|
|
165
165
|
| Node.js 22.13 o posterior | paquete npm, bus WebSocket, recepción local y frontends |
|
|
166
166
|
| npm | instalación y empaquetado |
|
|
167
167
|
| Python 3 | Hall, onboarding, ciudades, mapas y utilidades |
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
| WSL | necesario en Windows porque Windows no incluye bash/tmux nativos |
|
|
168
|
+
| un servidor de ventanas | una ventana por asiento/agente: **tmux** en macOS y Linux, **herdr** en Windows; `seat` intenta instalarlo si falta |
|
|
169
|
+
| macOS, Linux o Windows | los tres de forma nativa. Cuatro comandos siguen necesitando shell — `city`, `demo`, `benchmark`, `test` — y lo dicen por su nombre en Windows |
|
|
170
|
+
| bash | sólo para esos cuatro |
|
|
172
171
|
|
|
173
172
|
Cada motor necesita además su propio CLI instalado y autenticado. Agents City no
|
|
174
173
|
incluye ni suplanta las cuentas de Claude, Codex, OpenCode o Kimi.
|
|
@@ -340,15 +339,18 @@ agents-city shortcut --remove # quitarlo otra vez
|
|
|
340
339
|
agents-city shortcut --to ~/bin # en otro sitio que no sea el escritorio
|
|
341
340
|
```
|
|
342
341
|
|
|
343
|
-
**En Windows**
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
adivinarlo a partir del nombre de usuario
|
|
347
|
-
a un perfil de dominio no está bajo `C:\Users\<nombre>\Desktop
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
342
|
+
**En Windows** es un `.lnk` de verdad, construido con la propia PowerShell de
|
|
343
|
+
Windows para que pueda llevar un `.ico`, y ejecuta el mismo comando de la puerta
|
|
344
|
+
principal que cualquier otro escritorio. El escritorio se le pregunta a Windows
|
|
345
|
+
en vez de adivinarlo a partir del nombre de usuario: uno redirigido a OneDrive o
|
|
346
|
+
a un perfil de dominio no está bajo `C:\Users\<nombre>\Desktop`. Si no hay
|
|
347
|
+
PowerShell se escribe un `.cmd` que también se abre con doble clic: la misma
|
|
348
|
+
puerta, con icono genérico.
|
|
349
|
+
|
|
350
|
+
**Dentro de WSL** el acceso directo sigue cruzando de vuelta: un `~/Desktop` de
|
|
351
|
+
ahí es el escritorio del home de Linux, que nadie mira, así que el escritorio de
|
|
352
|
+
Windows recibe un `.lnk` que arranca `wsl.exe` con el mismo comando en una shell
|
|
353
|
+
de login.
|
|
352
354
|
|
|
353
355
|
### Qué se crea
|
|
354
356
|
|
package/README.md
CHANGED
|
@@ -163,10 +163,9 @@ agents-city --version
|
|
|
163
163
|
| Node.js 22.13 or later | npm package, WebSocket bus, local reception, and frontends |
|
|
164
164
|
| npm | installation and packaging |
|
|
165
165
|
| Python 3 | Hall, onboarding, cities, maps, and utilities |
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
| WSL | required on Windows because native Windows has no bash/tmux |
|
|
166
|
+
| a window server | one window per seat/agent: **tmux** on macOS and Linux, **herdr** on Windows; `seat` tries to install it when missing |
|
|
167
|
+
| macOS, Linux or Windows | all three natively. Four commands still need a shell — `city`, `demo`, `benchmark`, `test` — and say so by name on Windows |
|
|
168
|
+
| bash | only for those four |
|
|
170
169
|
| bubblewrap | optional, Linux only: it IS the cage there. Without it agents run uncaged — see [the cage](#the-cage-the-broker-and-the-audit-chain) |
|
|
171
170
|
|
|
172
171
|
Each runtime also needs its own installed and authenticated CLI. Agents City
|
|
@@ -336,14 +335,16 @@ agents-city shortcut --remove # take it off again
|
|
|
336
335
|
agents-city shortcut --to ~/bin # somewhere other than the desktop
|
|
337
336
|
```
|
|
338
337
|
|
|
339
|
-
**On Windows**
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
338
|
+
**On Windows** it is a real `.lnk`, built through Windows' own PowerShell so it
|
|
339
|
+
can carry an `.ico`, running the same front-door command as every other desktop.
|
|
340
|
+
The desktop is asked of Windows itself rather than guessed from a username — one
|
|
341
|
+
redirected to OneDrive or a domain profile is not under `C:\Users\<name>\Desktop`.
|
|
342
|
+
Where PowerShell is unavailable a double-clickable `.cmd` is written instead:
|
|
343
|
+
same door, plain icon.
|
|
344
|
+
|
|
345
|
+
**Inside WSL** the shortcut still crosses back: a `~/Desktop` there is the Linux
|
|
346
|
+
home's desktop, which nobody looks at, so the Windows desktop gets a `.lnk` that
|
|
347
|
+
launches `wsl.exe` running the same command in a login shell.
|
|
347
348
|
|
|
348
349
|
### What gets created
|
|
349
350
|
|
|
@@ -989,7 +990,8 @@ script pretending:
|
|
|
989
990
|
|---|---|---|
|
|
990
991
|
| macOS | `.app` bundle running the city in Terminal | `.icns`, built with the system's `iconutil` |
|
|
991
992
|
| Linux | `.desktop` entry, marked trusted where `gio` exists | `.png` under `XDG_DATA_HOME` |
|
|
992
|
-
| Windows
|
|
993
|
+
| Windows | `.lnk` on the desktop, running the front-door command | `.ico`, built by hand; a `.cmd` without PowerShell |
|
|
994
|
+
| Windows (inside WSL) | `.lnk` on the **Windows** desktop, launching `wsl.exe` | `.ico`, when PowerShell interop is reachable |
|
|
993
995
|
|
|
994
996
|
All of them run the same line you would type, so the shortcut is a labelled
|
|
995
997
|
button on the front door rather than a second way in. The icon is generated
|
|
@@ -34,12 +34,12 @@ def table(rows):
|
|
|
34
34
|
widths = [max(len(label), *(len(str(row[key])) for row in rows))
|
|
35
35
|
for label, key in columns]
|
|
36
36
|
lines = [
|
|
37
|
-
' '.join(label.ljust(width) for (label, _), width in zip(columns, widths)),
|
|
37
|
+
' '.join(label.ljust(width) for (label, _), width in zip(columns, widths, strict=False)),
|
|
38
38
|
' '.join('-' * width for width in widths),
|
|
39
39
|
]
|
|
40
40
|
for row in rows:
|
|
41
41
|
lines.append(' '.join(str(row[key]).ljust(width)
|
|
42
|
-
for (_, key), width in zip(columns, widths)))
|
|
42
|
+
for (_, key), width in zip(columns, widths, strict=False)))
|
|
43
43
|
return '\n'.join(lines)
|
|
44
44
|
|
|
45
45
|
|
|
@@ -42,7 +42,8 @@ def parse_args():
|
|
|
42
42
|
parser.add_argument('--timeout', type=int, default=180,
|
|
43
43
|
help='seconds to wait for model positions (default: 180)')
|
|
44
44
|
parser.add_argument('--json', action='store_true', help='print only the JSON report')
|
|
45
|
-
parser.add_argument('--no-save', action='store_true',
|
|
45
|
+
parser.add_argument('--no-save', action='store_true',
|
|
46
|
+
help='do not append the local baseline ledger')
|
|
46
47
|
parser.add_argument('--keep', action='store_true', help='keep the temporary city and logs')
|
|
47
48
|
return parser.parse_args()
|
|
48
49
|
|
|
@@ -81,7 +82,7 @@ def main():
|
|
|
81
82
|
os.symlink(os.path.join(ROOT, 'bin', 'agents-city.js'), os.path.join(tools, 'agents-city'))
|
|
82
83
|
marker = f'CITY_NATIVE_{int(time.time())}_{os.getpid()}'
|
|
83
84
|
actors = {runtime: f'{runtime}-agent' for runtime in selected}
|
|
84
|
-
for
|
|
85
|
+
for _runtime, actor in actors.items():
|
|
85
86
|
repo = os.path.join(repos, actor)
|
|
86
87
|
os.makedirs(repo)
|
|
87
88
|
with open(os.path.join(repo, 'README.md'), 'w', encoding='utf-8') as stream:
|
|
@@ -227,7 +228,9 @@ def wait_ready(runtimes, actors, processes, streams, app, timeout):
|
|
|
227
228
|
for runtime in runtimes:
|
|
228
229
|
process = processes[runtime]
|
|
229
230
|
if process.poll() is not None:
|
|
230
|
-
raise RuntimeError(
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
f'{runtime} exited during startup: '
|
|
233
|
+
f'{tail(read_stream(streams[runtime]), 12)}')
|
|
231
234
|
ready = ready and os.path.exists(
|
|
232
235
|
os.path.join(status_dir, f'{actors[runtime]}.json'))
|
|
233
236
|
if ready:
|
|
@@ -256,7 +259,8 @@ def report_for(results, unavailable, thread, duration):
|
|
|
256
259
|
return {
|
|
257
260
|
'schema': 'agents-city/native-e2e-latency@1',
|
|
258
261
|
'runAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
259
|
-
'scope': 'live task completion through bus, native provider protocol
|
|
262
|
+
'scope': ('live task completion through bus, native provider protocol '
|
|
263
|
+
'and committee position'),
|
|
260
264
|
'thread': thread,
|
|
261
265
|
'durationMs': duration,
|
|
262
266
|
'results': results + unavailable,
|
package/benchmarks/stress/run.py
CHANGED
|
@@ -140,7 +140,8 @@ def run(args):
|
|
|
140
140
|
raise RuntimeError('a Codex gateway exited during startup')
|
|
141
141
|
startup_hubs = hub_pids(city)
|
|
142
142
|
if len(startup_hubs) != 1:
|
|
143
|
-
raise RuntimeError(
|
|
143
|
+
raise RuntimeError(
|
|
144
|
+
f'concurrent startup created {len(startup_hubs)} hubs: {startup_hubs}')
|
|
144
145
|
report['startupMs'] = round((time.monotonic() - started) * 1000)
|
|
145
146
|
report['startupHubProcesses'] = len(startup_hubs)
|
|
146
147
|
|
|
@@ -156,8 +157,11 @@ def run(args):
|
|
|
156
157
|
# recovery, not a stale endpoint. What must disappear is the
|
|
157
158
|
# stopped PID's ownership; the endpoint may validly already name
|
|
158
159
|
# the one new hub.
|
|
160
|
+
# Bound as a default: the lambda is called in this same
|
|
161
|
+
# iteration, so late binding does not bite — but correct by
|
|
162
|
+
# accident is not correct.
|
|
159
163
|
if not wait_for(
|
|
160
|
-
lambda: endpoint_released_or_replaced(endpoint_path,
|
|
164
|
+
lambda pid=stopped_pid: endpoint_released_or_replaced(endpoint_path, pid), 4
|
|
161
165
|
):
|
|
162
166
|
raise RuntimeError('the stopped hub still owns the endpoint')
|
|
163
167
|
round_started = time.monotonic()
|
|
@@ -178,8 +182,8 @@ def run(args):
|
|
|
178
182
|
f'round {round_number} spectator feed did not represent all '
|
|
179
183
|
f'{args.agents} selected actors exactly once')
|
|
180
184
|
if not wait_for(
|
|
181
|
-
lambda: round_complete(
|
|
182
|
-
|
|
185
|
+
lambda t=thread: round_complete(t, claude_capture, codex_capture,
|
|
186
|
+
metrics_path, args.agents // 2, args.agents),
|
|
183
187
|
args.timeout,
|
|
184
188
|
):
|
|
185
189
|
raise RuntimeError(
|
|
@@ -197,7 +201,8 @@ def run(args):
|
|
|
197
201
|
f'Claude={claude_count}, Codex={codex_count}')
|
|
198
202
|
if len(metrics) != args.agents:
|
|
199
203
|
raise RuntimeError(
|
|
200
|
-
f'round {round_number} recorded {len(metrics)} acceptances,
|
|
204
|
+
f'round {round_number} recorded {len(metrics)} acceptances, '
|
|
205
|
+
f'expected {args.agents}')
|
|
201
206
|
latencies = sorted(int(row.get('totalToNativeAcceptMs', 0)) for row in metrics)
|
|
202
207
|
round_report = {
|
|
203
208
|
'round': round_number,
|
package/bin/agents-city.js
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The same trick Claude Code uses — npm as the installer, the payload being
|
|
5
5
|
* whatever the product actually is. Here the payload is the repo itself: the
|
|
6
|
-
* Python that writes your files
|
|
7
|
-
* prebuilt map. Node is only the dispatcher, and it is guaranteed
|
|
8
|
-
* npm just ran.
|
|
6
|
+
* Python that writes your files and builds your city, the Node that carries the
|
|
7
|
+
* bus, and the prebuilt map. Node is only the dispatcher, and it is guaranteed
|
|
8
|
+
* present because npm just ran.
|
|
9
|
+
*
|
|
10
|
+
* Every command used to go through a bash door, and fifteen of them were one
|
|
11
|
+
* `exec python3 x.py "$@"` line — a spelling of the interpreter, in the one
|
|
12
|
+
* language Windows does not have. They are named by their implementation now,
|
|
13
|
+
* and this file picks the interpreter, so what is left needing a shell is five
|
|
14
|
+
* commands rather than all of them.
|
|
9
15
|
*
|
|
10
16
|
* Deliberately not a port to JS: the shared modules (card, parcels, units, roles)
|
|
11
17
|
* are the product's single source of truth and every door reads them — the
|
|
@@ -19,35 +25,114 @@ const fs = require('fs');
|
|
|
19
25
|
|
|
20
26
|
const RAIZ = path.dirname(__dirname);
|
|
21
27
|
|
|
28
|
+
/* Every command, named by what it IS rather than by the door it used to have.
|
|
29
|
+
*
|
|
30
|
+
* A door that was one `exec python3 x.py "$@"` line is not a door: it is a
|
|
31
|
+
* spelling of the interpreter, and spelling it in bash is what kept this
|
|
32
|
+
* product off Windows for the fifteen commands that never needed a shell at
|
|
33
|
+
* all. So each entry says its kind, and this file picks the interpreter:
|
|
34
|
+
*
|
|
35
|
+
* py a Python module — everywhere
|
|
36
|
+
* node a Node entry point — everywhere; `env` first resolves the city's
|
|
37
|
+
* settings, which the bash door used to do by sourcing city-env.sh
|
|
38
|
+
* sh a door with shell logic of its own, and therefore POSIX for now
|
|
39
|
+
*/
|
|
22
40
|
const ORDENES = {
|
|
23
|
-
hall: {
|
|
24
|
-
seat: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
hall: { py: 'bin/hall', di: 'the town hall: manage your city from the browser' },
|
|
42
|
+
seat: {
|
|
43
|
+
py: 'plugin/scripts/seat.py',
|
|
44
|
+
di: 'your chair: domain, seat role, repo-agent roles, goal, tmux',
|
|
45
|
+
},
|
|
46
|
+
cities: { py: 'plugin/scripts/cities.py', di: 'list, create or select your cities' },
|
|
47
|
+
agents: {
|
|
48
|
+
py: 'plugin/scripts/workspace.py',
|
|
49
|
+
di: 'list agents and manage their workspace mounts',
|
|
50
|
+
},
|
|
51
|
+
road: { py: 'plugin/scripts/roads.py', di: 'connect two cities explicitly' },
|
|
52
|
+
connect: {
|
|
53
|
+
node: 'plugin/channel/managed-connect-cli.js',
|
|
54
|
+
di: 'pair this computer and open encrypted managed Roads',
|
|
55
|
+
},
|
|
56
|
+
bus: {
|
|
57
|
+
node: 'plugin/channel/client.js',
|
|
58
|
+
antes: ['bus'],
|
|
59
|
+
entorno: true,
|
|
60
|
+
di: 'send information over those roads (seat only)',
|
|
61
|
+
},
|
|
62
|
+
committee: {
|
|
63
|
+
node: 'plugin/channel/client.js',
|
|
64
|
+
antes: ['committee'],
|
|
65
|
+
entorno: true,
|
|
66
|
+
di: 'chair-mediated work with repo agents',
|
|
67
|
+
},
|
|
68
|
+
benchmark: {
|
|
69
|
+
sh: 'bin/benchmark',
|
|
70
|
+
di: 'offline stress/governance or opt-in live runtime latency',
|
|
71
|
+
},
|
|
72
|
+
logs: {
|
|
73
|
+
py: 'plugin/scripts/logs.py',
|
|
74
|
+
di: 'read or follow visible activity and operational diagnostics',
|
|
75
|
+
},
|
|
76
|
+
reset: { py: 'plugin/scripts/reset.py', di: 'reset one city to onboarding, recoverably' },
|
|
34
77
|
uninstall: {
|
|
35
|
-
|
|
78
|
+
py: 'plugin/scripts/desinstala.py',
|
|
36
79
|
di: 'remove everything this wrote on this machine (previews first)',
|
|
37
80
|
},
|
|
38
|
-
skills: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
skills: {
|
|
82
|
+
py: 'plugin/scripts/capabilities.py',
|
|
83
|
+
di: 'recognise the skills installed in each repo',
|
|
84
|
+
},
|
|
85
|
+
city: { sh: 'bin/city', di: 'draw your city' },
|
|
86
|
+
shortcut: {
|
|
87
|
+
py: 'plugin/scripts/atajos.py',
|
|
88
|
+
di: 'put a city on your desktop: icon, name, double-click',
|
|
89
|
+
},
|
|
90
|
+
demo: { sh: 'bin/demo', di: 'Aurora Games — see it working, no account' },
|
|
91
|
+
setup: { py: 'bin/setup.py', di: 'create or open a personal city' },
|
|
92
|
+
report: { py: 'bin/report.py', di: 'report growth from your folders' },
|
|
93
|
+
tokens: { py: 'bin/tokens.py', di: 'report token spend' },
|
|
94
|
+
exit: {
|
|
95
|
+
py: 'plugin/scripts/apaga.py',
|
|
96
|
+
di: 'close the day — every session and agent, or one city',
|
|
97
|
+
},
|
|
98
|
+
doctor: {
|
|
99
|
+
py: 'plugin/scripts/doctor.py',
|
|
100
|
+
di: 'check this machine: tools, runtimes, cage, city, version',
|
|
101
|
+
},
|
|
102
|
+
update: {
|
|
103
|
+
py: 'plugin/scripts/actualiza.py',
|
|
104
|
+
di: 'install the newest published version (--check just asks)',
|
|
105
|
+
},
|
|
106
|
+
test: { sh: 'bin/test', di: 'every check in the repo' },
|
|
49
107
|
};
|
|
50
108
|
|
|
109
|
+
/* Which Python. `python3` is the POSIX spelling and it is NOT what a Windows
|
|
110
|
+
* install from python.org gives you — that is `python` and the `py` launcher,
|
|
111
|
+
* and the Microsoft Store's `python3.exe` is a stub that opens the Store when
|
|
112
|
+
* nothing is installed. So: ask, in order, and take the first that answers with
|
|
113
|
+
* a version. Resolved once per run.
|
|
114
|
+
*/
|
|
115
|
+
let elInterprete;
|
|
116
|
+
function interprete() {
|
|
117
|
+
if (elInterprete !== undefined) return elInterprete;
|
|
118
|
+
const candidatos =
|
|
119
|
+
process.platform === 'win32'
|
|
120
|
+
? [['python'], ['py', '-3'], ['python3']]
|
|
121
|
+
: [['python3'], ['python']];
|
|
122
|
+
for (const c of candidatos) {
|
|
123
|
+
const r = spawnSync(c[0], [...c.slice(1), '-c', 'import sys; print(sys.version_info[0])'], {
|
|
124
|
+
encoding: 'utf8',
|
|
125
|
+
});
|
|
126
|
+
if (r.status === 0 && String(r.stdout).trim() === '3') {
|
|
127
|
+
elInterprete = c;
|
|
128
|
+
return elInterprete;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
elInterprete = null;
|
|
132
|
+
return elInterprete;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
51
136
|
function ayuda() {
|
|
52
137
|
const v = require(path.join(RAIZ, 'package.json')).version;
|
|
53
138
|
console.log(`
|
|
@@ -60,17 +145,16 @@ function ayuda() {
|
|
|
60
145
|
for (const [n, o] of Object.entries(ORDENES)) {
|
|
61
146
|
console.log(` ${n.padEnd(11)}${o.di}`);
|
|
62
147
|
}
|
|
148
|
+
const conShell = Object.entries(ORDENES).filter(([, o]) => o.sh).map(([n]) => n);
|
|
63
149
|
console.log(`
|
|
64
|
-
Needs
|
|
65
|
-
|
|
150
|
+
Needs Python 3, and a window server for the ones that open a city (the seat
|
|
151
|
+
installs it itself).
|
|
66
152
|
`);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{ shell: process.platform !== 'win32', stdio: 'ignore' });
|
|
73
|
-
return r.status === 0;
|
|
153
|
+
if (process.platform === 'win32') {
|
|
154
|
+
console.log(` On Windows these still need WSL, because they are shell programs:
|
|
155
|
+
${conShell.join(', ')}
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
74
158
|
}
|
|
75
159
|
|
|
76
160
|
function main() {
|
|
@@ -83,42 +167,83 @@ function main() {
|
|
|
83
167
|
return 0;
|
|
84
168
|
}
|
|
85
169
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.error(' wsl -- npm install -g agents-city\n');
|
|
90
|
-
return 1;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const orden = ORDENES[primera];
|
|
94
|
-
const resto = orden ? args.slice(1) : args;
|
|
95
|
-
const entrada = orden ? orden.que[0] : ORDENES.hall.que[0];
|
|
96
|
-
if (!orden && args.length && primera.startsWith('-') === false) {
|
|
170
|
+
const orden = ORDENES[primera] || ORDENES.hall;
|
|
171
|
+
const resto = ORDENES[primera] ? args.slice(1) : args;
|
|
172
|
+
if (!ORDENES[primera] && args.length && primera.startsWith('-') === false) {
|
|
97
173
|
console.error(`\n No such command: ${primera}\n`);
|
|
98
174
|
ayuda();
|
|
99
175
|
return 1;
|
|
100
176
|
}
|
|
101
177
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
console.error(
|
|
178
|
+
// A door with shell logic of its own. Named, with what it is, rather than a
|
|
179
|
+
// blanket "this product does not run here" — fifteen of these commands do.
|
|
180
|
+
if (orden.sh && process.platform === 'win32') {
|
|
181
|
+
console.error(`\n \`agents-city ${primera}\` is still a shell program, and Windows has`);
|
|
182
|
+
console.error(' no shell to run it in. Everything else on this install works;');
|
|
183
|
+
console.error(' for this one, run it inside WSL:\n');
|
|
184
|
+
console.error(` wsl -- agents-city ${primera}\n`);
|
|
106
185
|
return 1;
|
|
107
186
|
}
|
|
108
187
|
|
|
109
|
-
const
|
|
188
|
+
const objetivo = orden.sh || orden.py || orden.node;
|
|
189
|
+
const guion = path.join(RAIZ, objetivo);
|
|
110
190
|
if (!fs.existsSync(guion)) {
|
|
111
|
-
console.error(`\n ${
|
|
191
|
+
console.error(`\n ${objetivo} is missing from this install — reinstall:`);
|
|
112
192
|
console.error(' npm install -g agents-city\n');
|
|
113
193
|
return 1;
|
|
114
194
|
}
|
|
195
|
+
|
|
196
|
+
let programa = guion;
|
|
197
|
+
let antes = [];
|
|
198
|
+
if (orden.py) {
|
|
199
|
+
const py = interprete();
|
|
200
|
+
if (!py) {
|
|
201
|
+
console.error('\n agents-city needs Python 3 on PATH — the files it manages are');
|
|
202
|
+
console.error(' written by Python, the same modules every door shares.');
|
|
203
|
+
console.error(' macOS: xcode-select --install · Debian: apt install python3');
|
|
204
|
+
console.error(' Windows: winget install Python.Python.3.12\n');
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
programa = py[0];
|
|
208
|
+
antes = [...py.slice(1), guion];
|
|
209
|
+
} else if (orden.node) {
|
|
210
|
+
programa = process.execPath;
|
|
211
|
+
antes = [guion, ...(orden.antes || [])];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The settings the bash doors used to establish by sourcing city-env.sh.
|
|
215
|
+
// Asked of the one resolver, in the one shape a non-shell can read.
|
|
216
|
+
// PYTHONUTF8 makes `open()` and stdout default to UTF-8 whatever code page
|
|
217
|
+
// the console is on. Without it, on Windows, a card with an em dash is
|
|
218
|
+
// written in cp1252 and read back as UTF-8 by the next door along.
|
|
219
|
+
let entorno = {
|
|
220
|
+
...process.env, PYTHONUNBUFFERED: '1', PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8',
|
|
221
|
+
};
|
|
222
|
+
if (orden.entorno) {
|
|
223
|
+
const py = interprete();
|
|
224
|
+
if (py) {
|
|
225
|
+
const r = spawnSync(py[0], [...py.slice(1), path.join(RAIZ, 'plugin/scripts/city_env.py'),
|
|
226
|
+
'--json'], { encoding: 'utf8' });
|
|
227
|
+
if (r.status === 0) {
|
|
228
|
+
try {
|
|
229
|
+
entorno = { ...entorno, ...JSON.parse(r.stdout) };
|
|
230
|
+
} catch {
|
|
231
|
+
/* a resolver that cannot answer leaves the environment as it found it */
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
115
237
|
// Unbuffered, so the URL a server prints reaches a pipe as well as a terminal
|
|
116
238
|
// — a CI log or a `| head` otherwise swallows it and the run looks silent.
|
|
117
|
-
const r = spawnSync(
|
|
118
|
-
stdio: 'inherit', cwd: RAIZ,
|
|
119
|
-
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
239
|
+
const r = spawnSync(programa, [...antes, ...resto], {
|
|
240
|
+
stdio: 'inherit', cwd: RAIZ, env: entorno,
|
|
120
241
|
});
|
|
121
242
|
return r.status === null ? 1 : r.status;
|
|
122
243
|
}
|
|
123
244
|
|
|
124
|
-
|
|
245
|
+
// Only when this IS the command. Exported so the checks can read the real map
|
|
246
|
+
// rather than a regex over this file — what each command runs, and which ones
|
|
247
|
+
// still need a shell, is exactly the thing that must not drift unnoticed.
|
|
248
|
+
if (require.main === module) process.exit(main());
|
|
249
|
+
module.exports = { ORDENES };
|
package/bin/hall
CHANGED
|
@@ -28,6 +28,7 @@ sys.path.insert(0, AQUI)
|
|
|
28
28
|
sys.path.insert(0, os.path.join(os.path.dirname(AQUI), 'plugin', 'scripts'))
|
|
29
29
|
import serve # noqa: E402
|
|
30
30
|
import cities # noqa: E402
|
|
31
|
+
import runtime_processes # noqa: E402 how a child is cut loose from a terminal
|
|
31
32
|
|
|
32
33
|
def asegura_build():
|
|
33
34
|
"""hall.js, built if missing or older than its source. Same npm the map already
|
|
@@ -96,7 +97,7 @@ def despega(datos, abrir):
|
|
|
96
97
|
|
|
97
98
|
subprocess.Popen(
|
|
98
99
|
[sys.executable, os.path.join(AQUI, 'serve.py'), datos],
|
|
99
|
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
100
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **runtime_processes.DESPEGADO,
|
|
100
101
|
)
|
|
101
102
|
limite = time.monotonic() + 25
|
|
102
103
|
while time.monotonic() < limite:
|
package/bin/serve.py
CHANGED
|
@@ -45,6 +45,7 @@ import parcels # noqa: E402
|
|
|
45
45
|
import units # noqa: E402
|
|
46
46
|
import busca # noqa: E402 the disk scanner, and the one list of where work lives
|
|
47
47
|
import cities # noqa: E402
|
|
48
|
+
import multiplexor # noqa: E402
|
|
48
49
|
import diario # noqa: E402 what happened, written down
|
|
49
50
|
import demos # noqa: E402 the recorded demos the Hall plays back
|
|
50
51
|
import roads # noqa: E402
|
|
@@ -277,7 +278,9 @@ def refresca_roster(ficha, datos):
|
|
|
277
278
|
agentes = workspace.agentes(texto, datos)
|
|
278
279
|
except (OSError, ValueError):
|
|
279
280
|
return
|
|
280
|
-
card.cambia_agentes(
|
|
281
|
+
card.cambia_agentes(
|
|
282
|
+
ficha, [workspace.como_ficha(a, workspace.ajustes_de(texto, a.slug)) for a in agentes]
|
|
283
|
+
)
|
|
281
284
|
|
|
282
285
|
|
|
283
286
|
def _crecimiento_cacheado(a, datos, vida=90):
|
|
@@ -402,26 +405,13 @@ def plugin_de_verdad():
|
|
|
402
405
|
|
|
403
406
|
|
|
404
407
|
def ventanas_vivas(owner, datos):
|
|
405
|
-
"""The window names alive in this city's
|
|
408
|
+
"""The window names alive in this city's session, or nothing.
|
|
406
409
|
|
|
407
|
-
The
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
The exact-match form matters: tmux's bare `-t home` also matches a session
|
|
411
|
+
called `home-2`, and the green dot must never be lit by a different city's
|
|
412
|
+
windows. Which form that is belongs to the window server, not here."""
|
|
410
413
|
try:
|
|
411
|
-
|
|
412
|
-
[
|
|
413
|
-
"tmux",
|
|
414
|
-
"list-windows",
|
|
415
|
-
"-t",
|
|
416
|
-
"=" + cities.sesion(owner, datos),
|
|
417
|
-
"-F",
|
|
418
|
-
"#{window_name}",
|
|
419
|
-
],
|
|
420
|
-
capture_output=True,
|
|
421
|
-
text=True,
|
|
422
|
-
timeout=3,
|
|
423
|
-
)
|
|
424
|
-
return set(salida.stdout.split()) if salida.returncode == 0 else set()
|
|
414
|
+
return set(multiplexor.ventanas(cities.sesion(owner, datos)))
|
|
425
415
|
except (OSError, subprocess.SubprocessError):
|
|
426
416
|
return set()
|
|
427
417
|
|
|
@@ -524,7 +514,7 @@ def lee_clave(fichero, clave, defecto=""):
|
|
|
524
514
|
try:
|
|
525
515
|
import re as _re
|
|
526
516
|
|
|
527
|
-
m = _re.search(rf"^{clave}:[ \t]*(.*)$", open(fichero).read(), _re.M)
|
|
517
|
+
m = _re.search(rf"^{clave}:[ \t]*(.*)$", open(fichero, encoding='utf-8').read(), _re.M)
|
|
528
518
|
return (m.group(1).strip() if m else defecto) or defecto
|
|
529
519
|
except OSError:
|
|
530
520
|
return defecto
|
|
@@ -971,9 +961,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
971
961
|
c.pop("texto", None)
|
|
972
962
|
tarjetas.append(c)
|
|
973
963
|
ps, lab, _ = parcels.lee(os.path.join(datos, "parcels.yml"))
|
|
974
|
-
sesiones =
|
|
975
|
-
l.split(":")[0] for l in gh.sh(["tmux", "ls", "-F", "#{session_name}"]).splitlines()
|
|
976
|
-
]
|
|
964
|
+
sesiones = multiplexor.sesiones()
|
|
977
965
|
return self.responde(
|
|
978
966
|
{
|
|
979
967
|
"datos": datos,
|
|
@@ -991,7 +979,19 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
991
979
|
"parcelas": ps,
|
|
992
980
|
"lab": sorted(lab),
|
|
993
981
|
"gh": gh.conectado(),
|
|
994
|
-
|
|
982
|
+
# The window server, named on the wire rather than in the page.
|
|
983
|
+
# The Hall shows somebody the command to reattach to their own
|
|
984
|
+
# day, and that command belongs to whichever server is actually
|
|
985
|
+
# running the windows — so it is built from the same table the
|
|
986
|
+
# rest of the product drives it with, and the page prints what
|
|
987
|
+
# it is handed.
|
|
988
|
+
"mux": {
|
|
989
|
+
"name": multiplexor.cual(),
|
|
990
|
+
"sesiones": sesiones,
|
|
991
|
+
"attach": " ".join(
|
|
992
|
+
multiplexor.argv("attach", session=cities.sesion(owner, datos))
|
|
993
|
+
),
|
|
994
|
+
},
|
|
995
995
|
"plugin": plugin_de_verdad(),
|
|
996
996
|
"mapa": mapa_vivo(datos),
|
|
997
997
|
"sesion": cities.sesion(owner, datos),
|
|
@@ -1199,7 +1199,8 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1199
1199
|
ficha_previa = os.path.join(datos, f"{quien}.md")
|
|
1200
1200
|
try:
|
|
1201
1201
|
texto_previo = card.lee(ficha_previa).get("texto") or ""
|
|
1202
|
-
roster = [workspace.como_ficha(a
|
|
1202
|
+
roster = [workspace.como_ficha(a, workspace.ajustes_de(texto_previo, a.slug))
|
|
1203
|
+
for a in workspace.agentes(texto_previo, datos)]
|
|
1203
1204
|
except (OSError, ValueError):
|
|
1204
1205
|
roster = []
|
|
1205
1206
|
# Absent means unchanged, present means replace. This endpoint rewrites
|
|
@@ -1444,7 +1445,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1444
1445
|
[guion, datos],
|
|
1445
1446
|
stdout=subprocess.DEVNULL,
|
|
1446
1447
|
stderr=subprocess.DEVNULL,
|
|
1447
|
-
|
|
1448
|
+
**runtime_processes.DESPEGADO,
|
|
1448
1449
|
)
|
|
1449
1450
|
return self.responde({"ok": True, "mapa": ""})
|
|
1450
1451
|
|
|
@@ -1466,7 +1467,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1466
1467
|
),
|
|
1467
1468
|
stdout=subprocess.DEVNULL,
|
|
1468
1469
|
stderr=subprocess.DEVNULL,
|
|
1469
|
-
|
|
1470
|
+
**runtime_processes.DESPEGADO,
|
|
1470
1471
|
)
|
|
1471
1472
|
return self.responde(
|
|
1472
1473
|
{"ok": True, "attach": f"tmux attach -t {cities.sesion(quien, datos)}"}
|
|
@@ -1543,7 +1544,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1543
1544
|
# the new one joins them. Nothing that was working stops working. The
|
|
1544
1545
|
# keys come from workspace, which owns what a roster looks like on a
|
|
1545
1546
|
# card — the wizard writes the very same ones after its seven questions.
|
|
1546
|
-
roster = [workspace.como_ficha(x) for x in ya]
|
|
1547
|
+
roster = [workspace.como_ficha(x, workspace.ajustes_de(texto, x.slug)) for x in ya]
|
|
1547
1548
|
roster.append(
|
|
1548
1549
|
{"nombre": nombre, "slug": slug, "clase": clase, "rol": rol,
|
|
1549
1550
|
"mounts": [], "motor": {}, "skills": []}
|