agents-city 0.5.3 → 0.5.4
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 +11 -0
- package/README.md +10 -0
- package/benchmarks/latency/fake-claude-cli.mjs +3 -2
- package/bin/serve.py +48 -8
- package/bin/test +2 -2
- package/bin/test-alcance.py +470 -0
- package/bin/test-channel.py +261 -7
- package/bin/test-cities.py +57 -0
- package/bin/test-claude-runtime.py +17 -6
- package/bin/test-connect-client.mjs +8 -0
- package/bin/test-connect.py +1 -1
- package/bin/test-contracts.py +129 -3
- package/bin/test-e2e.py +371 -0
- package/bin/test-seat.py +102 -1
- package/bin/test-serve.py +59 -0
- package/city/web/dist-hall/hall.js +384 -3240
- package/docs/managed-connect.md +5 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/hub/lo-que-te-llega.ts +78 -0
- package/plugin/channel/hub/local-roads.ts +36 -0
- package/plugin/channel/hub/road-controller.ts +30 -5
- package/plugin/channel/local-hub.js +177 -105
- package/plugin/channel/local-hub.ts +8 -0
- package/plugin/channel/managed-connect-client.js +53 -3
- package/plugin/channel/managed-connect-client.manifest.json +2 -2
- package/plugin/channel/protocol.ts +24 -0
- package/plugin/commands/notice.md +14 -0
- package/plugin/hooks/ask-the-house.sh +25 -0
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/hooks/notice-on-stop.sh +7 -1
- package/plugin/scripts/alcance.py +318 -0
- package/plugin/scripts/busca.py +13 -0
- package/plugin/scripts/card.py +40 -0
- package/plugin/scripts/cities.py +46 -4
- package/plugin/scripts/city-session.sh +80 -7
- package/plugin/scripts/domains.py +7 -0
- package/plugin/scripts/seat.py +33 -13
- package/plugin/skills/city/SKILL.md +12 -0
package/bin/test-seat.py
CHANGED
|
@@ -1255,10 +1255,16 @@ def arranque_escalonado():
|
|
|
1255
1255
|
|
|
1256
1256
|
registro = os.path.join(casa, "tmux.log")
|
|
1257
1257
|
falso = os.path.join(fbin, "tmux")
|
|
1258
|
+
# A tmux that writes down what it is told. FAKE_SESSION/FAKE_WINDOWS let a
|
|
1259
|
+
# test say "this city is already open, with these windows in it", which is
|
|
1260
|
+
# the only way to exercise the reconcile path without a real tmux server.
|
|
1258
1261
|
open(falso, "w").write(
|
|
1259
1262
|
'#!/bin/bash\nprintf "%s\\n" "$*" >> '
|
|
1260
1263
|
+ registro
|
|
1261
|
-
+ '\
|
|
1264
|
+
+ '\nif [ "$1" = "has-session" ]; then [ -n "${FAKE_SESSION:-}" ] || exit 1; exit 0; fi'
|
|
1265
|
+
+ '\nif [ "$1" = "list-windows" ]; then'
|
|
1266
|
+
+ ' for w in ${FAKE_WINDOWS:-}; do echo "$w"; done; exit 0; fi'
|
|
1267
|
+
+ '\nexit 0\n'
|
|
1262
1268
|
)
|
|
1263
1269
|
os.chmod(falso, 0o755)
|
|
1264
1270
|
falso_node = os.path.join(fbin, "node")
|
|
@@ -1498,6 +1504,101 @@ def arranque_escalonado():
|
|
|
1498
1504
|
"city-runtime.sh gateway seat" in asiento, asiento[:200],
|
|
1499
1505
|
)
|
|
1500
1506
|
card.pon_campo(ficha, "ui.seat", "")
|
|
1507
|
+
|
|
1508
|
+
# ── a city that is already open ──────────────────────────────────────────
|
|
1509
|
+
#
|
|
1510
|
+
# Adding a house to a running city used to do nothing you could see. The
|
|
1511
|
+
# launcher's first act was "session exists → attach", so the card had four
|
|
1512
|
+
# agents and tmux had three windows, and closing the terminal did not help:
|
|
1513
|
+
# detaching from a tmux session is not ending it. The only way to meet the
|
|
1514
|
+
# new agent was to kill the city — every agent running in it — and start
|
|
1515
|
+
# over.
|
|
1516
|
+
#
|
|
1517
|
+
# These read the RAW tmux log rather than the decoded launch commands,
|
|
1518
|
+
# because what is being asserted is which windows were created and which
|
|
1519
|
+
# were left alone.
|
|
1520
|
+
def crudo(**extra):
|
|
1521
|
+
corre(**extra)
|
|
1522
|
+
return open(registro).read()
|
|
1523
|
+
|
|
1524
|
+
log = crudo(FAKE_SESSION="1", FAKE_WINDOWS="seat api", CITY_SETTLE="0", CITY_STAGGER="0")
|
|
1525
|
+
afirma(
|
|
1526
|
+
"· happy: an agent added to a running city gets its window",
|
|
1527
|
+
"new-window" in log and " -n docs " in log,
|
|
1528
|
+
log[:600],
|
|
1529
|
+
)
|
|
1530
|
+
afirma(
|
|
1531
|
+
"· and the window that was already open is not created twice",
|
|
1532
|
+
log.count(" -n api ") == 0,
|
|
1533
|
+
log[:600],
|
|
1534
|
+
)
|
|
1535
|
+
afirma(
|
|
1536
|
+
"· non-happy: nothing is killed, ever",
|
|
1537
|
+
"kill-window" not in log and "kill-session" not in log,
|
|
1538
|
+
log[:600],
|
|
1539
|
+
)
|
|
1540
|
+
afirma(
|
|
1541
|
+
"· non-happy: and the running session is not re-created underneath it",
|
|
1542
|
+
"new-session" not in log,
|
|
1543
|
+
log[:600],
|
|
1544
|
+
)
|
|
1545
|
+
afirma(
|
|
1546
|
+
"· the new house is what you are looking at when it opens",
|
|
1547
|
+
"select-window" in log and ":docs" in log.split("select-window")[-1][:40],
|
|
1548
|
+
log.split("select-window")[-1][:80],
|
|
1549
|
+
)
|
|
1550
|
+
lineas = corre(FAKE_SESSION="1", FAKE_WINDOWS="seat api", CITY_SETTLE="0", CITY_STAGGER="0")
|
|
1551
|
+
afirma(
|
|
1552
|
+
"· non-happy: no runtime is started in a chair that is already sitting",
|
|
1553
|
+
not any("CITY_BUS_ACTOR=seat" in l for l in lineas)
|
|
1554
|
+
and any("CITY_BUS_ACTOR=docs" in l for l in lineas),
|
|
1555
|
+
str(lineas)[:400],
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
log = crudo(FAKE_SESSION="1", FAKE_WINDOWS="seat api docs", CITY_SETTLE="0", CITY_STAGGER="0")
|
|
1559
|
+
afirma(
|
|
1560
|
+
"· happy: a city already whole opens no windows at all",
|
|
1561
|
+
" -n api " not in log and " -n docs " not in log and "new-session" not in log,
|
|
1562
|
+
log[:600],
|
|
1563
|
+
)
|
|
1564
|
+
afirma("· and still attaches", "attach" in log, log[-200:])
|
|
1565
|
+
|
|
1566
|
+
# The seat is the one window whose absence is not somebody's work in
|
|
1567
|
+
# progress: a city without its chair is not a city.
|
|
1568
|
+
log = crudo(FAKE_SESSION="1", FAKE_WINDOWS="api docs", CITY_SETTLE="0", CITY_STAGGER="0")
|
|
1569
|
+
afirma(
|
|
1570
|
+
"· happy: a session that outlived its own chair gets the chair back",
|
|
1571
|
+
" -n seat " in log,
|
|
1572
|
+
log[:600],
|
|
1573
|
+
)
|
|
1574
|
+
lineas = corre(FAKE_SESSION="1", FAKE_WINDOWS="api docs", CITY_SETTLE="0", CITY_STAGGER="0")
|
|
1575
|
+
afirma(
|
|
1576
|
+
"· and only the chair — the houses are left running",
|
|
1577
|
+
any("CITY_BUS_ACTOR=seat" in l for l in lineas)
|
|
1578
|
+
and not any("CITY_BUS_ACTOR=api" in l for l in lineas),
|
|
1579
|
+
str(lineas)[:400],
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
# A window whose agent left the card may be mid-task. Say so; do not close
|
|
1583
|
+
# it. The owner decides what to do with somebody's unfinished work.
|
|
1584
|
+
sobra = subprocess.run(
|
|
1585
|
+
["bash", guion, "ana", "--claude", "--no-sync"],
|
|
1586
|
+
capture_output=True, text=True, cwd=casa,
|
|
1587
|
+
env=dict(os.environ, HOME=casa, AGENTS_CITY_DATA=datos, CITY_CODE_DIR=codigo,
|
|
1588
|
+
PATH=fbin + os.pathsep + os.environ["PATH"],
|
|
1589
|
+
FAKE_SESSION="1", FAKE_WINDOWS="seat api docs viejo"),
|
|
1590
|
+
)
|
|
1591
|
+
afirma(
|
|
1592
|
+
"· non-happy: a window no longer on the card is reported, not closed",
|
|
1593
|
+
"viejo" in sobra.stderr and "kill-window" in sobra.stderr,
|
|
1594
|
+
sobra.stderr[-400:],
|
|
1595
|
+
)
|
|
1596
|
+
afirma(
|
|
1597
|
+
"· and the killing is left to the person whose work it is",
|
|
1598
|
+
"kill-window" not in open(registro).read(),
|
|
1599
|
+
open(registro).read()[:400],
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1501
1602
|
shutil.rmtree(casa)
|
|
1502
1603
|
|
|
1503
1604
|
|
package/bin/test-serve.py
CHANGED
|
@@ -962,6 +962,27 @@ def main():
|
|
|
962
962
|
not os.path.exists(fantasma) and isinstance(donde, str),
|
|
963
963
|
f"{fantasma} exists after only asking where to log",
|
|
964
964
|
)
|
|
965
|
+
# And the same thing through the real server, which is where it actually
|
|
966
|
+
# bit: the resolver did not create the folder, but it asked `cities.lista`
|
|
967
|
+
# — and listing HEALS, which does `makedirs` on every managed city before
|
|
968
|
+
# deciding there is nothing to write. So the journal recreated the city it
|
|
969
|
+
# was recording the removal of, and raced this very cleanup while doing it.
|
|
970
|
+
# A 404 is chosen deliberately: its handler touches no city at all, so the
|
|
971
|
+
# only thing that can bring the folder back is the log line.
|
|
972
|
+
pide(puerto, "/api/no-such-thing?city=" + otra, metodo="POST", cuerpo={})
|
|
973
|
+
afirma(
|
|
974
|
+
"· non-happy: and a request naming a city that is gone leaves it gone",
|
|
975
|
+
not os.path.exists(otra),
|
|
976
|
+
f"{otra} came back from a log line",
|
|
977
|
+
)
|
|
978
|
+
# The listing itself, which is the part that writes.
|
|
979
|
+
antes_lista = os.path.exists(otra)
|
|
980
|
+
cities_mod = serve.cities
|
|
981
|
+
cities_mod.lista("halltest", tocando=False)
|
|
982
|
+
afirma(
|
|
983
|
+
"· non-happy: and listing this user's cities does not raise the dead either",
|
|
984
|
+
not antes_lista and not os.path.exists(otra),
|
|
985
|
+
)
|
|
965
986
|
|
|
966
987
|
# ── the demo's remote control ────────────────────────────────────────
|
|
967
988
|
print(" the demo's remote control")
|
|
@@ -1266,6 +1287,44 @@ def main():
|
|
|
1266
1287
|
and "role.urgencias-web: triage" in carta,
|
|
1267
1288
|
carta,
|
|
1268
1289
|
)
|
|
1290
|
+
# The frontmatter is what the launcher and the cage read. The BODY is
|
|
1291
|
+
# what the SEAT reads — and for a while only the first one moved, so a
|
|
1292
|
+
# city could have three agents and tell its own chair, in prose, that it
|
|
1293
|
+
# had one. This fixture's card is frontmatter only, which is a card a
|
|
1294
|
+
# person may legitimately hand-write, so the check needs a card shaped
|
|
1295
|
+
# the way the wizard writes them.
|
|
1296
|
+
import seat as _seat
|
|
1297
|
+
from testlib import roster as _roster
|
|
1298
|
+
|
|
1299
|
+
ficha_completa = os.path.join(hallDatos, "halltest.md")
|
|
1300
|
+
antes_carta = open(ficha_completa, encoding="utf-8").read()
|
|
1301
|
+
_seat.escribe_ficha(
|
|
1302
|
+
ficha_completa, "halltest", "cpto", _roster(("notas", "knowledge", "apuntes"))
|
|
1303
|
+
)
|
|
1304
|
+
st, _ = pide(
|
|
1305
|
+
puerto,
|
|
1306
|
+
"/api/agentes",
|
|
1307
|
+
metodo="POST",
|
|
1308
|
+
cuerpo={"name": "guardia", "kind": "code", "role": "dev"},
|
|
1309
|
+
)
|
|
1310
|
+
entera = open(ficha_completa, encoding="utf-8").read()
|
|
1311
|
+
cuerpo_carta = entera.split("## Agents", 1)[-1].split("## ", 1)[0]
|
|
1312
|
+
afirma(
|
|
1313
|
+
"· happy: and the seat is told about it in the half of the card it reads",
|
|
1314
|
+
st == 200 and "`guardia`" in cuerpo_carta and "`notas`" in cuerpo_carta,
|
|
1315
|
+
cuerpo_carta[:300],
|
|
1316
|
+
)
|
|
1317
|
+
afirma(
|
|
1318
|
+
"· non-happy: adding a house never eats the goals or the round history",
|
|
1319
|
+
"## Round history" in entera and "## Current goals" in entera,
|
|
1320
|
+
entera[-400:],
|
|
1321
|
+
)
|
|
1322
|
+
with open(ficha_completa, "w", encoding="utf-8") as f:
|
|
1323
|
+
f.write(antes_carta)
|
|
1324
|
+
afirma(
|
|
1325
|
+
"· non-happy: a hand-written card with no roster section is left alone",
|
|
1326
|
+
"## Agents" not in open(ficha_completa, encoding="utf-8").read(),
|
|
1327
|
+
)
|
|
1269
1328
|
st, _ = pide(
|
|
1270
1329
|
puerto, "/api/agentes", metodo="POST",
|
|
1271
1330
|
cuerpo={"name": "urgencias web", "kind": "code", "role": "dev"},
|