agents-city 0.5.2 → 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 (42) 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 +63 -6
  6. package/bin/test +2 -2
  7. package/bin/test-alcance.py +470 -0
  8. package/bin/test-arnes.py +109 -0
  9. package/bin/test-channel.py +295 -9
  10. package/bin/test-cities.py +57 -0
  11. package/bin/test-claude-runtime.py +17 -6
  12. package/bin/test-connect-client.mjs +8 -0
  13. package/bin/test-connect.py +1 -1
  14. package/bin/test-contracts.py +129 -3
  15. package/bin/test-e2e.py +371 -0
  16. package/bin/test-seat.py +154 -1
  17. package/bin/test-serve.py +83 -0
  18. package/city/web/dist-hall/hall.js +384 -3240
  19. package/docs/managed-connect.md +5 -0
  20. package/package.json +1 -1
  21. package/plugin/.claude-plugin/plugin.json +1 -1
  22. package/plugin/channel/hub/lo-que-te-llega.ts +78 -0
  23. package/plugin/channel/hub/local-roads.ts +36 -0
  24. package/plugin/channel/hub/road-controller.ts +30 -5
  25. package/plugin/channel/local-hub.js +177 -105
  26. package/plugin/channel/local-hub.ts +8 -0
  27. package/plugin/channel/managed-connect-client.js +53 -3
  28. package/plugin/channel/managed-connect-client.manifest.json +2 -2
  29. package/plugin/channel/protocol.ts +24 -0
  30. package/plugin/commands/notice.md +14 -0
  31. package/plugin/hooks/ask-the-house.sh +25 -0
  32. package/plugin/hooks/hooks.json +10 -0
  33. package/plugin/hooks/notice-on-stop.sh +7 -1
  34. package/plugin/scripts/alcance.py +318 -0
  35. package/plugin/scripts/arnes.py +10 -1
  36. package/plugin/scripts/busca.py +13 -0
  37. package/plugin/scripts/card.py +40 -0
  38. package/plugin/scripts/cities.py +46 -4
  39. package/plugin/scripts/city-session.sh +80 -7
  40. package/plugin/scripts/domains.py +7 -0
  41. package/plugin/scripts/seat.py +33 -13
  42. package/plugin/skills/city/SKILL.md +12 -0
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "city",
11
11
  "source": "./plugin",
12
- "version": "0.5.2",
12
+ "version": "0.5.4",
13
13
  "description": "Operate one autonomous city seat, its domain, role, repo support agents, goal, recognised skills and explicit roads to other cities."
14
14
  }
15
15
  ]
package/README.es.md CHANGED
@@ -653,6 +653,7 @@ agents-city seat --no-yolo --no-sync
653
653
  | `--model ALIAS` | override de modelo para todas las ventanas de este arranque |
654
654
  | `--effort LEVEL` | override `low`, `medium`, `high`, `xhigh` o `max` para este arranque |
655
655
  | `--seat-yolo on\|off` | si el propio asiento corre sin preguntar permisos; persiste por ciudad (`seat_yolo` en `city.yml`, también la sexta pregunta del asistente). En local el asiento son las manos del dueño; las ventanas de repo conservan su propia historia de yolo/jaula |
656
+ | `--seat-reach open\|closed` | si el propio asiento puede trabajar dentro de los mounts de sus agentes; persiste por ciudad (`seat_reach` en `city.yml`). Cerrado por defecto: el asiento pregunta al agente dueño de ese terreno en vez de leerlo, y el rechazo dice quién es y con qué comando se le pregunta |
656
657
  | `--no-yolo` | desactiva autoaprobación en este arranque — asiento incluido, diga lo que diga `seat_yolo` |
657
658
  | `--no-sync` | no hace `git fetch/pull` inicial en repos de este arranque |
658
659
 
@@ -1818,6 +1819,7 @@ slug: producto
1818
1819
  owner: alice
1819
1820
  domain: software
1820
1821
  seat_yolo: 1
1822
+ seat_reach: closed
1821
1823
  ```
1822
1824
 
1823
1825
  El address público se deriva como `owner/slug`; no se guarda como una identidad
@@ -1826,6 +1828,15 @@ global del plugin porque varias ciudades pueden ejecutarse a la vez.
1826
1828
  sexta pregunta del asistente o con `agents-city seat --seat-yolo on|off`;
1827
1829
  `--no-yolo` sigue frenando la sesión entera.
1828
1830
 
1831
+ `seat_reach` dice si la silla tiene manos dentro de los mounts de sus agentes.
1832
+ Está `closed` mientras no escribas `open`, y cerrado significa que una llamada a
1833
+ una herramienta que cae sobre un repo, un worktree o una carpeta de documentos
1834
+ que montó un agente se rechaza, con el nombre de quien es su dueño, su rol y el
1835
+ comando que le pregunta. La silla conserva la carpeta de la ciudad, las
1836
+ carreteras y el registro. Existe porque el fallo que evita no deja rastro: un
1837
+ asiento que lee el código y responde es idéntico a uno que consultó a sus
1838
+ especialistas.
1839
+
1829
1840
  ### `<owner>.md`: ficha del asiento
1830
1841
 
1831
1842
  Es Markdown con frontmatter. Contiene el rol del asiento, repos, rol de cada repo,
package/README.md CHANGED
@@ -644,6 +644,7 @@ agents-city seat --no-yolo --no-sync
644
644
  | `--model ALIAS` | model override for all windows in this launch |
645
645
  | `--effort LEVEL` | `low`, `medium`, `high`, `xhigh`, or `max` override for this launch |
646
646
  | `--seat-yolo on\|off` | whether the chair itself runs without permission prompts; persists per city (`city.yml seat_yolo`, also question six of the wizard). Locally the seat is the owner's own hands; repo windows keep their own yolo/cage story either way |
647
+ | `--seat-reach open\|closed` | whether the chair may work inside its agents' mounts; persists per city (`city.yml seat_reach`). Closed by default: the seat asks the agent who owns that ground instead of reading it, and a refusal names them and the command |
647
648
  | `--no-yolo` | disable auto-approval for this launch — seat included, whatever `seat_yolo` says |
648
649
  | `--no-sync` | skip initial `git fetch/pull` in repos for this launch |
649
650
 
@@ -1798,6 +1799,7 @@ slug: product
1798
1799
  owner: alice
1799
1800
  domain: software
1800
1801
  seat_yolo: 1
1802
+ seat_reach: closed
1801
1803
  ```
1802
1804
 
1803
1805
  The public address is derived as `owner/slug`. It is not stored as one global
@@ -1806,6 +1808,14 @@ launches the chair itself without permission prompts — set at the wizard's
1806
1808
  sixth question or with `agents-city seat --seat-yolo on|off`; `--no-yolo`
1807
1809
  still brakes the whole session.
1808
1810
 
1811
+ `seat_reach` says whether the chair has hands inside its agents' mounts. It is
1812
+ `closed` unless you write `open`, and closed means a tool call landing on a
1813
+ repo, worktree or document folder an agent mounted is refused, with the owner's
1814
+ name, their role and the one command that asks them. The chair keeps the city
1815
+ folder, the roads and the record. This exists because the failure it prevents is
1816
+ silent: a seat that reads the code and answers looks exactly like a seat that
1817
+ consulted its specialists.
1818
+
1809
1819
  ### `<owner>.md`: seat card
1810
1820
 
1811
1821
  This is Markdown with frontmatter. It stores the seat role, repos, each repo
@@ -72,7 +72,8 @@ function runNext() {
72
72
  working = true;
73
73
  const message = queue.shift();
74
74
  const text = userText(message);
75
- const delay = behavior() === 'slow' ? 350 : 5;
75
+ const mode = behavior();
76
+ const delay = mode === 'slow-long' ? 1_500 : mode === 'slow' ? 350 : 5;
76
77
  setTimeout(() => {
77
78
  emit({
78
79
  type: 'assistant',
@@ -103,7 +104,7 @@ function userText(message) {
103
104
  const content = message?.message?.content;
104
105
  if (typeof content === 'string') return content;
105
106
  if (!Array.isArray(content)) return '';
106
- return content.map((block) => block?.type === 'text' ? String(block.text || '') : '').join('');
107
+ return content.map((block) => (block?.type === 'text' ? String(block.text || '') : '')).join('');
107
108
  }
108
109
 
109
110
  function emit(value) {
package/bin/serve.py CHANGED
@@ -224,6 +224,24 @@ def olvida_crecimiento(datos, slug):
224
224
  _CRECIDO.pop((os.path.realpath(datos), slug), None)
225
225
 
226
226
 
227
+ def refresca_roster(ficha, datos):
228
+ """Bring the card's `## Agents` list back in line with its frontmatter.
229
+
230
+ The Hall writes the roster as frontmatter keys, which is what the launcher,
231
+ the cage and the map read — and for a while that was the whole of it. But
232
+ the seat reads the BODY: the list under `## Agents` is what the chair is
233
+ told its city contains. So a person who added two houses from the Hall got
234
+ two windows and a card that still said, in prose, that they had one. Every
235
+ door that changes the roster passes through here.
236
+ """
237
+ texto = card.lee(ficha).get("texto") or ""
238
+ try:
239
+ agentes = workspace.agentes(texto, datos)
240
+ except (OSError, ValueError):
241
+ return
242
+ card.cambia_agentes(ficha, [workspace.como_ficha(a) for a in agentes])
243
+
244
+
227
245
  def _crecimiento_cacheado(a, datos, vida=90):
228
246
  import time
229
247
 
@@ -619,6 +637,27 @@ class Manejador(http.server.BaseHTTPRequestHandler):
619
637
  _ultimo = None
620
638
  _error = None
621
639
 
640
+ def _ciudad_para_apuntar(self, q):
641
+ """The same city `ciudad` would resolve, resolved without touching it.
642
+
643
+ `ciudad` calls `asegura_metadata`, which does `makedirs` and writes
644
+ `city.yml` — right when a request is about to act on a city, and wrong
645
+ when the only reason we are asking is to write a log line. The journal
646
+ runs AFTER the handler, so on a request that archived or reset a city it
647
+ recreated the very folder that had just been taken away, leaving a ghost
648
+ behind. Observing something must not create it.
649
+ """
650
+ pedida = q.get("city", [""])[0]
651
+ if pedida:
652
+ resuelta = self.resuelve_conocida(pedida, tocando=False)
653
+ if resuelta:
654
+ return resuelta
655
+ # And not `donde_viven_las_fichas`, which resolves through `cities.actual`
656
+ # — a resolver that migrates, heals and will CREATE `home` when there is
657
+ # none. Asking it where to write a log line is how a request that removed
658
+ # a city brought it back.
659
+ return cities.actual_sin_tocar()
660
+
622
661
  def apunta(self, q, tipo, **campos):
623
662
  """One journal line, in the journal of the city the request acted on.
624
663
 
@@ -628,7 +667,11 @@ class Manejador(http.server.BaseHTTPRequestHandler):
628
667
  record, because it is a record that lies about where.
629
668
  """
630
669
  try:
631
- diario.apunta(self.ciudad(q), tipo, **campos)
670
+ donde = self._ciudad_para_apuntar(q)
671
+ # No city, no line. A journal with nowhere to go must not be handed a
672
+ # folder invented for it — that is the same bug wearing a default.
673
+ if donde:
674
+ diario.apunta(donde, tipo, **campos)
632
675
  except Exception: # noqa: BLE001 logging must not break the request
633
676
  pass
634
677
 
@@ -670,12 +713,23 @@ class Manejador(http.server.BaseHTTPRequestHandler):
670
713
  return False
671
714
  return True
672
715
 
673
- def resuelve_conocida(self, pedida):
674
- """Resolve only cities already in this Hall; never arbitrary folders."""
716
+ def resuelve_conocida(self, pedida, tocando=True):
717
+ """Resolve only cities already in this Hall; never arbitrary folders.
718
+
719
+ `tocando=False` makes the whole resolution read-only, for the journal:
720
+ both the listing and the fallback to the selected city are repairers by
721
+ default, and repairing something in order to write a line about it is
722
+ how a deleted city comes back.
723
+ """
675
724
  usuario = seat.quien_soy()
676
- actual = seat.donde_viven_las_fichas()
677
- candidatas = cities.lista(usuario)
678
- if not any(os.path.realpath(c["ruta"]) == os.path.realpath(actual) for c in candidatas):
725
+ actual = seat.donde_viven_las_fichas() if tocando else cities.actual_sin_tocar()
726
+ candidatas = cities.lista(usuario, tocando=tocando)
727
+ # The selected city, when the listing did not already cover it. With no
728
+ # city selected at all there is nothing to add, and asking anyway would
729
+ # hand `identidad('')` the current working directory.
730
+ if actual and not any(
731
+ os.path.realpath(c["ruta"]) == os.path.realpath(actual) for c in candidatas
732
+ ):
679
733
  candidatas.append(
680
734
  {
681
735
  "ruta": actual,
@@ -1426,6 +1480,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
1426
1480
  for clave, valor in workspace.claves_de_roster(roster).items():
1427
1481
  card.pon_campo(ficha, clave, valor)
1428
1482
  workspace.crea_workspace(datos, slug)
1483
+ refresca_roster(ficha, datos)
1429
1484
  return self.responde({"ok": True, "agent": slug, "name": nombre})
1430
1485
 
1431
1486
  def p_montaje(self, q, cuerpo):
@@ -1478,6 +1533,8 @@ class Manejador(http.server.BaseHTTPRequestHandler):
1478
1533
  # "this agent declares no mounts" in a longer, staler way.
1479
1534
  lista = ("[" + ", ".join(fuentes) + "]") if fuentes else ""
1480
1535
  card.pon_campo(ficha, f"mounts.{a.slug}", lista)
1536
+ # The body counts mounts out loud ("2 mounts"), so it moves too.
1537
+ refresca_roster(ficha, datos)
1481
1538
  # What this agent works on just changed, which is exactly what growth
1482
1539
  # counts: remembering the old number for 90s would read as a failed mount.
1483
1540
  olvida_crecimiento(datos, a.slug)
package/bin/test CHANGED
@@ -22,7 +22,7 @@ if [ -d .git ] && [ -x .githooks/pre-commit ] \
22
22
  && printf ' pre-commit gate armed (core.hooksPath .githooks)\n'
23
23
  fi
24
24
 
25
- SUITES=(widgets card parcels domains busca arnes i18n diario serve seat cities channel connect committee live-feed runtime claude-runtime runtime-ui runtime-failures stress adapter benchmark demo contracts exit rutas cage broker evidencia admision pairing hall-protocol doctor security workspace avatar crecimiento atajos navegador actualiza desinstala launch)
25
+ SUITES=(widgets card parcels domains busca arnes i18n diario alcance serve seat cities channel connect committee live-feed runtime claude-runtime runtime-ui runtime-failures stress adapter benchmark demo contracts exit rutas cage broker evidencia admision pairing hall-protocol doctor security workspace avatar crecimiento atajos navegador actualiza desinstala launch e2e)
26
26
  [ $# -gt 0 ] && SUITES=("$@")
27
27
 
28
28
  fallos=0
@@ -91,7 +91,7 @@ necesarios = ["bin/agents-city.js", "bin/hall", "bin/seat", "bin/city", "bin/dem
91
91
  "plugin/scripts/crecimiento.py",
92
92
  "plugin/scripts/reset.py", "plugin/scripts/desinstala.py",
93
93
  "plugin/scripts/demos.py", "plugin/scripts/arnes.py",
94
- "plugin/scripts/diario.py",
94
+ "plugin/scripts/diario.py", "plugin/scripts/alcance.py",
95
95
  "plugin/channel/runtime/arnes.json", "plugin/scripts/capabilities.py",
96
96
  "plugin/scripts/deliberations.py",
97
97
  "plugin/scripts/runtime_processes.py",