agents-city 0.1.0

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 (449) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2226 -0
  5. package/README.md +2196 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +119 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/doctor +3 -0
  28. package/bin/exit +5 -0
  29. package/bin/hall +73 -0
  30. package/bin/hall.html +498 -0
  31. package/bin/logs +3 -0
  32. package/bin/navegador.mjs +243 -0
  33. package/bin/report.py +14 -0
  34. package/bin/reset +3 -0
  35. package/bin/road +3 -0
  36. package/bin/seat +10 -0
  37. package/bin/serve.py +1579 -0
  38. package/bin/setup.py +753 -0
  39. package/bin/shortcut +3 -0
  40. package/bin/skills +3 -0
  41. package/bin/test +133 -0
  42. package/bin/test-actualiza.py +130 -0
  43. package/bin/test-adapter.py +183 -0
  44. package/bin/test-admision.py +99 -0
  45. package/bin/test-atajos.py +301 -0
  46. package/bin/test-avatar.py +58 -0
  47. package/bin/test-benchmark.py +40 -0
  48. package/bin/test-broker.py +334 -0
  49. package/bin/test-cage.py +374 -0
  50. package/bin/test-card.py +402 -0
  51. package/bin/test-channel.py +379 -0
  52. package/bin/test-cities.py +453 -0
  53. package/bin/test-claude-runtime.py +252 -0
  54. package/bin/test-committee.py +340 -0
  55. package/bin/test-contracts.py +551 -0
  56. package/bin/test-crecimiento.py +164 -0
  57. package/bin/test-demo.py +308 -0
  58. package/bin/test-doctor.py +139 -0
  59. package/bin/test-domains.py +151 -0
  60. package/bin/test-evidencia.py +50 -0
  61. package/bin/test-exit.py +128 -0
  62. package/bin/test-hall-protocol.py +93 -0
  63. package/bin/test-launch.py +169 -0
  64. package/bin/test-live-feed.py +508 -0
  65. package/bin/test-navegador.py +140 -0
  66. package/bin/test-pairing.py +99 -0
  67. package/bin/test-parcels.py +283 -0
  68. package/bin/test-runtime-failures.py +252 -0
  69. package/bin/test-runtime-ui.py +488 -0
  70. package/bin/test-runtime.py +268 -0
  71. package/bin/test-rutas.py +80 -0
  72. package/bin/test-seat.py +1598 -0
  73. package/bin/test-security.py +150 -0
  74. package/bin/test-serve.py +1251 -0
  75. package/bin/test-stress.py +64 -0
  76. package/bin/test-widgets.py +143 -0
  77. package/bin/test-workspace.py +251 -0
  78. package/bin/testlib.py +118 -0
  79. package/bin/tokens.py +14 -0
  80. package/bin/update +3 -0
  81. package/bus/scripts/deploy.sh +60 -0
  82. package/bus/scripts/mint-token.sh +68 -0
  83. package/bus/scripts/setup-dev.sh +79 -0
  84. package/bus/scripts/test-channel.ts +8 -0
  85. package/bus/scripts/test-hub.ts +266 -0
  86. package/bus/scripts/test-local.sh +40 -0
  87. package/bus/scripts/test-queue.ts +132 -0
  88. package/bus/worker/package-lock.json +1971 -0
  89. package/bus/worker/package.json +17 -0
  90. package/bus/worker/src/index.ts +485 -0
  91. package/bus/worker/tsconfig.json +13 -0
  92. package/bus/worker/wrangler.toml +32 -0
  93. package/city/oven/README.md +59 -0
  94. package/city/oven/collect.py +43 -0
  95. package/city/oven/oven.html +4 -0
  96. package/city/scripts/history.py +120 -0
  97. package/city/scripts/seed.py +442 -0
  98. package/city/web/assets/modelos/License.txt +28 -0
  99. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  100. package/city/web/assets/modelos/building-a.glb +0 -0
  101. package/city/web/assets/modelos/building-b.glb +0 -0
  102. package/city/web/assets/modelos/building-c.glb +0 -0
  103. package/city/web/assets/modelos/building-d.glb +0 -0
  104. package/city/web/assets/modelos/building-e.glb +0 -0
  105. package/city/web/assets/modelos/building-f.glb +0 -0
  106. package/city/web/assets/modelos/building-g.glb +0 -0
  107. package/city/web/assets/modelos/building-h.glb +0 -0
  108. package/city/web/assets/sprites/building-a.png +0 -0
  109. package/city/web/assets/sprites/building-b.png +0 -0
  110. package/city/web/assets/sprites/building-c.png +0 -0
  111. package/city/web/assets/sprites/building-d.png +0 -0
  112. package/city/web/assets/sprites/building-e.png +0 -0
  113. package/city/web/assets/sprites/building-f.png +0 -0
  114. package/city/web/assets/sprites/building-g.png +0 -0
  115. package/city/web/assets/sprites/building-h.png +0 -0
  116. package/city/web/assets/sprites/building-i.png +0 -0
  117. package/city/web/assets/sprites/building-j.png +0 -0
  118. package/city/web/assets/sprites/building-k.png +0 -0
  119. package/city/web/assets/sprites/building-l.png +0 -0
  120. package/city/web/assets/sprites/building-m.png +0 -0
  121. package/city/web/assets/sprites/building-n.png +0 -0
  122. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  123. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  124. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  125. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  126. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  127. package/city/web/assets/sprites/catalogo.json +47 -0
  128. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  129. package/city/web/assets/sprites/detail-awning.png +0 -0
  130. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  131. package/city/web/assets/sprites/detail-overhang.png +0 -0
  132. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  133. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  143. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  144. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  145. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  146. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  147. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  148. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  149. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  150. package/city/web/assets/sprites/medidas.json +248 -0
  151. package/city/web/dist/city.js +939 -0
  152. package/city/web/dist/index.html +294 -0
  153. package/city/web/dist/modelos/License.txt +28 -0
  154. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  155. package/city/web/dist/modelos/building-a.glb +0 -0
  156. package/city/web/dist/modelos/building-b.glb +0 -0
  157. package/city/web/dist/modelos/building-c.glb +0 -0
  158. package/city/web/dist/modelos/building-d.glb +0 -0
  159. package/city/web/dist/modelos/building-e.glb +0 -0
  160. package/city/web/dist/modelos/building-f.glb +0 -0
  161. package/city/web/dist/modelos/building-g.glb +0 -0
  162. package/city/web/dist/modelos/building-h.glb +0 -0
  163. package/city/web/dist/sprites/building-a.png +0 -0
  164. package/city/web/dist/sprites/building-b.png +0 -0
  165. package/city/web/dist/sprites/building-c.png +0 -0
  166. package/city/web/dist/sprites/building-d.png +0 -0
  167. package/city/web/dist/sprites/building-e.png +0 -0
  168. package/city/web/dist/sprites/building-f.png +0 -0
  169. package/city/web/dist/sprites/building-g.png +0 -0
  170. package/city/web/dist/sprites/building-h.png +0 -0
  171. package/city/web/dist/sprites/building-i.png +0 -0
  172. package/city/web/dist/sprites/building-j.png +0 -0
  173. package/city/web/dist/sprites/building-k.png +0 -0
  174. package/city/web/dist/sprites/building-l.png +0 -0
  175. package/city/web/dist/sprites/building-m.png +0 -0
  176. package/city/web/dist/sprites/building-n.png +0 -0
  177. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  178. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  179. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  180. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  181. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  182. package/city/web/dist/sprites/catalogo.json +47 -0
  183. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  184. package/city/web/dist/sprites/detail-awning.png +0 -0
  185. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  186. package/city/web/dist/sprites/detail-overhang.png +0 -0
  187. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  188. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  198. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  199. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  200. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  201. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  202. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  203. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  204. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  205. package/city/web/dist/sprites/medidas.json +248 -0
  206. package/city/web/dist-hall/hall.js +1363 -0
  207. package/city/web/index.html +294 -0
  208. package/city/web/package.json +18 -0
  209. package/city/web/sella.py +21 -0
  210. package/city/web/src/activity-actors.ts +47 -0
  211. package/city/web/src/activity.ts +172 -0
  212. package/city/web/src/ayuntamiento.ts +580 -0
  213. package/city/web/src/draw.ts +640 -0
  214. package/city/web/src/game-speech.ts +79 -0
  215. package/city/web/src/hall.ts +1959 -0
  216. package/city/web/src/main.ts +2252 -0
  217. package/city/web/src/oven.ts +124 -0
  218. package/city/web/src/people.ts +244 -0
  219. package/city/web/src/presencia.ts +193 -0
  220. package/city/web/src/puertas.ts +180 -0
  221. package/city/web/src/repo-roles.ts +59 -0
  222. package/city/worker/package.json +16 -0
  223. package/city/worker/schema.sql +117 -0
  224. package/city/worker/src/index.ts +702 -0
  225. package/city/worker/src/square.ts +218 -0
  226. package/city/worker/wrangler.toml +58 -0
  227. package/demo/ada.md +50 -0
  228. package/demo/bruno.md +62 -0
  229. package/demo/camila.md +56 -0
  230. package/demo/city/ada.md +40 -0
  231. package/demo/city/city.yml +8 -0
  232. package/demo/city/parcels.yml +15 -0
  233. package/demo/city/roads.json +4 -0
  234. package/demo/city/units.yml +10 -0
  235. package/demo/clinica/city.yml +8 -0
  236. package/demo/clinica/parcels.yml +15 -0
  237. package/demo/clinica/roads.json +4 -0
  238. package/demo/clinica/units.yml +9 -0
  239. package/demo/clinica/vera.md +46 -0
  240. package/demo/dante.md +54 -0
  241. package/demo/despacho/city.yml +8 -0
  242. package/demo/despacho/marta.md +46 -0
  243. package/demo/despacho/parcels.yml +15 -0
  244. package/demo/despacho/roads.json +4 -0
  245. package/demo/despacho/units.yml +9 -0
  246. package/demo/elsa.md +50 -0
  247. package/demo/farid.md +52 -0
  248. package/demo/greta.md +50 -0
  249. package/demo/hugo.md +49 -0
  250. package/demo/iris.md +49 -0
  251. package/demo/jonas.md +51 -0
  252. package/demo/kira.md +51 -0
  253. package/demo/luca.md +49 -0
  254. package/demo/parcels.yml +92 -0
  255. package/demo/seed.py +20 -0
  256. package/demo/show.py +161 -0
  257. package/demo/stories.py +824 -0
  258. package/demo/units.yml +33 -0
  259. package/docs/agents-first.md +106 -0
  260. package/docs/glossary.md +55 -0
  261. package/docs/map-live-layers.md +73 -0
  262. package/docs/security.md +265 -0
  263. package/docs/self-host.md +151 -0
  264. package/docs/testing.md +205 -0
  265. package/package.json +73 -0
  266. package/plugin/.claude-plugin/plugin.json +62 -0
  267. package/plugin/.mcp.json +15 -0
  268. package/plugin/channel/activity-cli.ts +16 -0
  269. package/plugin/channel/adapter-prompts.ts +113 -0
  270. package/plugin/channel/adapter.js +4292 -0
  271. package/plugin/channel/adapter.ts +133 -0
  272. package/plugin/channel/bus.js +20188 -0
  273. package/plugin/channel/bus.ts +131 -0
  274. package/plugin/channel/city-config.ts +125 -0
  275. package/plugin/channel/claude-channel.ts +58 -0
  276. package/plugin/channel/cli-args.ts +75 -0
  277. package/plugin/channel/client.js +4200 -0
  278. package/plugin/channel/client.ts +38 -0
  279. package/plugin/channel/committee/activity.ts +256 -0
  280. package/plugin/channel/committee/collection.ts +154 -0
  281. package/plugin/channel/committee/decision.ts +175 -0
  282. package/plugin/channel/committee/floor.ts +135 -0
  283. package/plugin/channel/committee/guards.ts +30 -0
  284. package/plugin/channel/committee/history.ts +38 -0
  285. package/plugin/channel/committee/render.ts +102 -0
  286. package/plugin/channel/committee/service.ts +72 -0
  287. package/plugin/channel/committee/storage.ts +92 -0
  288. package/plugin/channel/committee/types.ts +163 -0
  289. package/plugin/channel/committee/view.ts +63 -0
  290. package/plugin/channel/committee-cli.ts +109 -0
  291. package/plugin/channel/delivery-metrics.ts +61 -0
  292. package/plugin/channel/delivery-queue.ts +118 -0
  293. package/plugin/channel/hub/activity-controller.ts +72 -0
  294. package/plugin/channel/hub/activity-feed.ts +169 -0
  295. package/plugin/channel/hub/committee-controller.ts +60 -0
  296. package/plugin/channel/hub/connections.ts +48 -0
  297. package/plugin/channel/hub/diagnostics.ts +67 -0
  298. package/plugin/channel/hub/envelope-validity.ts +109 -0
  299. package/plugin/channel/hub/envelopes.ts +66 -0
  300. package/plugin/channel/hub/lifecycle.ts +91 -0
  301. package/plugin/channel/hub/local-roads.ts +89 -0
  302. package/plugin/channel/hub/remote-roads.ts +165 -0
  303. package/plugin/channel/hub/road-controller.ts +99 -0
  304. package/plugin/channel/hub-client.ts +149 -0
  305. package/plugin/channel/local-hub.js +5995 -0
  306. package/plugin/channel/local-hub.ts +349 -0
  307. package/plugin/channel/map-reporter.ts +48 -0
  308. package/plugin/channel/package-lock.json +1731 -0
  309. package/plugin/channel/package.json +19 -0
  310. package/plugin/channel/protocol.ts +98 -0
  311. package/plugin/channel/road-cli.ts +18 -0
  312. package/plugin/channel/run.sh +15 -0
  313. package/plugin/channel/runtime/claude.ts +458 -0
  314. package/plugin/channel/runtime/codex-config.ts +77 -0
  315. package/plugin/channel/runtime/codex.ts +669 -0
  316. package/plugin/channel/runtime/command.ts +62 -0
  317. package/plugin/channel/runtime/factory.ts +15 -0
  318. package/plugin/channel/runtime/json-rpc.ts +154 -0
  319. package/plugin/channel/runtime/kimi.ts +277 -0
  320. package/plugin/channel/runtime/opencode.ts +248 -0
  321. package/plugin/channel/runtime/process.ts +84 -0
  322. package/plugin/channel/runtime/types.ts +45 -0
  323. package/plugin/channel/runtime-files.ts +73 -0
  324. package/plugin/channel/runtime-gateway.js +6396 -0
  325. package/plugin/channel/runtime-gateway.ts +358 -0
  326. package/plugin/channel/runtime-metrics.ts +52 -0
  327. package/plugin/channel/runtime-subscription.ts +180 -0
  328. package/plugin/channel/terminal-delivery.ts +131 -0
  329. package/plugin/channel/untrusted.ts +60 -0
  330. package/plugin/commands/committee.md +31 -0
  331. package/plugin/commands/exit.md +32 -0
  332. package/plugin/commands/goals.md +20 -0
  333. package/plugin/commands/join.md +18 -0
  334. package/plugin/commands/notice.md +20 -0
  335. package/plugin/commands/propose.md +19 -0
  336. package/plugin/commands/round.md +17 -0
  337. package/plugin/commands/session.md +22 -0
  338. package/plugin/commands/settings.md +25 -0
  339. package/plugin/commands/setup.md +21 -0
  340. package/plugin/commands/team.md +23 -0
  341. package/plugin/domains/custom.md +27 -0
  342. package/plugin/domains/finance.md +31 -0
  343. package/plugin/domains/healthcare.md +34 -0
  344. package/plugin/domains/legal.md +31 -0
  345. package/plugin/domains/marketing.md +33 -0
  346. package/plugin/domains/operations.md +31 -0
  347. package/plugin/domains/research.md +32 -0
  348. package/plugin/domains/sales.md +30 -0
  349. package/plugin/domains/software.md +37 -0
  350. package/plugin/hooks/activity.sh +6 -0
  351. package/plugin/hooks/digging.sh +68 -0
  352. package/plugin/hooks/growth.sh +42 -0
  353. package/plugin/hooks/hooks.json +106 -0
  354. package/plugin/hooks/notice-on-pr.sh +35 -0
  355. package/plugin/hooks/notice-on-stop.sh +80 -0
  356. package/plugin/hooks/notice-pending.sh +58 -0
  357. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  358. package/plugin/hooks/tokens.sh +47 -0
  359. package/plugin/roles/examples/account-executive.md +25 -0
  360. package/plugin/roles/examples/ai-manager.md +34 -0
  361. package/plugin/roles/examples/associate.md +24 -0
  362. package/plugin/roles/examples/brand-lead.md +20 -0
  363. package/plugin/roles/examples/cfo.md +20 -0
  364. package/plugin/roles/examples/city-lead.md +25 -0
  365. package/plugin/roles/examples/clinical-director.md +26 -0
  366. package/plugin/roles/examples/clinical-ops.md +25 -0
  367. package/plugin/roles/examples/clinician.md +25 -0
  368. package/plugin/roles/examples/compliance.md +27 -0
  369. package/plugin/roles/examples/content.md +24 -0
  370. package/plugin/roles/examples/controller.md +27 -0
  371. package/plugin/roles/examples/cpto.md +41 -0
  372. package/plugin/roles/examples/customer-success.md +25 -0
  373. package/plugin/roles/examples/data-engineer.md +37 -0
  374. package/plugin/roles/examples/data.md +60 -0
  375. package/plugin/roles/examples/dev.md +37 -0
  376. package/plugin/roles/examples/devops.md +39 -0
  377. package/plugin/roles/examples/enablement.md +24 -0
  378. package/plugin/roles/examples/ethics.md +24 -0
  379. package/plugin/roles/examples/fin-analytics.md +27 -0
  380. package/plugin/roles/examples/health-compliance.md +25 -0
  381. package/plugin/roles/examples/health-data.md +26 -0
  382. package/plugin/roles/examples/knowledge.md +25 -0
  383. package/plugin/roles/examples/lifecycle.md +26 -0
  384. package/plugin/roles/examples/llm-engineer.md +34 -0
  385. package/plugin/roles/examples/managing-partner.md +20 -0
  386. package/plugin/roles/examples/methods.md +23 -0
  387. package/plugin/roles/examples/operations-lead.md +23 -0
  388. package/plugin/roles/examples/ops.md +24 -0
  389. package/plugin/roles/examples/patient-safety.md +26 -0
  390. package/plugin/roles/examples/performance.md +27 -0
  391. package/plugin/roles/examples/po.md +36 -0
  392. package/plugin/roles/examples/process-owner.md +23 -0
  393. package/plugin/roles/examples/product-design.md +39 -0
  394. package/plugin/roles/examples/program-manager.md +24 -0
  395. package/plugin/roles/examples/quality.md +25 -0
  396. package/plugin/roles/examples/research-director.md +24 -0
  397. package/plugin/roles/examples/research-ops.md +25 -0
  398. package/plugin/roles/examples/researcher.md +25 -0
  399. package/plugin/roles/examples/revenue-lead.md +25 -0
  400. package/plugin/roles/examples/revops.md +26 -0
  401. package/plugin/roles/examples/seo.md +27 -0
  402. package/plugin/roles/examples/specialist.md +24 -0
  403. package/plugin/scripts/actualiza.py +198 -0
  404. package/plugin/scripts/admision.py +165 -0
  405. package/plugin/scripts/apaga.py +232 -0
  406. package/plugin/scripts/atajos.py +506 -0
  407. package/plugin/scripts/avatar.py +211 -0
  408. package/plugin/scripts/broker.py +531 -0
  409. package/plugin/scripts/cage.py +492 -0
  410. package/plugin/scripts/capabilities.py +210 -0
  411. package/plugin/scripts/card.py +322 -0
  412. package/plugin/scripts/cities.py +520 -0
  413. package/plugin/scripts/city-env.sh +90 -0
  414. package/plugin/scripts/city-runtime.sh +87 -0
  415. package/plugin/scripts/city-session.sh +552 -0
  416. package/plugin/scripts/city_env.py +67 -0
  417. package/plugin/scripts/crecimiento.py +140 -0
  418. package/plugin/scripts/deliberations.py +48 -0
  419. package/plugin/scripts/doctor.py +294 -0
  420. package/plugin/scripts/domains.py +201 -0
  421. package/plugin/scripts/evidencia.py +64 -0
  422. package/plugin/scripts/find-repos.sh +111 -0
  423. package/plugin/scripts/gh.py +159 -0
  424. package/plugin/scripts/hall_protocol.py +147 -0
  425. package/plugin/scripts/hook_activity.py +139 -0
  426. package/plugin/scripts/launch.py +94 -0
  427. package/plugin/scripts/logs.py +98 -0
  428. package/plugin/scripts/pairing.py +185 -0
  429. package/plugin/scripts/parcels.py +132 -0
  430. package/plugin/scripts/read-card.py +55 -0
  431. package/plugin/scripts/report.py +201 -0
  432. package/plugin/scripts/reset.py +180 -0
  433. package/plugin/scripts/roads.py +230 -0
  434. package/plugin/scripts/roles.py +274 -0
  435. package/plugin/scripts/runtime_log.py +69 -0
  436. package/plugin/scripts/runtime_processes.py +132 -0
  437. package/plugin/scripts/rutas.py +83 -0
  438. package/plugin/scripts/seat.py +1696 -0
  439. package/plugin/scripts/tokens.py +194 -0
  440. package/plugin/scripts/trust-repos.py +59 -0
  441. package/plugin/scripts/ui.py +280 -0
  442. package/plugin/scripts/units.py +80 -0
  443. package/plugin/scripts/workspace.py +561 -0
  444. package/plugin/skills/city/SKILL.md +191 -0
  445. package/templates/blank.md +39 -0
  446. package/templates/finance.md +44 -0
  447. package/templates/legal.md +45 -0
  448. package/templates/marketing.md +48 -0
  449. package/templates/product.md +45 -0
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """Knowing there is a new version, and getting it.
3
+
4
+ A tool that cannot tell you it is out of date leaves every owner running
5
+ whatever they installed the day they found it — including the day a security
6
+ fix shipped. So this asks npm, and it asks rarely: once a day at most, cached
7
+ under the runtime dir, and never on a plain command. The check runs where the
8
+ person deliberately opened something (`doctor`, `update`, the Hall), not behind
9
+ their back on every `agents-city cities`.
10
+
11
+ It is a GET to the public registry and nothing else: no identifiers, no
12
+ counters, nothing about this machine leaves it. `CITY_UPDATE_CHECK=0` turns it
13
+ off completely, and the answer is cached so a plane, a firewall or a dead
14
+ registry degrade to silence rather than to an error.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ import time
22
+ import urllib.error
23
+ import urllib.request
24
+
25
+ GUIONES = os.path.dirname(os.path.abspath(__file__))
26
+ sys.path.insert(0, GUIONES)
27
+ import cities # noqa: E402
28
+
29
+ PAQUETE = 'agents-city'
30
+ REGISTRO = f'https://registry.npmjs.org/{PAQUETE}/latest'
31
+ #: A day. The point is to notice a release, not to poll a registry.
32
+ VIDA = 24 * 3600
33
+
34
+
35
+ def version_instalada():
36
+ """What is running right now, read from the package this file ships in."""
37
+ raiz = os.path.dirname(os.path.dirname(GUIONES))
38
+ try:
39
+ with open(os.path.join(raiz, 'package.json'), encoding='utf-8') as f:
40
+ return str(json.load(f).get('version') or '')
41
+ except (OSError, ValueError):
42
+ return ''
43
+
44
+
45
+ def _cache():
46
+ return os.path.join(cities.raiz(), '.runtime', 'version.json')
47
+
48
+
49
+ def _lee_cache():
50
+ try:
51
+ with open(_cache(), encoding='utf-8') as f:
52
+ return json.load(f)
53
+ except (OSError, ValueError):
54
+ return {}
55
+
56
+
57
+ def _guarda_cache(ultima):
58
+ ruta = _cache()
59
+ try:
60
+ os.makedirs(os.path.dirname(ruta), mode=0o700, exist_ok=True)
61
+ cities.escribe_atomico(ruta, json.dumps({'latest': ultima, 'when': time.time()}))
62
+ except OSError:
63
+ pass # not being able to remember is not a reason to fail a command
64
+
65
+
66
+ def consulta_registro(timeout=8):
67
+ """The published version, or '' when the network says nothing useful.
68
+
69
+ Eight seconds because the first call of the day pays DNS and a TLS
70
+ handshake from cold; a tighter budget turns "you are up to date" into a
71
+ lie told by a stopwatch.
72
+ """
73
+ try:
74
+ peticion = urllib.request.Request(
75
+ REGISTRO, headers={'Accept': 'application/vnd.npm.install-v1+json'}
76
+ )
77
+ with urllib.request.urlopen(peticion, timeout=timeout) as r:
78
+ return str(json.load(r).get('version') or '')
79
+ except (urllib.error.URLError, OSError, ValueError, TimeoutError):
80
+ return ''
81
+
82
+
83
+ def _partes(v):
84
+ """A version as comparable pieces. A prerelease sorts BELOW its release."""
85
+ base, _, pre = str(v).partition('-')
86
+ numeros = []
87
+ for trozo in base.split('.'):
88
+ try:
89
+ numeros.append(int(trozo))
90
+ except ValueError:
91
+ numeros.append(0)
92
+ while len(numeros) < 3:
93
+ numeros.append(0)
94
+ # No prerelease outranks one: 1.0.0 > 1.0.0-beta.1, as semver says.
95
+ return (numeros[0], numeros[1], numeros[2], 1 if not pre else 0, pre)
96
+
97
+
98
+ def es_mas_nueva(candidata, actual):
99
+ return bool(candidata) and bool(actual) and _partes(candidata) > _partes(actual)
100
+
101
+
102
+ def comprueba(forzar=False, vida=VIDA):
103
+ """(installed, published, there-is-an-update). Cached, quiet, opt-out-able.
104
+
105
+ Returns the published version as '' when the check is switched off or the
106
+ registry could not be reached — the caller then says nothing, which is the
107
+ right thing to say when you do not know.
108
+ """
109
+ instalada = version_instalada()
110
+ if os.environ.get('CITY_UPDATE_CHECK', '1') == '0':
111
+ return instalada, '', False
112
+ guardado = _lee_cache()
113
+ fresca = (time.time() - float(guardado.get('when') or 0)) < vida
114
+ if fresca and not forzar:
115
+ ultima = str(guardado.get('latest') or '')
116
+ else:
117
+ ultima = consulta_registro()
118
+ if ultima:
119
+ _guarda_cache(ultima)
120
+ else:
121
+ ultima = str(guardado.get('latest') or '')
122
+ return instalada, ultima, es_mas_nueva(ultima, instalada)
123
+
124
+
125
+ def aviso():
126
+ """One line for a person who did not ask, or '' when there is nothing to say."""
127
+ instalada, ultima, hay = comprueba()
128
+ if not hay:
129
+ return ''
130
+ return (f' A newer Agents City is out: {instalada} → {ultima}. '
131
+ f'Update with: agents-city update')
132
+
133
+
134
+ def como_se_instalo():
135
+ """'npm' when this copy lives inside a global npm install, else 'clone'.
136
+
137
+ It decides what `update` may do: pulling somebody's git checkout from under
138
+ them is not an update, it is a surprise.
139
+ """
140
+ raiz = os.path.dirname(os.path.dirname(GUIONES))
141
+ if os.path.isdir(os.path.join(raiz, '.git')):
142
+ return 'clone'
143
+ return 'npm' if f'node_modules{os.sep}{PAQUETE}' in raiz else 'clone'
144
+
145
+
146
+ def actualiza(canal=''):
147
+ """Install the newest published version over this one. Returns an exit code."""
148
+ if como_se_instalo() == 'clone':
149
+ print(' This is a git checkout, not an npm install.\n'
150
+ ' Update it the way you got it:\n'
151
+ ' git pull && npm pack && npm install -g ./agents-city-*.tgz')
152
+ return 1
153
+ destino = f'{PAQUETE}@{canal}' if canal else PAQUETE
154
+ print(f' Installing {destino} over {version_instalada()}…')
155
+ hecho = subprocess.run(['npm', 'install', '-g', destino])
156
+ if hecho.returncode != 0:
157
+ print(' npm could not install it. Nothing was changed.', file=sys.stderr)
158
+ return hecho.returncode
159
+ print(' Done. Open cities keep the code they already loaded;\n'
160
+ ' `agents-city exit <city>` and then `agents-city seat` picks the new one up.')
161
+ return 0
162
+
163
+
164
+ def _uso():
165
+ print(
166
+ ' usage: agents-city update [--check] [--tag beta]\n\n'
167
+ ' Install the newest published version, or just ask whether there is one.\n\n'
168
+ ' --check say what is installed and what is published, and stop\n'
169
+ ' --tag NAME follow a dist-tag (for example: beta)\n\n'
170
+ ' The check is one GET to the public npm registry, cached for a day.\n'
171
+ ' Nothing about this machine is sent. CITY_UPDATE_CHECK=0 disables it.'
172
+ )
173
+
174
+
175
+ def main():
176
+ args = sys.argv[1:]
177
+ if any(a in ('-h', '--help', 'help') for a in args):
178
+ _uso()
179
+ return 0
180
+ if '--check' in args:
181
+ instalada, ultima, hay = comprueba(forzar=True)
182
+ if not ultima:
183
+ print(f' Installed: {instalada}. The registry could not be reached '
184
+ '(or the check is off).')
185
+ return 0
186
+ print(f' Installed: {instalada}\n Published: {ultima}')
187
+ print(' There is a newer version — agents-city update' if hay
188
+ else ' You are up to date.')
189
+ return 0
190
+ canal = ''
191
+ if '--tag' in args:
192
+ i = args.index('--tag')
193
+ canal = args[i + 1] if len(args) > i + 1 else ''
194
+ return actualiza(canal)
195
+
196
+
197
+ if __name__ == '__main__':
198
+ sys.exit(main())
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """Admission as an ordered gate graph, not a boolean.
3
+
4
+ Whether a seat accepts an inbound road message is a decision with a *reason*,
5
+ and the reason must be inspectable without leaking who is on the allowlist. So
6
+ every admission returns the ordered list of gates it evaluated, which gate was
7
+ decisive, a stable machine-readable reason code, and — for each gate — a
8
+ redacted diagnostic: counts and opaque ids, never a raw sender or allowlist
9
+ entry.
10
+
11
+ The mechanism (an ordered reducer that stops at the first blocking gate) is
12
+ kept separate from the policy (`decide_entrada`), so new gates or new callers
13
+ compose without rewriting either. Pure and transport-free: the tests drive it
14
+ directly, and the same function can decide a local road, a remote road, or a
15
+ future chat channel.
16
+ """
17
+
18
+ import hashlib
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+
22
+
23
+ class Admision(str, Enum):
24
+ DESPACHA = 'dispatch' # deliver to the seat
25
+ OBSERVA = 'observe' # record as context, do not act
26
+ OMITE = 'skip' # not addressed to us; ignore quietly
27
+ DESCARTA = 'drop' # malformed or refused; drop
28
+ EMPAREJA = 'pairing-required' # unknown sender, a pairing code is owed
29
+
30
+ def __str__(self):
31
+ return self.value
32
+
33
+
34
+ #: Closed set of reason codes. A code outside this set is a bug, so the
35
+ #: constructor asserts membership rather than letting prose leak in.
36
+ RAZONES = frozenset({
37
+ 'road_allowed',
38
+ 'road_missing',
39
+ 'road_incident',
40
+ 'sender_not_paired',
41
+ 'sender_paired',
42
+ 'payload_malformed',
43
+ 'payload_empty',
44
+ 'not_addressed_here',
45
+ })
46
+
47
+ #: Which reason a gate emits maps to how the seat should treat the message.
48
+ _ADMISION_POR_RAZON = {
49
+ 'road_allowed': Admision.DESPACHA,
50
+ 'sender_paired': Admision.DESPACHA,
51
+ 'road_missing': Admision.OMITE,
52
+ 'not_addressed_here': Admision.OMITE,
53
+ 'road_incident': Admision.DESCARTA,
54
+ 'payload_malformed': Admision.DESCARTA,
55
+ 'payload_empty': Admision.DESCARTA,
56
+ 'sender_not_paired': Admision.EMPAREJA,
57
+ }
58
+
59
+
60
+ def id_opaco(valor):
61
+ """A stable, non-reversible short id for one allowlist entry or sender."""
62
+ return 'x' + hashlib.sha256(str(valor).encode()).hexdigest()[:12]
63
+
64
+
65
+ def redacta(entradas):
66
+ """A diagnostic for an allowlist that reveals shape, never content."""
67
+ entradas = list(entradas)
68
+ return {'n': len(entradas), 'ids': [id_opaco(e) for e in entradas]}
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class Puerta:
73
+ id: str
74
+ fase: str
75
+ permitido: bool
76
+ razon: str
77
+ diagnostico: dict = field(default_factory=dict)
78
+
79
+ def __post_init__(self):
80
+ # A real raise, not assert: validation must survive `python -O`.
81
+ if self.razon not in RAZONES:
82
+ raise ValueError(f'unknown reason code: {self.razon}')
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class Decision:
87
+ admision: Admision
88
+ razon: str
89
+ puerta_decisiva: str
90
+ grafo: tuple
91
+
92
+ def permitido(self):
93
+ # Authorization lives here and ONLY here — never inferred from an
94
+ # evidence field, so a blocked decision can never read as authorised.
95
+ return self.admision is Admision.DESPACHA
96
+
97
+ def como_dict(self):
98
+ return {
99
+ 'admission': str(self.admision),
100
+ 'reasonCode': self.razon,
101
+ 'decisiveGate': self.puerta_decisiva,
102
+ 'gates': [
103
+ {'id': g.id, 'phase': g.fase, 'allowed': g.permitido,
104
+ 'reasonCode': g.razon, 'diagnostic': g.diagnostico}
105
+ for g in self.grafo
106
+ ],
107
+ }
108
+
109
+
110
+ def _resuelve(grafo):
111
+ """Reduce an ordered gate list to a decision: first blocker wins, else last.
112
+
113
+ The decision carries no evidence state on purpose: allow/deny is `permitido()`
114
+ (derived from the admission), and an audit-provenance vocabulary belongs to
115
+ the layer that records the action, not to this pure routing decision. That
116
+ keeps authorization impossible to read off a decorative field.
117
+ """
118
+ decisiva = next((g for g in grafo if not g.permitido), grafo[-1])
119
+ admision = _ADMISION_POR_RAZON[decisiva.razon]
120
+ return Decision(admision, decisiva.razon, decisiva.id, tuple(grafo))
121
+
122
+
123
+ def decide_entrada(*, road_existe, road_incidente, direccion_coincide,
124
+ emparejado, allowlist, payload_valido, texto_no_vacio,
125
+ dirigido_aqui=True):
126
+ """Agents City's road-inbound policy, phase by phase.
127
+
128
+ Order matters and is the policy: address → road → sender → payload. A road
129
+ that resolves but has no matching sender is an explicit block, never a
130
+ fallthrough; an unknown sender under pairing yields EMPAREJA so the seat can
131
+ mint a code rather than silently dropping a first contact.
132
+ """
133
+ grafo = []
134
+ diag = redacta(allowlist)
135
+
136
+ if not dirigido_aqui:
137
+ grafo.append(Puerta('address', 'route', False, 'not_addressed_here'))
138
+ return _resuelve(grafo)
139
+ grafo.append(Puerta('address', 'route', True, 'road_allowed'))
140
+
141
+ if not road_existe:
142
+ grafo.append(Puerta('road', 'route', False, 'road_missing', diag))
143
+ return _resuelve(grafo)
144
+ if road_incidente:
145
+ grafo.append(Puerta('road', 'route', False, 'road_incident', diag))
146
+ return _resuelve(grafo)
147
+ grafo.append(Puerta('road', 'route', True, 'road_allowed', diag))
148
+
149
+ if not direccion_coincide:
150
+ if emparejado:
151
+ grafo.append(Puerta('sender', 'sender', True, 'sender_paired', diag))
152
+ else:
153
+ grafo.append(Puerta('sender', 'sender', False, 'sender_not_paired', diag))
154
+ return _resuelve(grafo)
155
+ else:
156
+ grafo.append(Puerta('sender', 'sender', True, 'road_allowed', diag))
157
+
158
+ if not payload_valido:
159
+ grafo.append(Puerta('payload', 'event', False, 'payload_malformed'))
160
+ return _resuelve(grafo)
161
+ if not texto_no_vacio:
162
+ grafo.append(Puerta('payload', 'event', False, 'payload_empty'))
163
+ return _resuelve(grafo)
164
+ grafo.append(Puerta('payload', 'event', True, 'road_allowed'))
165
+ return _resuelve(grafo)
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env python3
2
+ """Close the whole day: the tmux sessions, every agent inside them, the hall,
3
+ the map. The seat's missing other half.
4
+
5
+ agents-city exit close everything, every city
6
+ agents-city exit home close ONE city — its session, its agents, its
7
+ map — and leave the others working
8
+ agents-city exit --dry-run say what would close, close nothing
9
+
10
+ `seat` builds a session with an agent per window and nothing ever tore it down:
11
+ detach, open another city, run the hall — and a machine fills with processes
12
+ nobody remembers starting. One session was found nineteen windows deep while
13
+ this was being written.
14
+
15
+ Precision matters more than reach here. This closes ONLY what the city started,
16
+ identified by marks the city itself leaves, and never by broad process-name
17
+ sweeps that would take down some other project's dev server:
18
+
19
+ * tmux sessions whose name is one this product minted — always user-city —
20
+ across every city this user owns. Your own
21
+ unrelated tmux sessions are not touched.
22
+ * the town hall, matched by its own entry point.
23
+ * the map and the demo, found by who holds the city's local database open
24
+ (`~/.agents-city/state`) — not by killing every wrangler on the machine.
25
+
26
+ Killing a session SIGHUPs its panes, which is how every Claude inside exits.
27
+ """
28
+ import argparse
29
+ import os
30
+ import signal
31
+ import subprocess
32
+ import sys
33
+ import time
34
+
35
+ AQUI = os.path.dirname(os.path.abspath(__file__))
36
+ sys.path.insert(0, AQUI)
37
+ import cities # noqa: E402
38
+ import runtime_processes # noqa: E402
39
+
40
+ ESTADO = os.path.join(cities.raiz(), 'state')
41
+
42
+
43
+ def _corre(args):
44
+ try:
45
+ r = subprocess.run(args, capture_output=True, text=True, timeout=30)
46
+ return r.stdout
47
+ except (OSError, subprocess.TimeoutExpired):
48
+ return ''
49
+
50
+
51
+ def sesiones_de_la_ciudad(solo=''):
52
+ """Every live tmux session this product would have minted, and only those.
53
+
54
+ Candidates come from city metadata: for each city this machine knows, the
55
+ owner-city session minted by cities.py. A session called anything else is
56
+ somebody's own work and stays alive. With `solo` set to one
57
+ city's folder, only that city's sessions qualify — closing the client's day
58
+ must not end the company's.
59
+ """
60
+ vivas = {l.strip() for l in _corre(['tmux', 'ls', '-F', '#{session_name}']).splitlines()
61
+ if l.strip()}
62
+ if not vivas:
63
+ return []
64
+ donde = ([{'ruta': solo}] if solo else cities.lista())
65
+ candidatas = set()
66
+ for ciudad in donde:
67
+ usuario = cities.lee_clave(ciudad['ruta'], 'owner')
68
+ if usuario:
69
+ candidatas.add(cities.sesion(usuario, ciudad['ruta']))
70
+ return sorted(vivas & candidatas)
71
+
72
+
73
+ def servidores():
74
+ """The Hall and an in-flight setup launcher, by their own entry points —
75
+ never a pattern loose enough to match somebody else's Python."""
76
+ pids = []
77
+ for patron in ('bin/hall', 'bin/setup.py'):
78
+ salida = _corre(['pgrep', '-f', patron])
79
+ pids += [int(p) for p in salida.split() if p.strip().isdigit()]
80
+ return sorted(set(pids) - {os.getpid(), os.getppid()})
81
+
82
+
83
+ def mapas(solo=''):
84
+ """Whoever holds the city's local database open: wrangler, workerd, the demo.
85
+ lsof on our own state directory is exact — it cannot name another project's
86
+ dev server, because no other project has these files open. With `solo`, only
87
+ that city's slice of the state; the demo counts as everything-only."""
88
+ donde = os.path.join(ESTADO, cities.slug(solo)) if solo else ESTADO
89
+ if not os.path.isdir(donde):
90
+ return []
91
+ salida = _corre(['lsof', '-t', '+D', donde])
92
+ return sorted({int(p) for p in salida.split() if p.strip().isdigit()}
93
+ - {os.getpid(), os.getppid()})
94
+
95
+
96
+ def mata(pids):
97
+ """TERM first, KILL for whatever ignores it. Returns how many actually died."""
98
+ if not pids:
99
+ return 0
100
+ for pid in pids:
101
+ try:
102
+ os.kill(pid, signal.SIGTERM)
103
+ except (ProcessLookupError, PermissionError):
104
+ pass
105
+ time.sleep(1.2)
106
+ muertos = 0
107
+ for pid in pids:
108
+ try:
109
+ os.kill(pid, 0)
110
+ os.kill(pid, signal.SIGKILL)
111
+ except ProcessLookupError:
112
+ muertos += 1
113
+ except PermissionError:
114
+ pass
115
+ return muertos
116
+
117
+
118
+ def _argumentos():
119
+ ap = argparse.ArgumentParser(add_help=False)
120
+ ap.add_argument('ciudad', nargs='?', default='',
121
+ help='close only this city (name or path); empty closes all')
122
+ ap.add_argument('--dry-run', action='store_true')
123
+ ap.add_argument('-h', '--help', action='store_true')
124
+ return ap.parse_args()
125
+
126
+
127
+ def _objetivo(nombre):
128
+ if not nombre:
129
+ return ''
130
+ solo = cities.resuelve(nombre)
131
+ if solo:
132
+ print(f' Only the city at {solo} — the rest keep working.')
133
+ return solo
134
+ conocidas = ', '.join(c['nombre'] for c in cities.lista()) or 'none'
135
+ raise ValueError(f'No city called {nombre!r}. Known here: {conocidas}.')
136
+
137
+
138
+ def _recursos(solo):
139
+ ciudades = [solo] if solo else [c['ruta'] for c in cities.lista()]
140
+ procesos = [(ciudad, proceso) for ciudad in ciudades
141
+ for proceso in runtime_processes.procesos(ciudad)]
142
+ return {
143
+ 'sesiones': sesiones_de_la_ciudad(solo),
144
+ # The hall covers all cities, so a scoped exit leaves it alive.
145
+ 'webs': [] if solo else servidores(),
146
+ 'mapas': mapas(solo),
147
+ 'ciudades': ciudades,
148
+ 'bus': procesos,
149
+ }
150
+
151
+
152
+ def _vacio(recursos):
153
+ return not any(recursos[k] for k in ('sesiones', 'webs', 'mapas', 'bus'))
154
+
155
+
156
+ def _describe(recursos, dry_run):
157
+ verbo = 'Would close' if dry_run else 'Closing'
158
+ for sesion_ in recursos['sesiones']:
159
+ ventanas = _corre(['tmux', 'list-windows', '-t', sesion_]).count('\n')
160
+ print(f' {verbo}: tmux session `{sesion_}` — {ventanas} windows, '
161
+ f'every agent inside included')
162
+ webs = recursos['webs']
163
+ if webs:
164
+ print(f' {verbo}: the town hall server ({len(webs)} process'
165
+ f'{"es" if len(webs) > 1 else ""})')
166
+ dibujos = recursos['mapas']
167
+ if dibujos:
168
+ print(f' {verbo}: the map ({len(dibujos)} process'
169
+ f'{"es" if len(dibujos) > 1 else ""} holding the local database)')
170
+ for _, proceso in recursos['bus']:
171
+ print(f' {verbo}: {proceso["label"]} (pid {proceso["pid"]})')
172
+
173
+
174
+ def _cierra_sesiones(sesiones):
175
+ """Close our current tmux session last so this command can finish."""
176
+ propia = os.environ.get('TMUX', '')
177
+ actual = _corre(['tmux', 'display-message', '-p', '#{session_name}']).strip()
178
+ ultimas = []
179
+ for sesion_ in sesiones:
180
+ if propia and actual == sesion_:
181
+ ultimas.append(sesion_)
182
+ else:
183
+ subprocess.run(['tmux', 'kill-session', '-t', sesion_], capture_output=True)
184
+ return ultimas
185
+
186
+
187
+ def _cierra(recursos):
188
+ # Sessions first: SIGHUP lets every runtime exit on its own terms.
189
+ ultimas = _cierra_sesiones(recursos['sesiones'])
190
+ mata(recursos['webs'])
191
+ mata(recursos['mapas'])
192
+ mata([proceso['pid'] for _, proceso in recursos['bus']])
193
+ # A gateway can notice the hub dying in the narrow interval before tmux's
194
+ # SIGHUP reaches it and recreate a hub. Re-scan exact --data argv after the
195
+ # session is gone; this also catches old hubs whose marker was overwritten.
196
+ for _ in range(3):
197
+ restantes = [proceso['pid'] for ciudad in recursos['ciudades']
198
+ for proceso in runtime_processes.procesos(ciudad)]
199
+ if not restantes:
200
+ break
201
+ mata(sorted(set(restantes)))
202
+ for ciudad in recursos['ciudades']:
203
+ runtime_processes.limpia_marcas(ciudad)
204
+ for sesion_ in ultimas:
205
+ subprocess.run(['tmux', 'kill-session', '-t', sesion_], capture_output=True)
206
+
207
+
208
+ def main():
209
+ a = _argumentos()
210
+ if a.help:
211
+ print(__doc__)
212
+ return 0
213
+
214
+ try:
215
+ solo = _objetivo(a.ciudad)
216
+ except ValueError as error:
217
+ print(f' {error}', file=sys.stderr)
218
+ return 1
219
+ recursos = _recursos(solo)
220
+ if _vacio(recursos):
221
+ print(' Nothing of the city is running. The machine is yours.')
222
+ return 0
223
+ _describe(recursos, a.dry_run)
224
+ if a.dry_run:
225
+ return 0
226
+ _cierra(recursos)
227
+ print(' Done. `agents-city seat` brings it all back.')
228
+ return 0
229
+
230
+
231
+ if __name__ == '__main__':
232
+ sys.exit(main())