agents-city 0.3.0-beta.21

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 (440) 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 +2021 -0
  5. package/README.md +1999 -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 +116 -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/exit +5 -0
  28. package/bin/hall +73 -0
  29. package/bin/hall.html +426 -0
  30. package/bin/logs +3 -0
  31. package/bin/report.py +14 -0
  32. package/bin/reset +3 -0
  33. package/bin/road +3 -0
  34. package/bin/seat +10 -0
  35. package/bin/serve.py +1428 -0
  36. package/bin/setup.py +753 -0
  37. package/bin/skills +3 -0
  38. package/bin/test +131 -0
  39. package/bin/test-adapter.py +183 -0
  40. package/bin/test-admision.py +99 -0
  41. package/bin/test-avatar.py +58 -0
  42. package/bin/test-benchmark.py +40 -0
  43. package/bin/test-broker.py +334 -0
  44. package/bin/test-cage.py +206 -0
  45. package/bin/test-card.py +402 -0
  46. package/bin/test-channel.py +379 -0
  47. package/bin/test-cities.py +453 -0
  48. package/bin/test-claude-runtime.py +252 -0
  49. package/bin/test-committee.py +340 -0
  50. package/bin/test-contracts.py +544 -0
  51. package/bin/test-crecimiento.py +164 -0
  52. package/bin/test-demo.py +308 -0
  53. package/bin/test-doctor.py +106 -0
  54. package/bin/test-domains.py +151 -0
  55. package/bin/test-evidencia.py +50 -0
  56. package/bin/test-exit.py +128 -0
  57. package/bin/test-hall-protocol.py +93 -0
  58. package/bin/test-launch.py +169 -0
  59. package/bin/test-live-feed.py +508 -0
  60. package/bin/test-pairing.py +99 -0
  61. package/bin/test-parcels.py +283 -0
  62. package/bin/test-runtime-failures.py +252 -0
  63. package/bin/test-runtime-ui.py +488 -0
  64. package/bin/test-runtime.py +268 -0
  65. package/bin/test-rutas.py +80 -0
  66. package/bin/test-seat.py +1386 -0
  67. package/bin/test-security.py +150 -0
  68. package/bin/test-serve.py +1057 -0
  69. package/bin/test-stress.py +64 -0
  70. package/bin/test-widgets.py +143 -0
  71. package/bin/test-workspace.py +188 -0
  72. package/bin/testlib.py +95 -0
  73. package/bin/tokens.py +14 -0
  74. package/bus/scripts/deploy.sh +60 -0
  75. package/bus/scripts/mint-token.sh +68 -0
  76. package/bus/scripts/setup-dev.sh +79 -0
  77. package/bus/scripts/test-channel.ts +8 -0
  78. package/bus/scripts/test-hub.ts +266 -0
  79. package/bus/scripts/test-local.sh +40 -0
  80. package/bus/scripts/test-queue.ts +132 -0
  81. package/bus/worker/package-lock.json +1971 -0
  82. package/bus/worker/package.json +17 -0
  83. package/bus/worker/src/index.ts +485 -0
  84. package/bus/worker/tsconfig.json +13 -0
  85. package/bus/worker/wrangler.toml +32 -0
  86. package/city/oven/README.md +59 -0
  87. package/city/oven/collect.py +43 -0
  88. package/city/oven/oven.html +4 -0
  89. package/city/scripts/history.py +120 -0
  90. package/city/scripts/seed.py +442 -0
  91. package/city/web/assets/modelos/License.txt +28 -0
  92. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  93. package/city/web/assets/modelos/building-a.glb +0 -0
  94. package/city/web/assets/modelos/building-b.glb +0 -0
  95. package/city/web/assets/modelos/building-c.glb +0 -0
  96. package/city/web/assets/modelos/building-d.glb +0 -0
  97. package/city/web/assets/modelos/building-e.glb +0 -0
  98. package/city/web/assets/modelos/building-f.glb +0 -0
  99. package/city/web/assets/modelos/building-g.glb +0 -0
  100. package/city/web/assets/modelos/building-h.glb +0 -0
  101. package/city/web/assets/sprites/building-a.png +0 -0
  102. package/city/web/assets/sprites/building-b.png +0 -0
  103. package/city/web/assets/sprites/building-c.png +0 -0
  104. package/city/web/assets/sprites/building-d.png +0 -0
  105. package/city/web/assets/sprites/building-e.png +0 -0
  106. package/city/web/assets/sprites/building-f.png +0 -0
  107. package/city/web/assets/sprites/building-g.png +0 -0
  108. package/city/web/assets/sprites/building-h.png +0 -0
  109. package/city/web/assets/sprites/building-i.png +0 -0
  110. package/city/web/assets/sprites/building-j.png +0 -0
  111. package/city/web/assets/sprites/building-k.png +0 -0
  112. package/city/web/assets/sprites/building-l.png +0 -0
  113. package/city/web/assets/sprites/building-m.png +0 -0
  114. package/city/web/assets/sprites/building-n.png +0 -0
  115. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  116. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  117. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  118. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  119. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  120. package/city/web/assets/sprites/catalogo.json +47 -0
  121. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  122. package/city/web/assets/sprites/detail-awning.png +0 -0
  123. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  124. package/city/web/assets/sprites/detail-overhang.png +0 -0
  125. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  126. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  127. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  128. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  129. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  130. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  131. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  132. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  133. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  143. package/city/web/assets/sprites/medidas.json +248 -0
  144. package/city/web/dist/city.js +939 -0
  145. package/city/web/dist/index.html +294 -0
  146. package/city/web/dist/modelos/License.txt +28 -0
  147. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  148. package/city/web/dist/modelos/building-a.glb +0 -0
  149. package/city/web/dist/modelos/building-b.glb +0 -0
  150. package/city/web/dist/modelos/building-c.glb +0 -0
  151. package/city/web/dist/modelos/building-d.glb +0 -0
  152. package/city/web/dist/modelos/building-e.glb +0 -0
  153. package/city/web/dist/modelos/building-f.glb +0 -0
  154. package/city/web/dist/modelos/building-g.glb +0 -0
  155. package/city/web/dist/modelos/building-h.glb +0 -0
  156. package/city/web/dist/sprites/building-a.png +0 -0
  157. package/city/web/dist/sprites/building-b.png +0 -0
  158. package/city/web/dist/sprites/building-c.png +0 -0
  159. package/city/web/dist/sprites/building-d.png +0 -0
  160. package/city/web/dist/sprites/building-e.png +0 -0
  161. package/city/web/dist/sprites/building-f.png +0 -0
  162. package/city/web/dist/sprites/building-g.png +0 -0
  163. package/city/web/dist/sprites/building-h.png +0 -0
  164. package/city/web/dist/sprites/building-i.png +0 -0
  165. package/city/web/dist/sprites/building-j.png +0 -0
  166. package/city/web/dist/sprites/building-k.png +0 -0
  167. package/city/web/dist/sprites/building-l.png +0 -0
  168. package/city/web/dist/sprites/building-m.png +0 -0
  169. package/city/web/dist/sprites/building-n.png +0 -0
  170. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  171. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  172. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  173. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  174. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  175. package/city/web/dist/sprites/catalogo.json +47 -0
  176. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  177. package/city/web/dist/sprites/detail-awning.png +0 -0
  178. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  179. package/city/web/dist/sprites/detail-overhang.png +0 -0
  180. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  181. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  182. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  183. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  184. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  185. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  186. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  187. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  188. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  198. package/city/web/dist/sprites/medidas.json +248 -0
  199. package/city/web/dist-hall/hall.js +264 -0
  200. package/city/web/index.html +294 -0
  201. package/city/web/package.json +18 -0
  202. package/city/web/sella.py +21 -0
  203. package/city/web/src/activity-actors.ts +47 -0
  204. package/city/web/src/activity.ts +172 -0
  205. package/city/web/src/ayuntamiento.ts +580 -0
  206. package/city/web/src/draw.ts +640 -0
  207. package/city/web/src/game-speech.ts +79 -0
  208. package/city/web/src/hall.ts +1867 -0
  209. package/city/web/src/main.ts +2225 -0
  210. package/city/web/src/oven.ts +124 -0
  211. package/city/web/src/people.ts +244 -0
  212. package/city/web/src/presencia.ts +193 -0
  213. package/city/web/src/puertas.ts +180 -0
  214. package/city/web/src/repo-roles.ts +59 -0
  215. package/city/worker/package.json +16 -0
  216. package/city/worker/schema.sql +117 -0
  217. package/city/worker/src/index.ts +702 -0
  218. package/city/worker/src/square.ts +218 -0
  219. package/city/worker/wrangler.toml +58 -0
  220. package/demo/ada.md +50 -0
  221. package/demo/bruno.md +62 -0
  222. package/demo/camila.md +56 -0
  223. package/demo/city/ada.md +40 -0
  224. package/demo/city/city.yml +8 -0
  225. package/demo/city/parcels.yml +15 -0
  226. package/demo/city/roads.json +4 -0
  227. package/demo/city/units.yml +10 -0
  228. package/demo/clinica/city.yml +8 -0
  229. package/demo/clinica/parcels.yml +15 -0
  230. package/demo/clinica/roads.json +4 -0
  231. package/demo/clinica/units.yml +9 -0
  232. package/demo/clinica/vera.md +46 -0
  233. package/demo/dante.md +54 -0
  234. package/demo/despacho/city.yml +8 -0
  235. package/demo/despacho/marta.md +46 -0
  236. package/demo/despacho/parcels.yml +15 -0
  237. package/demo/despacho/roads.json +4 -0
  238. package/demo/despacho/units.yml +9 -0
  239. package/demo/elsa.md +50 -0
  240. package/demo/farid.md +52 -0
  241. package/demo/greta.md +50 -0
  242. package/demo/hugo.md +49 -0
  243. package/demo/iris.md +49 -0
  244. package/demo/jonas.md +51 -0
  245. package/demo/kira.md +51 -0
  246. package/demo/luca.md +49 -0
  247. package/demo/parcels.yml +92 -0
  248. package/demo/seed.py +20 -0
  249. package/demo/show.py +161 -0
  250. package/demo/stories.py +824 -0
  251. package/demo/units.yml +33 -0
  252. package/docs/agents-first.md +99 -0
  253. package/docs/glossary.md +55 -0
  254. package/docs/map-live-layers.md +73 -0
  255. package/docs/security.md +231 -0
  256. package/docs/self-host.md +151 -0
  257. package/docs/testing.md +205 -0
  258. package/package.json +73 -0
  259. package/plugin/.claude-plugin/plugin.json +62 -0
  260. package/plugin/.mcp.json +15 -0
  261. package/plugin/channel/activity-cli.ts +16 -0
  262. package/plugin/channel/adapter-prompts.ts +113 -0
  263. package/plugin/channel/adapter.js +4292 -0
  264. package/plugin/channel/adapter.ts +133 -0
  265. package/plugin/channel/bus.js +20188 -0
  266. package/plugin/channel/bus.ts +131 -0
  267. package/plugin/channel/city-config.ts +125 -0
  268. package/plugin/channel/claude-channel.ts +58 -0
  269. package/plugin/channel/cli-args.ts +75 -0
  270. package/plugin/channel/client.js +4200 -0
  271. package/plugin/channel/client.ts +38 -0
  272. package/plugin/channel/committee/activity.ts +256 -0
  273. package/plugin/channel/committee/collection.ts +154 -0
  274. package/plugin/channel/committee/decision.ts +175 -0
  275. package/plugin/channel/committee/floor.ts +135 -0
  276. package/plugin/channel/committee/guards.ts +30 -0
  277. package/plugin/channel/committee/history.ts +38 -0
  278. package/plugin/channel/committee/render.ts +102 -0
  279. package/plugin/channel/committee/service.ts +72 -0
  280. package/plugin/channel/committee/storage.ts +92 -0
  281. package/plugin/channel/committee/types.ts +163 -0
  282. package/plugin/channel/committee/view.ts +63 -0
  283. package/plugin/channel/committee-cli.ts +109 -0
  284. package/plugin/channel/delivery-metrics.ts +61 -0
  285. package/plugin/channel/delivery-queue.ts +118 -0
  286. package/plugin/channel/hub/activity-controller.ts +72 -0
  287. package/plugin/channel/hub/activity-feed.ts +169 -0
  288. package/plugin/channel/hub/committee-controller.ts +60 -0
  289. package/plugin/channel/hub/connections.ts +48 -0
  290. package/plugin/channel/hub/diagnostics.ts +67 -0
  291. package/plugin/channel/hub/envelope-validity.ts +109 -0
  292. package/plugin/channel/hub/envelopes.ts +66 -0
  293. package/plugin/channel/hub/lifecycle.ts +91 -0
  294. package/plugin/channel/hub/local-roads.ts +89 -0
  295. package/plugin/channel/hub/remote-roads.ts +165 -0
  296. package/plugin/channel/hub/road-controller.ts +99 -0
  297. package/plugin/channel/hub-client.ts +149 -0
  298. package/plugin/channel/local-hub.js +5995 -0
  299. package/plugin/channel/local-hub.ts +349 -0
  300. package/plugin/channel/map-reporter.ts +48 -0
  301. package/plugin/channel/package-lock.json +1731 -0
  302. package/plugin/channel/package.json +19 -0
  303. package/plugin/channel/protocol.ts +98 -0
  304. package/plugin/channel/road-cli.ts +18 -0
  305. package/plugin/channel/run.sh +15 -0
  306. package/plugin/channel/runtime/claude.ts +458 -0
  307. package/plugin/channel/runtime/codex-config.ts +77 -0
  308. package/plugin/channel/runtime/codex.ts +669 -0
  309. package/plugin/channel/runtime/command.ts +62 -0
  310. package/plugin/channel/runtime/factory.ts +15 -0
  311. package/plugin/channel/runtime/json-rpc.ts +154 -0
  312. package/plugin/channel/runtime/kimi.ts +277 -0
  313. package/plugin/channel/runtime/opencode.ts +248 -0
  314. package/plugin/channel/runtime/process.ts +84 -0
  315. package/plugin/channel/runtime/types.ts +45 -0
  316. package/plugin/channel/runtime-files.ts +73 -0
  317. package/plugin/channel/runtime-gateway.js +6396 -0
  318. package/plugin/channel/runtime-gateway.ts +358 -0
  319. package/plugin/channel/runtime-metrics.ts +52 -0
  320. package/plugin/channel/runtime-subscription.ts +180 -0
  321. package/plugin/channel/terminal-delivery.ts +131 -0
  322. package/plugin/channel/untrusted.ts +60 -0
  323. package/plugin/commands/committee.md +31 -0
  324. package/plugin/commands/exit.md +32 -0
  325. package/plugin/commands/goals.md +20 -0
  326. package/plugin/commands/join.md +18 -0
  327. package/plugin/commands/notice.md +20 -0
  328. package/plugin/commands/propose.md +19 -0
  329. package/plugin/commands/round.md +17 -0
  330. package/plugin/commands/session.md +22 -0
  331. package/plugin/commands/settings.md +25 -0
  332. package/plugin/commands/setup.md +21 -0
  333. package/plugin/commands/team.md +23 -0
  334. package/plugin/domains/custom.md +27 -0
  335. package/plugin/domains/finance.md +31 -0
  336. package/plugin/domains/healthcare.md +34 -0
  337. package/plugin/domains/legal.md +31 -0
  338. package/plugin/domains/marketing.md +33 -0
  339. package/plugin/domains/operations.md +31 -0
  340. package/plugin/domains/research.md +32 -0
  341. package/plugin/domains/sales.md +30 -0
  342. package/plugin/domains/software.md +37 -0
  343. package/plugin/hooks/activity.sh +6 -0
  344. package/plugin/hooks/digging.sh +68 -0
  345. package/plugin/hooks/growth.sh +42 -0
  346. package/plugin/hooks/hooks.json +106 -0
  347. package/plugin/hooks/notice-on-pr.sh +35 -0
  348. package/plugin/hooks/notice-on-stop.sh +80 -0
  349. package/plugin/hooks/notice-pending.sh +58 -0
  350. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  351. package/plugin/hooks/tokens.sh +47 -0
  352. package/plugin/roles/examples/account-executive.md +25 -0
  353. package/plugin/roles/examples/ai-manager.md +34 -0
  354. package/plugin/roles/examples/associate.md +24 -0
  355. package/plugin/roles/examples/brand-lead.md +20 -0
  356. package/plugin/roles/examples/cfo.md +20 -0
  357. package/plugin/roles/examples/city-lead.md +25 -0
  358. package/plugin/roles/examples/clinical-director.md +26 -0
  359. package/plugin/roles/examples/clinical-ops.md +25 -0
  360. package/plugin/roles/examples/clinician.md +25 -0
  361. package/plugin/roles/examples/compliance.md +27 -0
  362. package/plugin/roles/examples/content.md +24 -0
  363. package/plugin/roles/examples/controller.md +27 -0
  364. package/plugin/roles/examples/cpto.md +41 -0
  365. package/plugin/roles/examples/customer-success.md +25 -0
  366. package/plugin/roles/examples/data-engineer.md +37 -0
  367. package/plugin/roles/examples/data.md +60 -0
  368. package/plugin/roles/examples/dev.md +37 -0
  369. package/plugin/roles/examples/devops.md +39 -0
  370. package/plugin/roles/examples/enablement.md +24 -0
  371. package/plugin/roles/examples/ethics.md +24 -0
  372. package/plugin/roles/examples/fin-analytics.md +27 -0
  373. package/plugin/roles/examples/health-compliance.md +25 -0
  374. package/plugin/roles/examples/health-data.md +26 -0
  375. package/plugin/roles/examples/knowledge.md +25 -0
  376. package/plugin/roles/examples/lifecycle.md +26 -0
  377. package/plugin/roles/examples/llm-engineer.md +34 -0
  378. package/plugin/roles/examples/managing-partner.md +20 -0
  379. package/plugin/roles/examples/methods.md +23 -0
  380. package/plugin/roles/examples/operations-lead.md +23 -0
  381. package/plugin/roles/examples/ops.md +24 -0
  382. package/plugin/roles/examples/patient-safety.md +26 -0
  383. package/plugin/roles/examples/performance.md +27 -0
  384. package/plugin/roles/examples/po.md +36 -0
  385. package/plugin/roles/examples/process-owner.md +23 -0
  386. package/plugin/roles/examples/product-design.md +39 -0
  387. package/plugin/roles/examples/program-manager.md +24 -0
  388. package/plugin/roles/examples/quality.md +25 -0
  389. package/plugin/roles/examples/research-director.md +24 -0
  390. package/plugin/roles/examples/research-ops.md +25 -0
  391. package/plugin/roles/examples/researcher.md +25 -0
  392. package/plugin/roles/examples/revenue-lead.md +25 -0
  393. package/plugin/roles/examples/revops.md +26 -0
  394. package/plugin/roles/examples/seo.md +27 -0
  395. package/plugin/roles/examples/specialist.md +24 -0
  396. package/plugin/scripts/admision.py +165 -0
  397. package/plugin/scripts/apaga.py +232 -0
  398. package/plugin/scripts/avatar.py +211 -0
  399. package/plugin/scripts/broker.py +531 -0
  400. package/plugin/scripts/cage.py +252 -0
  401. package/plugin/scripts/capabilities.py +197 -0
  402. package/plugin/scripts/card.py +312 -0
  403. package/plugin/scripts/cities.py +520 -0
  404. package/plugin/scripts/city-env.sh +90 -0
  405. package/plugin/scripts/city-runtime.sh +87 -0
  406. package/plugin/scripts/city-session.sh +526 -0
  407. package/plugin/scripts/city_env.py +67 -0
  408. package/plugin/scripts/crecimiento.py +140 -0
  409. package/plugin/scripts/deliberations.py +48 -0
  410. package/plugin/scripts/doctor.py +172 -0
  411. package/plugin/scripts/domains.py +201 -0
  412. package/plugin/scripts/evidencia.py +64 -0
  413. package/plugin/scripts/find-repos.sh +111 -0
  414. package/plugin/scripts/gh.py +159 -0
  415. package/plugin/scripts/hall_protocol.py +147 -0
  416. package/plugin/scripts/hook_activity.py +139 -0
  417. package/plugin/scripts/launch.py +94 -0
  418. package/plugin/scripts/logs.py +98 -0
  419. package/plugin/scripts/pairing.py +185 -0
  420. package/plugin/scripts/parcels.py +132 -0
  421. package/plugin/scripts/read-card.py +51 -0
  422. package/plugin/scripts/report.py +201 -0
  423. package/plugin/scripts/reset.py +180 -0
  424. package/plugin/scripts/roads.py +230 -0
  425. package/plugin/scripts/roles.py +274 -0
  426. package/plugin/scripts/runtime_log.py +69 -0
  427. package/plugin/scripts/runtime_processes.py +132 -0
  428. package/plugin/scripts/rutas.py +83 -0
  429. package/plugin/scripts/seat.py +1349 -0
  430. package/plugin/scripts/tokens.py +194 -0
  431. package/plugin/scripts/trust-repos.py +59 -0
  432. package/plugin/scripts/ui.py +280 -0
  433. package/plugin/scripts/units.py +80 -0
  434. package/plugin/scripts/workspace.py +364 -0
  435. package/plugin/skills/city/SKILL.md +191 -0
  436. package/templates/blank.md +39 -0
  437. package/templates/finance.md +44 -0
  438. package/templates/legal.md +45 -0
  439. package/templates/marketing.md +48 -0
  440. package/templates/product.md +45 -0
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env python3
2
+ """The local catalogue and identity of Agents City cities.
3
+
4
+ A user owns any number of cities. A managed city lives at::
5
+
6
+ ~/.agents-city/<user>/<city>/
7
+
8
+ The application root is a container; it is never a city itself. Each city has
9
+ one stable id, one immutable address slug, one seat/card, its repos, and its own
10
+ roads. The same city can be moved without changing its identity.
11
+
12
+ Old releases wrote the first city directly into ``~/.agents-city``. The first
13
+ v2 command migrates that flat folder to ``<user>/home`` after making a complete
14
+ copy under ``~/.agents-city/.backups``. State and runtime directories stay at
15
+ the application root.
16
+
17
+ This module owns every path and naming decision. Python callers import it; bash
18
+ callers use the CLI, so there is no second implementation to drift.
19
+ """
20
+ import hashlib
21
+ import os
22
+ import re
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ import time
27
+ import uuid
28
+
29
+
30
+ # Kept as a public variable because tests and embedded callers redirect it. New
31
+ # entries are ``user<TAB>path``; legacy one-column entries remain readable.
32
+ REGISTRO = os.path.expanduser('~/.config/agents-city/cities')
33
+ RESERVADOS = {'state', '.state', '.runtime', '.backups', '.layout-v2'}
34
+ MARCADORES = ('city.yml', 'units.yml', 'parcels.yml')
35
+
36
+
37
+ def _real(ruta):
38
+ return os.path.realpath(os.path.expanduser(ruta))
39
+
40
+
41
+ def _slug(valor, defecto='city'):
42
+ limpio = re.sub(r'[^a-z0-9-]+', '-', str(valor).lower()).strip('-')
43
+ return limpio or defecto
44
+
45
+
46
+ def usuario_actual():
47
+ """The local owner. Explicit env wins; git is the useful default."""
48
+ if os.environ.get('AGENTS_CITY_USER'):
49
+ return _slug(os.environ['AGENTS_CITY_USER'], 'me')
50
+ try:
51
+ correo = subprocess.run(
52
+ ['git', 'config', 'user.email'], capture_output=True, text=True,
53
+ timeout=5).stdout.strip()
54
+ except (OSError, subprocess.TimeoutExpired):
55
+ correo = ''
56
+ local = correo.split('@', 1)[0].lower()
57
+ # GitHub's noreply form: 12345678+alice@users.noreply.github.com.
58
+ if '+' in local and local.split('+', 1)[0].isdigit():
59
+ local = local.split('+', 1)[1]
60
+ return _slug(local or os.environ.get('USER', 'me'), 'me')
61
+
62
+
63
+ def raiz():
64
+ return _real(os.environ.get('AGENTS_CITY_HOME', '~/.agents-city'))
65
+
66
+
67
+ def carpeta_usuario(usuario=''):
68
+ return os.path.join(raiz(), _slug(usuario or usuario_actual(), 'me'))
69
+
70
+
71
+ def _lee_clave(datos, clave, defecto=''):
72
+ try:
73
+ texto = open(os.path.join(datos, 'city.yml'), encoding='utf-8').read()
74
+ except OSError:
75
+ return defecto
76
+ m = re.search(rf'^{re.escape(clave)}:[ \t]*(.+)$', texto, re.M)
77
+ return m.group(1).strip().strip('"\'') if m else defecto
78
+
79
+
80
+ def lee_clave(datos, clave, defecto=''):
81
+ """Read one scalar from city.yml through the shared tiny parser."""
82
+ return _lee_clave(datos, clave, defecto)
83
+
84
+
85
+ def _atomico(ruta, texto):
86
+ os.makedirs(os.path.dirname(ruta), exist_ok=True)
87
+ temporal = ruta + f'.tmp-{os.getpid()}'
88
+ with open(temporal, 'w', encoding='utf-8') as f:
89
+ f.write(texto)
90
+ os.replace(temporal, ruta)
91
+
92
+
93
+ def escribe_atomico(ruta, texto):
94
+ """Write one of the city's small text files atomically."""
95
+ _atomico(ruta, texto)
96
+
97
+
98
+ def pon_clave(datos, clave, valor):
99
+ """Upsert one scalar in city.yml, atomically, preserving everything else.
100
+
101
+ The city's little truths (seat_yolo, grow_command…) live here rather than
102
+ scattered across shells: one reader, one writer, no drift.
103
+ """
104
+ ruta = os.path.join(_real(datos), 'city.yml')
105
+ try:
106
+ texto = open(ruta, encoding='utf-8').read()
107
+ except OSError:
108
+ texto = '# Agents City identity. Plain text, safe to version.\n'
109
+ linea = f'{clave}: {valor}'
110
+ patron = re.compile(rf'^{re.escape(clave)}:.*$', re.M)
111
+ if patron.search(texto):
112
+ texto = patron.sub(linea, texto)
113
+ else:
114
+ if texto and not texto.endswith('\n'):
115
+ texto += '\n'
116
+ texto += linea + '\n'
117
+ _atomico(ruta, texto)
118
+
119
+
120
+ def es_ciudad(ruta):
121
+ """A folder with city metadata, a seat card, or the old map files."""
122
+ if not os.path.isdir(ruta):
123
+ return False
124
+ if any(os.path.exists(os.path.join(ruta, f)) for f in MARCADORES):
125
+ return True
126
+ try:
127
+ return any(f.endswith('.md') and not f.startswith('README')
128
+ for f in os.listdir(ruta))
129
+ except OSError:
130
+ return False
131
+
132
+
133
+ def gestionada(datos, usuario=''):
134
+ """Whether ``datos`` is one of this user's managed city folders."""
135
+ return os.path.dirname(_real(datos)) == _real(carpeta_usuario(usuario))
136
+
137
+
138
+ def asegura_metadata(datos, usuario='', nombre_ciudad='', ident=''):
139
+ """Give a city a stable id/address without overwriting existing metadata."""
140
+ usuario = _slug(usuario or usuario_actual(), 'me')
141
+ datos = _real(datos)
142
+ os.makedirs(datos, exist_ok=True)
143
+ ruta = os.path.join(datos, 'city.yml')
144
+ try:
145
+ texto = open(ruta, encoding='utf-8').read()
146
+ except OSError:
147
+ texto = '# Agents City identity. Plain text, safe to version.\n'
148
+ existente_id = _lee_clave(datos, 'id')
149
+ nombre_ciudad = (nombre_ciudad or _lee_clave(datos, 'name')
150
+ or os.path.basename(datos))
151
+ nombre_ciudad = ' '.join(str(nombre_ciudad).split()) or 'city'
152
+ valores = {
153
+ # An identity already on disk is immutable. ``ident`` is only a seed for
154
+ # a new city (not authority to rewrite an existing one).
155
+ 'id': existente_id or ident or f'city_{uuid.uuid4().hex}',
156
+ 'name': nombre_ciudad,
157
+ 'slug': _lee_clave(datos, 'slug') or _slug(nombre_ciudad),
158
+ 'owner': _lee_clave(datos, 'owner') or usuario,
159
+ 'layout': _lee_clave(datos, 'layout') or 'personal-v2',
160
+ }
161
+ faltan = [f'{k}: {v}' for k, v in valores.items()
162
+ if not re.search(rf'^{re.escape(k)}:', texto, re.M)]
163
+ if faltan:
164
+ if texto and not texto.endswith('\n'):
165
+ texto += '\n'
166
+ texto += '\n'.join(faltan) + '\n'
167
+ _atomico(ruta, texto)
168
+ caminos = os.path.join(datos, 'roads.json')
169
+ if not os.path.exists(caminos):
170
+ _atomico(caminos, '{\n "version": 1,\n "roads": []\n}\n')
171
+ return valores
172
+
173
+
174
+ def _legacy_plana():
175
+ base = raiz()
176
+ if not os.path.isdir(base):
177
+ return False
178
+ return (any(os.path.exists(os.path.join(base, f)) for f in MARCADORES)
179
+ or any(f.endswith('.md') and not f.startswith('README')
180
+ for f in os.listdir(base)))
181
+
182
+
183
+ def _respaldo_libre(base, prefijo):
184
+ """Return a timestamped backup path that cannot collide in one second."""
185
+ sello = time.strftime('%Y%m%d-%H%M%S')
186
+ candidato = os.path.join(base, f'{prefijo}-{sello}')
187
+ n = 2
188
+ while os.path.exists(candidato):
189
+ candidato = os.path.join(base, f'{prefijo}-{sello}-{n}')
190
+ n += 1
191
+ return candidato, sello
192
+
193
+
194
+ def migra_legacy(usuario='', anunciar=False):
195
+ """Move the old flat city to ``<user>/home``, after a full backup.
196
+
197
+ Returns the destination, or ``''`` when there was nothing to migrate. The
198
+ operation is idempotent: once the flat markers are gone it is a no-op.
199
+ """
200
+ usuario = _slug(usuario or usuario_actual(), 'me')
201
+ base = raiz()
202
+ if not _legacy_plana():
203
+ return ''
204
+ # A v2 user folder can already exist after an interrupted first run. It is a
205
+ # container, not part of the old flat city; moving it below itself would be
206
+ # recursive and could swallow another city.
207
+ entradas = [n for n in os.listdir(base)
208
+ if n not in RESERVADOS and n != usuario]
209
+ # A previous partial migration must never overwrite its destination.
210
+ destino = os.path.join(carpeta_usuario(usuario), 'home')
211
+ if os.path.exists(destino) and os.listdir(destino):
212
+ return ''
213
+
214
+ copia, sello = _respaldo_libre(os.path.join(base, '.backups'), 'legacy-flat')
215
+ os.makedirs(copia, exist_ok=False)
216
+ for nombre_ in entradas:
217
+ origen = os.path.join(base, nombre_)
218
+ respaldo = os.path.join(copia, nombre_)
219
+ if os.path.isdir(origen) and not os.path.islink(origen):
220
+ shutil.copytree(origen, respaldo, symlinks=True)
221
+ else:
222
+ shutil.copy2(origen, respaldo, follow_symlinks=False)
223
+
224
+ os.makedirs(destino, exist_ok=True)
225
+ for nombre_ in entradas:
226
+ shutil.move(os.path.join(base, nombre_), os.path.join(destino, nombre_))
227
+ asegura_metadata(destino, usuario, 'home')
228
+ _atomico(os.path.join(base, '.layout-v2'),
229
+ f'migrated: {sello}\nuser: {usuario}\ncity: home\nbackup: {copia}\n')
230
+ if anunciar:
231
+ corto_d = destino.replace(os.path.expanduser('~'), '~')
232
+ corto_b = copia.replace(os.path.expanduser('~'), '~')
233
+ print(f'\n Migrated the old flat city to {corto_d}')
234
+ print(f' Recovery copy: {corto_b}\n')
235
+ return destino
236
+
237
+
238
+ def crea(usuario, nombre_ciudad, usar=True):
239
+ """Create a managed city. Existing cities are returned untouched."""
240
+ usuario = _slug(usuario or usuario_actual(), 'me')
241
+ slug_ = _slug(nombre_ciudad)
242
+ destino = os.path.join(carpeta_usuario(usuario), slug_)
243
+ if os.path.exists(destino) and os.listdir(destino) and not es_ciudad(destino):
244
+ raise ValueError(f'{destino} exists and is not a city')
245
+ asegura_metadata(destino, usuario, str(nombre_ciudad).strip() or slug_)
246
+ if usar:
247
+ selecciona(usuario, destino)
248
+ return _real(destino)
249
+
250
+
251
+ def _registro_para(usuario):
252
+ try:
253
+ lineas = [l.strip() for l in open(REGISTRO, encoding='utf-8') if l.strip()]
254
+ except OSError:
255
+ return []
256
+ fuera = []
257
+ for linea in lineas:
258
+ if '\t' in linea:
259
+ dueno, ruta = linea.split('\t', 1)
260
+ if dueno != usuario:
261
+ continue
262
+ else:
263
+ ruta = linea # v1 registry: implicitly this user
264
+ fuera.append(ruta)
265
+ return fuera
266
+
267
+
268
+ def registra(datos, usuario=''):
269
+ """Remember an external city; managed cities are discovered from folders."""
270
+ usuario = _slug(usuario or usuario_actual(), 'me')
271
+ r = _real(datos)
272
+ owner = _slug(_lee_clave(r, 'owner'), '') if _lee_clave(r, 'owner') else ''
273
+ if gestionada(r, usuario) or not es_ciudad(r) or (owner and owner != usuario):
274
+ return
275
+ actuales = {_real(x) for x in _registro_para(usuario)}
276
+ if r in actuales:
277
+ return
278
+ os.makedirs(os.path.dirname(REGISTRO), exist_ok=True)
279
+ with open(REGISTRO, 'a', encoding='utf-8') as f:
280
+ f.write(f'{usuario}\t{r}\n')
281
+
282
+
283
+ def lista(usuario=''):
284
+ """Every city this user owns or has explicitly registered."""
285
+ usuario = _slug(usuario or usuario_actual(), 'me')
286
+ migra_legacy(usuario)
287
+ candidatas = []
288
+ propia = carpeta_usuario(usuario)
289
+ try:
290
+ candidatas += [os.path.join(propia, n) for n in sorted(os.listdir(propia))
291
+ if not n.startswith('.')]
292
+ except OSError:
293
+ pass
294
+ candidatas += _registro_para(usuario)
295
+ fuera, vistos = [], set()
296
+ for candidata in candidatas:
297
+ r = _real(candidata)
298
+ if r in vistos or not es_ciudad(r):
299
+ continue
300
+ owner_en_disco = _lee_clave(r, 'owner')
301
+ if owner_en_disco and _slug(owner_en_disco, 'me') != usuario:
302
+ continue
303
+ vistos.add(r)
304
+ meta = asegura_metadata(r, usuario) if gestionada(r, usuario) else {}
305
+ fuera.append({
306
+ 'ruta': r,
307
+ 'nombre': nombre(r),
308
+ 'slug': slug_ciudad(r),
309
+ 'id': meta.get('id') or identidad(r),
310
+ 'owner': meta.get('owner') or _lee_clave(r, 'owner', usuario),
311
+ 'managed': gestionada(r, usuario),
312
+ })
313
+ return fuera
314
+
315
+
316
+ def _fichero_actual(usuario):
317
+ return os.path.join(carpeta_usuario(usuario), '.current')
318
+
319
+
320
+ def selecciona(usuario, datos):
321
+ usuario = _slug(usuario or usuario_actual(), 'me')
322
+ datos = _real(datos)
323
+ os.makedirs(carpeta_usuario(usuario), exist_ok=True)
324
+ _atomico(_fichero_actual(usuario), datos + '\n')
325
+
326
+
327
+ def actual(usuario='', crear=True, migrar=True):
328
+ """The selected city. Explicit ``AGENTS_CITY_DATA`` always wins."""
329
+ explicita = os.environ.get('AGENTS_CITY_DATA')
330
+ if explicita and os.path.isdir(os.path.expanduser(explicita)):
331
+ return _real(explicita)
332
+ usuario = _slug(usuario or usuario_actual(), 'me')
333
+ if migrar:
334
+ migra_legacy(usuario)
335
+ try:
336
+ elegida = open(_fichero_actual(usuario), encoding='utf-8').read().strip()
337
+ except OSError:
338
+ elegida = ''
339
+ if elegida and es_ciudad(elegida):
340
+ owner = _lee_clave(elegida, 'owner')
341
+ if not owner or _slug(owner, 'me') == usuario:
342
+ return _real(elegida)
343
+ ciudades = lista(usuario)
344
+ home = next((c for c in ciudades if c['slug'] == 'home'), None)
345
+ if home:
346
+ selecciona(usuario, home['ruta'])
347
+ return home['ruta']
348
+ if ciudades:
349
+ selecciona(usuario, ciudades[0]['ruta'])
350
+ return ciudades[0]['ruta']
351
+ return crea(usuario, 'home') if crear else ''
352
+
353
+
354
+ def nombre(datos):
355
+ return _lee_clave(_real(datos), 'name') or os.path.basename(_real(datos))
356
+
357
+
358
+ def slug_ciudad(datos):
359
+ return _slug(_lee_clave(_real(datos), 'slug') or nombre(datos))
360
+
361
+
362
+ def identidad(datos):
363
+ valor = _lee_clave(_real(datos), 'id')
364
+ if valor:
365
+ return valor
366
+ return 'legacy_' + hashlib.sha256(_real(datos).encode()).hexdigest()[:16]
367
+
368
+
369
+ def slug(datos):
370
+ """Stable, filesystem-safe state key; path moves do not change it in v2."""
371
+ return f'{slug_ciudad(datos)}-{identidad(datos).split("_")[-1][:8]}'
372
+
373
+
374
+ def sesion(usuario, datos):
375
+ """Every session names both owner and city, including the first one."""
376
+ return f'{_slug(usuario or usuario_actual(), "me")}-{slug_ciudad(datos)}'
377
+
378
+
379
+ def direccion(usuario, datos):
380
+ """The city seat's stable road/bus address."""
381
+ owner = _lee_clave(_real(datos), 'owner') or usuario or usuario_actual()
382
+ return f'{_slug(owner, "me")}/{slug_ciudad(datos)}'
383
+
384
+
385
+ def resuelve(cual, usuario=''):
386
+ """A path, display name, slug or stable id -> city folder."""
387
+ usuario = _slug(usuario or usuario_actual(), 'me')
388
+ if cual and os.path.isdir(os.path.expanduser(cual)):
389
+ candidata = _real(cual)
390
+ return candidata if es_ciudad(candidata) else ''
391
+ buscada = str(cual or '').lower()
392
+ for ciudad in lista(usuario):
393
+ if buscada in {ciudad['nombre'].lower(), ciudad['slug'].lower(),
394
+ ciudad['id'].lower(), os.path.basename(ciudad['ruta']).lower()}:
395
+ return ciudad['ruta']
396
+ return ''
397
+
398
+
399
+ def _uso():
400
+ return """usage:
401
+ agents-city cities list
402
+ agents-city cities current
403
+ agents-city cities create <name>
404
+ agents-city cities use <name-or-path>
405
+
406
+ Managed cities live at ~/.agents-city/<owner>/<city>/. Creating or selecting one
407
+ never starts its tmux session; use `agents-city seat --city <name>` for that."""
408
+
409
+
410
+ def _cli_help(_args):
411
+ print(_uso())
412
+ return True
413
+
414
+
415
+ def _cli_user(_args):
416
+ print(usuario_actual())
417
+ return True
418
+
419
+
420
+ def _cli_list(args):
421
+ usuario = args[1] if len(args) > 1 else usuario_actual()
422
+ actual_ = actual(usuario, crear=False)
423
+ for ciudad in lista(usuario):
424
+ marca = '*' if _real(ciudad['ruta']) == _real(actual_) else ' '
425
+ print(f"{marca}\t{ciudad['slug']}\t{ciudad['id']}\t{ciudad['ruta']}")
426
+ return True
427
+
428
+
429
+ def _cli_current(args):
430
+ print(actual(args[1] if len(args) > 1 else usuario_actual()))
431
+ return True
432
+
433
+
434
+ def _cli_create(args):
435
+ if len(args) < 2:
436
+ return False
437
+ try:
438
+ print(crea(args[2] if len(args) > 2 else usuario_actual(), args[1]))
439
+ except (OSError, ValueError) as error:
440
+ sys.exit(str(error))
441
+ return True
442
+
443
+
444
+ def _cli_use(args):
445
+ if len(args) < 2:
446
+ return False
447
+ usuario = args[2] if len(args) > 2 else usuario_actual()
448
+ datos = resuelve(args[1], usuario)
449
+ if not datos:
450
+ sys.exit(f'No city called {args[1]!r}.')
451
+ selecciona(usuario, datos)
452
+ print(datos)
453
+ return True
454
+
455
+
456
+ def _cli_session(args):
457
+ if len(args) != 3:
458
+ return False
459
+ print(sesion(args[1], args[2]))
460
+ return True
461
+
462
+
463
+ def _cli_slug(args):
464
+ if len(args) != 2:
465
+ return False
466
+ print(slug(args[1]))
467
+ return True
468
+
469
+
470
+ def _cli_id(args):
471
+ if len(args) != 2:
472
+ return False
473
+ print(identidad(args[1]))
474
+ return True
475
+
476
+
477
+ def _cli_address(args):
478
+ if len(args) != 3:
479
+ return False
480
+ print(direccion(args[1], args[2]))
481
+ return True
482
+
483
+
484
+ def _cli_key(args):
485
+ # clave <path> <key> — read one city.yml scalar; add <value> to upsert it.
486
+ if len(args) == 3:
487
+ print(_lee_clave(args[1], args[2]))
488
+ return True
489
+ if len(args) == 4:
490
+ pon_clave(args[1], args[2], args[3])
491
+ return True
492
+ return False
493
+
494
+
495
+ def _cli_resolve(args):
496
+ if len(args) < 2:
497
+ return False
498
+ print(resuelve(args[1], args[2] if len(args) > 2 else usuario_actual()))
499
+ return True
500
+
501
+
502
+ def main():
503
+ args = sys.argv[1:]
504
+ orden = args[0] if args else 'list'
505
+ handlers = {
506
+ '-h': _cli_help, '--help': _cli_help, 'help': _cli_help,
507
+ 'user': _cli_user, 'usuario': _cli_user,
508
+ 'list': _cli_list, 'lista': _cli_list,
509
+ 'current': _cli_current, 'create': _cli_create, 'use': _cli_use,
510
+ 'sesion': _cli_session, 'session': _cli_session,
511
+ 'slug': _cli_slug, 'id': _cli_id, 'address': _cli_address,
512
+ 'clave': _cli_key, 'key': _cli_key,
513
+ 'resolve': _cli_resolve, 'resuelve': _cli_resolve,
514
+ }
515
+ if not handlers.get(orden, lambda _args: False)(args):
516
+ sys.exit(_uso())
517
+
518
+
519
+ if __name__ == '__main__':
520
+ main()
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # Where this seat's settings come from. Sourced by every hook and script, so
3
+ # there is one answer to "which city, which token, which data" instead of five.
4
+ #
5
+ # Order: the environment wins (that is how the MCP server passes user config),
6
+ # then an optional transport .env, then the keychain for the
7
+ # token alone. Nothing here fails: a hook with no settings has to stay quiet, not
8
+ # break somebody's turn.
9
+ #
10
+ # CITY_BUS_URL / CITY_BUS_TOKEN / CITY_BUS_AGENT the bus
11
+ # AGENTS_CITY_URL the map (optional)
12
+ # AGENTS_CITY_DATA the selected city folder
13
+ # AGENTS_CITY_HOME / AGENTS_CITY_USER local city namespace
14
+ # AGENTS_CITY_ORG only report repos of this org
15
+ #
16
+ # AGENTS_CITY_ORG is a filter, not a requirement: unset means every repo with a
17
+ # remote counts. It exists when one machine holds repos from more than one domain
18
+ # and a city should recognise only one of those sets.
19
+
20
+ CITY_DIR="${CITY_DIR:-$HOME/.claude/channels/city-bus}"
21
+
22
+ if [ -f "$CITY_DIR/.env" ]; then
23
+ # Only the keys we know, and only when not already set: a .env should not be
24
+ # able to redefine PATH.
25
+ while IFS='=' read -r k v; do
26
+ case "$k" in
27
+ CITY_BUS_URL|CITY_BUS_TOKEN|AGENTS_CITY_URL|AGENTS_CITY_DATA|AGENTS_CITY_DATA_DEFAULT|AGENTS_CITY_ORG|AGENTS_CITY_HOME|AGENTS_CITY_USER|CITY_ADDRESS|CITY_SEAT_NAME|CITY_HOOKS)
28
+ v="${v%\"}"; v="${v#\"}"
29
+ [ -z "$(eval "printf '%s' \"\${$k:-}\"")" ] && export "$k=$v" ;;
30
+ esac
31
+ done < "$CITY_DIR/.env"
32
+ fi
33
+
34
+ if [ -z "${CITY_BUS_TOKEN:-}" ] && command -v security >/dev/null 2>&1; then
35
+ CITY_BUS_TOKEN="$(security find-generic-password -s city@agents-city -w 2>/dev/null || true)"
36
+ export CITY_BUS_TOKEN
37
+ fi
38
+
39
+ # Delegate the selected-city decision to cities.py, the same owner used by the
40
+ # seat and the hall. A shell copy of the folder rule is exactly how v1 drifted.
41
+ if [ -z "${AGENTS_CITY_DATA:-}" ]; then
42
+ CITY_SCRIPTS="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/scripts"
43
+ if [ -n "${AGENTS_CITY_DATA_DEFAULT:-}" ] && [ -d "${AGENTS_CITY_DATA_DEFAULT}" ]; then
44
+ AGENTS_CITY_DATA="$AGENTS_CITY_DATA_DEFAULT"
45
+ else
46
+ AGENTS_CITY_DATA="$(python3 "$CITY_SCRIPTS/cities.py" current "${AGENTS_CITY_USER:-}" 2>/dev/null || true)"
47
+ fi
48
+ export AGENTS_CITY_DATA
49
+ fi
50
+
51
+ # The installed plugin is shared; the active city identity is resolved for each
52
+ # seat at runtime instead of being stored as one package-global user/role.
53
+ CITY_SCRIPTS="${CITY_SCRIPTS:-${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/scripts}"
54
+ if [ -z "${AGENTS_CITY_USER:-}" ]; then
55
+ AGENTS_CITY_USER="$(python3 "$CITY_SCRIPTS/cities.py" user 2>/dev/null || true)"
56
+ export AGENTS_CITY_USER
57
+ fi
58
+ if [ -z "${CITY_ADDRESS:-}" ] && [ -n "${AGENTS_CITY_DATA:-}" ]; then
59
+ CITY_ADDRESS="$(python3 "$CITY_SCRIPTS/cities.py" address \
60
+ "${AGENTS_CITY_USER:-me}" "$AGENTS_CITY_DATA" 2>/dev/null || true)"
61
+ export CITY_ADDRESS
62
+ fi
63
+ if [ -n "${CITY_ADDRESS:-}" ]; then
64
+ # A v1 .env may still carry one global CITY_BUS_AGENT. It cannot identify two
65
+ # concurrently running cities, so a resolved v2 city always overrides it.
66
+ CITY_BUS_AGENT="$CITY_ADDRESS"
67
+ export CITY_BUS_AGENT
68
+ fi
69
+ if [ -z "${CITY_SEAT_NAME:-}" ] && [ -n "${AGENTS_CITY_DATA:-}" ]; then
70
+ CITY_SEAT_NAME="$(python3 "$CITY_SCRIPTS/cities.py" session \
71
+ "${AGENTS_CITY_USER:-me}" "$AGENTS_CITY_DATA" 2>/dev/null || true)"
72
+ export CITY_SEAT_NAME
73
+ fi
74
+
75
+ # The map defaults to the bus: they are the same worker unless somebody split them.
76
+ [ -z "${AGENTS_CITY_URL:-}" ] && export AGENTS_CITY_URL="${CITY_BUS_URL:-}"
77
+
78
+ # Is this repo one this seat reports about? Prints the repo name and returns 0,
79
+ # or returns 1 and prints nothing.
80
+ repo_de_la_ciudad() {
81
+ local remoto repo
82
+ remoto="$(git remote get-url origin 2>/dev/null || true)"
83
+ [ -z "$remoto" ] && return 1
84
+ if [ -n "${AGENTS_CITY_ORG:-}" ]; then
85
+ case "$remoto" in *"/${AGENTS_CITY_ORG}/"*|*":${AGENTS_CITY_ORG}/"*) ;; *) return 1 ;; esac
86
+ fi
87
+ repo="${remoto##*/}"; repo="${repo%.git}"
88
+ [ -z "$repo" ] && return 1
89
+ printf '%s' "$repo"
90
+ }
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ # Own the city bus processes without adding transport details to city-session.sh.
3
+ set -euo pipefail
4
+
5
+ SCRIPTS="$(cd "$(dirname "$0")" && pwd)"
6
+ ROOT="$(cd "$SCRIPTS/.." && pwd)"
7
+ . "$SCRIPTS/city-env.sh"
8
+ CLIENT="$ROOT/channel/client.js"
9
+ ADAPTER="$ROOT/channel/adapter.js"
10
+ GATEWAY="$ROOT/channel/runtime-gateway.js"
11
+
12
+ case "${1:-}" in
13
+ ensure)
14
+ exec node "$CLIENT" ensure
15
+ ;;
16
+ gateway)
17
+ actor="${2:-}"
18
+ workdir="${3:-}"
19
+ command_line="${4:-}"
20
+ auto="${5:-1}"
21
+ [ -n "$actor" ] && [ -n "$workdir" ] && [ -n "$command_line" ] || {
22
+ echo "usage: city-runtime.sh gateway <actor> <workdir> <runtime-command> [auto]" >&2
23
+ exit 2
24
+ }
25
+ node "$CLIENT" ensure >/dev/null
26
+ if [ "$actor" = seat ]; then
27
+ road_url="${CITY_BUS_URL:-}"
28
+ road_token="${CITY_BUS_TOKEN:-}"
29
+ runtime_kind="seat"
30
+ else
31
+ road_url=""
32
+ road_token=""
33
+ runtime_kind="repo"
34
+ fi
35
+ exec env \
36
+ CITY_BUS_ACTOR="$actor" \
37
+ CITY_RUNTIME_KIND="$runtime_kind" \
38
+ CITY_RUNTIME_AUTO="$auto" \
39
+ CITY_BUS_URL="$road_url" \
40
+ CITY_BUS_TOKEN="$road_token" \
41
+ node "$GATEWAY" \
42
+ --data "$AGENTS_CITY_DATA" \
43
+ --actor "$actor" \
44
+ --cwd "$workdir" \
45
+ --command "$command_line" \
46
+ --auto "$auto" \
47
+ --interactive 1
48
+ ;;
49
+ fallback)
50
+ actor="${2:-}"
51
+ target="${3:-}"
52
+ runtime="${4:-unknown}"
53
+ [ -n "$actor" ] && [ -n "$target" ] || {
54
+ echo "usage: city-runtime.sh fallback <actor> <tmux-target> <runtime>" >&2
55
+ exit 2
56
+ }
57
+ echo "WARNING: $actor uses the explicit terminal fallback; native delivery is unavailable." >&2
58
+ node "$CLIENT" ensure >/dev/null
59
+ runtime_dir="$(node "$CLIENT" runtime-dir)"
60
+ mkdir -p "$runtime_dir/adapters"
61
+ if [ "$actor" = seat ]; then
62
+ road_url="${CITY_BUS_URL:-}"
63
+ road_token="${CITY_BUS_TOKEN:-}"
64
+ runtime_kind="seat"
65
+ else
66
+ road_url=""
67
+ road_token=""
68
+ runtime_kind="repo"
69
+ fi
70
+ nohup env \
71
+ CITY_BUS_ACTOR="$actor" \
72
+ CITY_RUNTIME_KIND="$runtime_kind" \
73
+ CITY_AGENT_RUNTIME="$runtime" \
74
+ CITY_BUS_URL="$road_url" \
75
+ CITY_BUS_TOKEN="$road_token" \
76
+ node "$ADAPTER" \
77
+ --data "$AGENTS_CITY_DATA" \
78
+ --actor "$actor" \
79
+ --target "$target" \
80
+ --runtime "$runtime" \
81
+ >>"$runtime_dir/adapters/$actor.log" 2>&1 </dev/null &
82
+ ;;
83
+ *)
84
+ echo "usage: city-runtime.sh <ensure | gateway actor workdir command [auto] | fallback actor tmux-target runtime>" >&2
85
+ exit 2
86
+ ;;
87
+ esac