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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +11 -0
  3. package/README.md +10 -0
  4. package/benchmarks/latency/fake-claude-cli.mjs +3 -2
  5. package/bin/serve.py +48 -8
  6. package/bin/test +2 -2
  7. package/bin/test-alcance.py +470 -0
  8. package/bin/test-channel.py +261 -7
  9. package/bin/test-cities.py +57 -0
  10. package/bin/test-claude-runtime.py +17 -6
  11. package/bin/test-connect-client.mjs +8 -0
  12. package/bin/test-connect.py +1 -1
  13. package/bin/test-contracts.py +129 -3
  14. package/bin/test-e2e.py +371 -0
  15. package/bin/test-seat.py +102 -1
  16. package/bin/test-serve.py +59 -0
  17. package/city/web/dist-hall/hall.js +384 -3240
  18. package/docs/managed-connect.md +5 -0
  19. package/package.json +1 -1
  20. package/plugin/.claude-plugin/plugin.json +1 -1
  21. package/plugin/channel/hub/lo-que-te-llega.ts +78 -0
  22. package/plugin/channel/hub/local-roads.ts +36 -0
  23. package/plugin/channel/hub/road-controller.ts +30 -5
  24. package/plugin/channel/local-hub.js +177 -105
  25. package/plugin/channel/local-hub.ts +8 -0
  26. package/plugin/channel/managed-connect-client.js +53 -3
  27. package/plugin/channel/managed-connect-client.manifest.json +2 -2
  28. package/plugin/channel/protocol.ts +24 -0
  29. package/plugin/commands/notice.md +14 -0
  30. package/plugin/hooks/ask-the-house.sh +25 -0
  31. package/plugin/hooks/hooks.json +10 -0
  32. package/plugin/hooks/notice-on-stop.sh +7 -1
  33. package/plugin/scripts/alcance.py +318 -0
  34. package/plugin/scripts/busca.py +13 -0
  35. package/plugin/scripts/card.py +40 -0
  36. package/plugin/scripts/cities.py +46 -4
  37. package/plugin/scripts/city-session.sh +80 -7
  38. package/plugin/scripts/domains.py +7 -0
  39. package/plugin/scripts/seat.py +33 -13
  40. 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
- + '\n[ "$1" = "has-session" ] && exit 1\nexit 0\n'
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"},