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,526 @@
1
+ #!/usr/bin/env bash
2
+ # Build somebody's tmux session out of their card:
3
+ # window "seat" -> this city's stable address, sitting in the city folder
4
+ # window N -> one repo support agent, on its configured runtime
5
+ #
6
+ # city-session.sh [user] [--claude] [--no-sync] [--no-yolo] [--only r1,r2]
7
+ #
8
+ # The repos on your card are looked for across the whole disk: they do not have
9
+ # to live in one folder, and the folder does not have to be named after the repo.
10
+ #
11
+ # Without --claude it just opens shells. With it, each window starts the
12
+ # runtime configured for that city member (Claude, Codex, OpenCode, or custom).
13
+ #
14
+ # The permissions are NOT the same in every window, and that is the point:
15
+ #
16
+ # the "seat" window -> NO bypass. It receives text from connected cities, and
17
+ # there the confirmations are the only door.
18
+ # repo windows -> bypass on (yolo). Your repos on your machine, and
19
+ # nothing from outside comes in through them. --no-yolo
20
+ # turns it off.
21
+ #
22
+ # Repo agents do not message one another. Every runtime receives assignments
23
+ # through the same authenticated local WebSocket bus; the seat chairs the turns.
24
+
25
+ set -uo pipefail
26
+
27
+ USUARIO=""
28
+ RUN_CLAUDE=0
29
+ DO_SYNC=1
30
+ YOLO=1
31
+ SOLO=""
32
+
33
+ while [ $# -gt 0 ]; do
34
+ case "$1" in
35
+ -c|--claude) RUN_CLAUDE=1; shift ;;
36
+ -n|--no-sync) DO_SYNC=0; shift ;;
37
+ --no-yolo) YOLO=0; shift ;;
38
+ --only) SOLO="${2:-}"; shift 2 ;;
39
+ -h|--help) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
40
+ -*) echo "Unknown option: $1" >&2; exit 1 ;;
41
+ *) USUARIO="$1"; shift ;;
42
+ esac
43
+ done
44
+
45
+ # ── The selected city, where its one owner card lives ──────────────────────
46
+ # Resolve it through the same owner as seat, hall, hooks and the road channel.
47
+ # An explicit AGENTS_CITY_DATA still wins inside city-env.sh.
48
+ . "$(dirname "$0")/city-env.sh"
49
+ EQUIPO="${AGENTS_CITY_DATA:-}"
50
+ if [ ! -d "$EQUIPO" ]; then
51
+ echo "I cannot resolve the selected city." >&2
52
+ echo "Create one with agents-city setup, or select one with agents-city cities use <city>." >&2
53
+ exit 1
54
+ fi
55
+
56
+ # ── Who ────────────────────────────────────────────────────────────────────
57
+ if [ -z "$USUARIO" ]; then USUARIO="${AGENTS_CITY_USER:-}"; fi
58
+ FICHA="$EQUIPO/$USUARIO.md"
59
+ if [ ! -f "$FICHA" ]; then
60
+ echo "There is no card for '$USUARIO' in $EQUIPO." >&2
61
+ fichas=""
62
+ for f in "$EQUIPO"/*.md; do
63
+ case "$(basename "$f")" in README*|AGENTS*) ;; *) fichas="$fichas $(basename "$f" .md)" ;; esac
64
+ done
65
+ echo "Cards there:$fichas" >&2
66
+ echo "Say who you are: $(basename "$0") <user>" >&2
67
+ exit 1
68
+ fi
69
+
70
+ # ── The card: agent and repos ──────────────────────────────────────────────
71
+ LEER="$(dirname "$0")/read-card.py"
72
+ RUNTIME="$(dirname "$0")/city-runtime.sh"
73
+ LAUNCHPY="$(dirname "$0")/launch.py"
74
+ CHANNEL_CLIENT="$(dirname "$0")/../channel/client.js"
75
+ # `agent`, which is what every card actually carries. A v1 reader once asked for
76
+ # `agente`, got nothing every single time,
77
+ # and fell through to the default below: so the seat window never carried anybody's
78
+ # role, and the architect sat on the bus as <user>/dev. The fallback hid it.
79
+ #
80
+ # One call, because card.py accepts `agente` as an alias for a hand-written card.
81
+ AGENTE="$(python3 "$LEER" "$FICHA" agent)"
82
+ REPOS_RAW="$(python3 "$LEER" "$FICHA" repos)"
83
+ [ -z "$AGENTE" ] && AGENTE="$(python3 "$(dirname "$0")/cities.py" address "$USUARIO" "$EQUIPO")"
84
+
85
+ [ -n "$SOLO" ] && REPOS_RAW="$SOLO"
86
+
87
+ CODE_DIR="${CITY_CODE_DIR:-$HOME/codigo}"
88
+ # The session's name comes from cities.py, not from a rule of our own: in the
89
+ # every city it carries owner and city — so two local seats never fight over one
90
+ # tmux session, including the first city named `home`.
91
+ SESSION="$(python3 "$(dirname "$0")/cities.py" sesion "$USUARIO" "$EQUIPO")"
92
+ [ -z "$SESSION" ] && SESSION="$USUARIO"
93
+ ADDRESS="$(python3 "$(dirname "$0")/cities.py" address "$USUARIO" "$EQUIPO")"
94
+ # Claude session names are machine-global. The old literal `seat` meant opening a
95
+ # second local city could address or replace the first one's seat by accident.
96
+ SEAT_NAME="$SESSION"
97
+
98
+ if tmux has-session -t "$SESSION" 2>/dev/null; then
99
+ echo "Session '$SESSION' is already up — attaching."
100
+ exec tmux attach -d -t "$SESSION"
101
+ fi
102
+
103
+ # Claude's native cross-session path is explicitly closed. The city bus is the
104
+ # only route, so Claude, Codex, OpenCode and Kimi obey the same topology.
105
+ # skipDangerousModePermissionPrompt only suppresses Claude's local yolo notice.
106
+ SETTINGS='{"crossSessionInbound":"refuse","skipDangerousModePermissionPrompt":true}'
107
+ NO_PEER_TOOLS=' --disallowed-tools SendMessage,ListAgents'
108
+ YOLO_FLAG=""
109
+ [ "$YOLO" -eq 1 ] && YOLO_FLAG=" --dangerously-skip-permissions"
110
+
111
+ # Whether the chair itself runs yolo. A per-city decision written in city.yml
112
+ # (`seat_yolo: 1`, set at setup or with `agents-city seat --seat-yolo on`):
113
+ # locally the seat is the owner's own hands on the owner's own machine, and an
114
+ # owner who trusts that should not be asked for permission by their own chair.
115
+ # The default stays off, and --no-yolo remains the session-wide brake for both.
116
+ SEAT_YOLO="$(python3 "$(dirname "$0")/cities.py" clave "$EQUIPO" seat_yolo 2>/dev/null || true)"
117
+ [ "$YOLO" -eq 0 ] && SEAT_YOLO=0
118
+ SEAT_AUTO=0
119
+ SEAT_YOLO_FLAG=""
120
+ if [ "${SEAT_YOLO:-0}" = "1" ]; then
121
+ SEAT_AUTO=1
122
+ SEAT_YOLO_FLAG=" --dangerously-skip-permissions"
123
+ fi
124
+
125
+ # Claude authentication can also arrive through the parent shell/tmux server.
126
+ # A stale CLAUDE_CODE_OAUTH_TOKEN (or API/gateway variable) takes precedence
127
+ # over the owner's healthy Claude.ai login and makes a Team/Max account appear
128
+ # as "Claude API" with no usage credits. Never delete or rewrite credentials:
129
+ # when a stored Claude.ai login works, remove provider overrides only from each
130
+ # city child process. Owners intentionally using environment auth can keep it
131
+ # with CITY_CLAUDE_AUTH=environment.
132
+ claude_auth_prefix() {
133
+ [ "${CITY_CLAUDE_AUTH:-auto}" = environment ] && return 0
134
+ command -v claude >/dev/null 2>&1 || return 0
135
+ local estado
136
+ estado="$(env \
137
+ -u CLAUDE_CODE_OAUTH_TOKEN \
138
+ -u ANTHROPIC_API_KEY \
139
+ -u ANTHROPIC_AUTH_TOKEN \
140
+ -u ANTHROPIC_BASE_URL \
141
+ -u CLAUDE_CODE_USE_BEDROCK \
142
+ -u CLAUDE_CODE_USE_VERTEX \
143
+ -u CLAUDE_CODE_USE_FOUNDRY \
144
+ claude auth status 2>/dev/null || true)"
145
+ if printf '%s' "$estado" | grep -Eq '"loggedIn"[[:space:]]*:[[:space:]]*true' \
146
+ && printf '%s' "$estado" | grep -Eq '"authMethod"[[:space:]]*:[[:space:]]*"claude\.ai"'; then
147
+ printf '%s' 'env -u CLAUDE_CODE_OAUTH_TOKEN -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL -u CLAUDE_CODE_USE_BEDROCK -u CLAUDE_CODE_USE_VERTEX -u CLAUDE_CODE_USE_FOUNDRY '
148
+ fi
149
+ }
150
+ CLAUDE_AUTH_PREFIX=""
151
+ [ "$RUN_CLAUDE" -eq 1 ] && CLAUDE_AUTH_PREFIX="$(claude_auth_prefix)"
152
+
153
+ # The cage. Yolo answers "does the agent ask permission?"; the cage answers
154
+ # "what can it touch?". They are independent, so yolo stays and each repo
155
+ # window is wrapped in a per-window macOS seatbelt profile: writes only in its
156
+ # own repo and runtime state, and the credential files (`~/.ssh`,
157
+ # `~/.git-credentials`, gh/cloud configs) sealed at the kernel. On a machine
158
+ # without seatbelt, or with CITY_CAGE=0, the prefix is empty and nothing
159
+ # changes. The seat is deliberately NOT caged: it is the permissioned side.
160
+ CAGE="$(dirname "$0")/cage.py"
161
+ BROKERPY="$(dirname "$0")/broker.py"
162
+ WORKSPACE="$(dirname "$0")/workspace.py"
163
+ jaula_de() { # window, cwd, [broker token file], [colon-joined mount targets] -> prefix or ''
164
+ local args=(line --window "$1" --repo "$2")
165
+ [ -n "${3:-}" ] && args+=(--token-file "$3")
166
+ [ -n "${4:-}" ] && args+=(--mounts "$4")
167
+ python3 "$CAGE" "${args[@]}" 2>/dev/null || true
168
+ }
169
+
170
+ # Which model and effort a window's agent starts with. Three voices, in order:
171
+ # this launch's flags (CITY_MODEL/CITY_EFFORT, set by `seat --model/--effort`),
172
+ # then the card's per-window key (`model.dbt:`), then the card's default
173
+ # (`model:`). Nothing set means no flag at all — the owner's Claude default,
174
+ # which is the right silence.
175
+ motor_de() { # $1 = window name -> prints the extra claude flags
176
+ local ventana="$1" modelo esfuerzo salida=""
177
+ modelo="${CITY_MODEL:-$(python3 "$LEER" "$FICHA" "model.$ventana")}"
178
+ [ -z "$modelo" ] && modelo="$(python3 "$LEER" "$FICHA" model)"
179
+ esfuerzo="${CITY_EFFORT:-$(python3 "$LEER" "$FICHA" "effort.$ventana")}"
180
+ [ -z "$esfuerzo" ] && esfuerzo="$(python3 "$LEER" "$FICHA" effort)"
181
+ [ -n "$modelo" ] && salida=" --model $modelo"
182
+ [ -n "$esfuerzo" ] && salida="$salida --effort $esfuerzo"
183
+ printf '%s' "$salida"
184
+ }
185
+
186
+ runtime_de() { # configured command -> claude | codex | opencode | kimi | terminal | unknown
187
+ local raw="$1" first
188
+ case "$raw" in terminal:*) printf 'terminal'; return ;; esac
189
+ first="${raw%% *}"
190
+ first="${first##*/}"
191
+ case "$first" in
192
+ claude|claude-code) printf 'claude' ;;
193
+ codex) printf 'codex' ;;
194
+ opencode) printf 'opencode' ;;
195
+ kimi|kimi-code) printf 'kimi' ;;
196
+ *) printf 'unknown' ;;
197
+ esac
198
+ }
199
+
200
+ gateway_line() { # actor, cwd, full runtime command, optional auto-approve
201
+ printf '%q ' "$RUNTIME" gateway "$1" "$2" "$3" "${4:-$YOLO}"
202
+ }
203
+
204
+ # Terminal emulators and tmux both have finite input queues. Sending a 1-2 KB
205
+ # shell program as simulated keystrokes can cut it at an arbitrary byte (we saw
206
+ # `--da`, `--dangerously`, and even a lone `-`). Put the exact command in a
207
+ # private launcher and type only its short path into the pane.
208
+ lanza() { # tmux target, actor, cwd, full command
209
+ local target="$1" actor="$2" cwd="$3" command="$4" launcher typed
210
+ launcher="$(python3 "$LAUNCHPY" create --data "$EQUIPO" --actor "$actor" \
211
+ --cwd "$cwd" --client "$CHANNEL_CLIENT" --command "$command")" || return 1
212
+ printf -v typed '%q' "$launcher"
213
+ tmux send-keys -t "$target" -l -- "$typed"
214
+ tmux send-keys -t "$target" C-m
215
+ }
216
+
217
+ # ── Claude windows start one at a time ─────────────────────────────────────
218
+ # Every Claude session on a machine shares one OAuth credential, and refreshing
219
+ # it rotates a single-use refresh token: the first process to refresh wins and
220
+ # the rest are left holding one the server has already invalidated. It does not
221
+ # read as an auth problem — it reads as "you have no quota left" on an account
222
+ # with plenty, and nothing but logging out and back in clears it. Upstream has
223
+ # had it reported many times over (claude-code#24317, #25609, #27933, #48786).
224
+ #
225
+ # One window per repo makes this the worst possible caller: eighteen sessions
226
+ # starting in the same millisecond, all refreshing at once. So the seat's Claude
227
+ # goes first and alone, and every repo window holds its own shell for a moment
228
+ # before starting. The wait happens INSIDE the window, so attaching is still
229
+ # instant and you watch the agents wake one by one; it also spaces out eighteen
230
+ # simultaneous `git fetch`es, which were their own small storm.
231
+ #
232
+ # What this does not fix: a token that expires at four in the afternoon expires
233
+ # for every session already open. That one is upstream's, not ours.
234
+ #
235
+ # CITY_SETTLE=0 CITY_STAGGER=0 everything at once, as it was before
236
+ entero() { # $1 = value, $2 = fallback. A typo must not crash somebody's day.
237
+ case "$1" in
238
+ '' | *[!0-9]*) printf '%s' "$2" ;;
239
+ *) printf '%s' "$1" ;;
240
+ esac
241
+ }
242
+ SETTLE="$(entero "${CITY_SETTLE-}" 8)"
243
+ STAGGER="$(entero "${CITY_STAGGER-}" 1)"
244
+
245
+ # Seconds the Nth repo window waits. The first carries the whole settle, because
246
+ # it is the one that would collide with the seat's refresh; after that a token is
247
+ # already fresh in the store and the rest only need to not arrive as a herd. A
248
+ # negative turn means nobody has refreshed yet — the seat runs something else, so
249
+ # this window IS the first Claude and has nothing to wait for.
250
+ retraso() { # $1 = claude repo windows already scheduled
251
+ if [ "$1" -lt 0 ]; then printf '0'; else printf '%s' "$((SETTLE + $1 * STAGGER))"; fi
252
+ }
253
+
254
+ sync_line() {
255
+ [ "$DO_SYNC" -eq 1 ] || return 0
256
+ printf '%s' 'if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git fetch origin -q --prune 2>/dev/null; b=$(for x in main master; do git show-ref -q --verify refs/remotes/origin/$x && echo $x && break; done); [ -n "$b" ] && { git checkout -q "$b" 2>/dev/null; git pull --ff-only -q origin "$b" 2>/dev/null && echo " ✓ $b up to date" || echo " ⚠ could not update"; } || echo " · no main/master on origin"; fi; '
257
+ }
258
+
259
+ # ── tmux comforts ──────────────────────────────────────────────────────────
260
+ # Applied to the tmux server, not to anybody's ~/.tmux.conf: this does not touch
261
+ # your configuration. They are additions, so they sit alongside whatever you run.
262
+ comodidades() {
263
+ # Mouse: click a window name in the bar to switch, click a pane to focus it,
264
+ # wheel for the scrollback.
265
+ tmux set-option -g mouse on 2>/dev/null
266
+
267
+ # Windows numbered from 1, so Alt+1 is the first one and not the second.
268
+ tmux set-option -g base-index 1 2>/dev/null
269
+ tmux set-option -g renumber-windows on 2>/dev/null
270
+
271
+ # Alt+1..9 jumps to that window, no prefix.
272
+ for n in 1 2 3 4 5 6 7 8 9; do
273
+ tmux bind-key -n "M-$n" select-window -t "$n" 2>/dev/null
274
+ done
275
+ # Alt+left / Alt+right to move in order.
276
+ tmux bind-key -n M-Left previous-window 2>/dev/null
277
+ tmux bind-key -n M-Right next-window 2>/dev/null
278
+
279
+ # Make the status bar say where you are.
280
+ tmux set-option -g status on 2>/dev/null
281
+ tmux set-option -g status-left '[#S] ' 2>/dev/null
282
+ tmux set-option -g status-left-length 20 2>/dev/null
283
+ tmux set-option -g window-status-current-style 'bg=colour4,fg=black,bold' 2>/dev/null
284
+
285
+ # Which window wants you. With one agent per repo you are looking at one window
286
+ # and five are working, so the useful signal is not "what is on screen" but
287
+ # "which tab needs me". Claude rings the terminal bell when it is waiting on you,
288
+ # so bell is what means come here: that tab turns red and stays red until you
289
+ # visit it. Activity is the softer one — output moved — and only underlines.
290
+ #
291
+ # No plugin manager and no third-party plugins for this: it is a handful of tmux
292
+ # options, and installing somebody else's tmux config to get a coloured tab is a
293
+ # bad trade.
294
+ #
295
+ # `-gw`, not `-g`. These four are *window* options, and setting them as server
296
+ # options fails — silently, because of the 2>/dev/null. Half of this block was
297
+ # doing nothing at all: monitor-activity stayed off and the styles stayed default.
298
+ tmux set-option -gw monitor-bell on 2>/dev/null
299
+ tmux set-option -gw monitor-activity on 2>/dev/null
300
+ tmux set-option -gw window-status-bell-style 'fg=colour231,bg=colour160,bold' 2>/dev/null
301
+ tmux set-option -gw window-status-activity-style 'fg=colour222,underscore' 2>/dev/null
302
+ # Session options: where the bell is allowed to come from, and no flashing bar
303
+ # over the pane you are reading.
304
+ tmux set-option -g bell-action other 2>/dev/null
305
+ tmux set-option -g visual-bell off 2>/dev/null
306
+ tmux set-option -g visual-activity off 2>/dev/null
307
+ # A clock, so a frozen status bar is visibly frozen. tmux expands strftime itself,
308
+ # so this needs no shell — `#(date ...)` here spawns a process every interval and
309
+ # was being written with a printf-escaped %% that tmux never understood.
310
+ tmux set-option -g status-interval 5 2>/dev/null
311
+ tmux set-option -g status-right ' %H:%M ' 2>/dev/null
312
+ tmux set-option -g status-right-length 12 2>/dev/null
313
+ }
314
+
315
+ # ── Work out where each agent works ────────────────────────────────────────
316
+ # Agents come first. A card that declares `agents:` drives the agent-first
317
+ # model: each agent's cwd is its workspace folder, and its mounts (symlinks to
318
+ # repos, worktrees or document folders) become the extra writable roots the
319
+ # cage allows. A legacy card with only `repos:` takes the path below, unchanged
320
+ # — every repo is still an agent whose one mount is that repo.
321
+ LOCALIZA="$(dirname "$0")/find-repos.sh"
322
+ faltan=()
323
+ RUTAS=()
324
+ NOMBRES=()
325
+ MONTAJES=()
326
+ AGENTS_RAW="$(python3 "$LEER" "$FICHA" agents 2>/dev/null || true)"
327
+
328
+ if [ -n "$AGENTS_RAW" ] && [ -z "$SOLO" ]; then
329
+ # Agent-first: one `sync-all` pass reads the card once, materialises every
330
+ # agent's workspace and mounts, and emits `slug<TAB>cwd<TAB>targets` per line —
331
+ # so the whole city costs one interpreter start and one card parse, not N+1.
332
+ while IFS=$'\t' read -r slug cwd targets; do
333
+ [ -z "$slug" ] && continue
334
+ RUTAS+=("$cwd")
335
+ NOMBRES+=("$slug")
336
+ MONTAJES+=("$targets")
337
+ # stderr is left attached so workspace.py's "skipping mount <x>" warnings
338
+ # reach the owner — a typo'd or missing mount source must not degrade in
339
+ # silence, the way the legacy path reports missing repos via `faltan`.
340
+ done < <(python3 "$WORKSPACE" sync-all --data "$EQUIPO" --card "$FICHA")
341
+ else
342
+ REPOS=()
343
+ # bash 3.2 — the one macOS ships — blows up on "${REPOS[@]}" when it is empty.
344
+ [ -n "$REPOS_RAW" ] && IFS=',' read -ra REPOS <<< "$REPOS_RAW"
345
+ for r in ${REPOS[@]+"${REPOS[@]}"}; do
346
+ r="$(echo "$r" | xargs)"; [ -z "$r" ] && continue
347
+ # 1) the usual place; 2) wherever you keep it, found by its remote.
348
+ path="$CODE_DIR/$r"
349
+ [ -d "$path" ] || path="$("$LOCALIZA" "$r" 2>/dev/null)"
350
+ if [ -z "$path" ] || [ ! -d "$path" ]; then faltan+=("$r"); continue; fi
351
+ RUTAS+=("$path")
352
+ # One canonical slug is also the tmux name, engine-key suffix and bus actor.
353
+ NOMBRES+=("$(python3 "$LEER" --window "$r")")
354
+ MONTAJES+=("")
355
+ done
356
+ fi
357
+
358
+ # Trust every folder up front, so no window sits waiting on the dialog.
359
+ python3 "$(dirname "$0")/trust-repos.py" "$EQUIPO" ${RUTAS[@]+"${RUTAS[@]}"}
360
+
361
+ # ── The seat window: this city's identity on its roads ─────────────────────
362
+ "$RUNTIME" ensure >/dev/null
363
+ comodidades
364
+ tmux new-session -d -s "$SESSION" -n seat -c "$EQUIPO"
365
+
366
+ # Where the cards live, told to the session rather than assumed.
367
+ #
368
+ # A window inherits the environment of the tmux *server*, not of whoever ran this.
369
+ # So on any machine where a tmux server was already up, AGENTS_CITY_DATA never
370
+ # reached the windows and the plugin inside them looked for the cards in the
371
+ # default place. Set on the session, and also written in front of each command
372
+ # below, because the two cost nothing and the failure is silent.
373
+ tmux set-environment -t "$SESSION" AGENTS_CITY_DATA "$EQUIPO" 2>/dev/null
374
+ tmux set-environment -t "$SESSION" AGENTS_CITY_HOME "${AGENTS_CITY_HOME:-$HOME/.agents-city}" 2>/dev/null
375
+ tmux set-environment -t "$SESSION" AGENTS_CITY_USER "$USUARIO" 2>/dev/null
376
+ tmux set-environment -t "$SESSION" CITY_ADDRESS "$ADDRESS" 2>/dev/null
377
+ tmux set-environment -t "$SESSION" CITY_SEAT_NAME "$SEAT_NAME" 2>/dev/null
378
+
379
+ # The seat runs Claude unless the card says otherwise. `runs.seat` is the same key
380
+ # the repo windows use, and it makes a city with no Claude in it possible at all:
381
+ # what that seat gives up is the `/city:` commands, which are Claude's, and what it
382
+ # keeps is the folder, the identity, and everything the terminal does.
383
+ SEAT_OTRO="$(python3 "$LEER" "$FICHA" "runs.seat")"
384
+ turno=0
385
+ [ -n "$SEAT_OTRO" ] && [ "$(runtime_de "$SEAT_OTRO")" != claude ] && turno=-1
386
+ # If the seat runs another engine, the first repo window is the first Claude.
387
+ if [ "$RUN_CLAUDE" -eq 1 ]; then
388
+ if [ -n "$SEAT_OTRO" ]; then
389
+ SEAT_RUNTIME="$(runtime_de "$SEAT_OTRO")"
390
+ if [ "$SEAT_RUNTIME" = claude ]; then
391
+ CLAUDE_SEAT="$SEAT_OTRO --name $SEAT_NAME --settings '$SETTINGS'$NO_PEER_TOOLS$SEAT_YOLO_FLAG"
392
+ lanza "$SESSION:seat" seat "$EQUIPO" \
393
+ "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE ${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$CLAUDE_SEAT" "$SEAT_AUTO")"
394
+ elif [ "$SEAT_RUNTIME" = codex ] || [ "$SEAT_RUNTIME" = opencode ] || [ "$SEAT_RUNTIME" = kimi ]; then
395
+ lanza "$SESSION:seat" seat "$EQUIPO" \
396
+ "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE $(gateway_line seat "$EQUIPO" "$SEAT_OTRO" "$SEAT_AUTO")"
397
+ elif [ "$SEAT_RUNTIME" = terminal ]; then
398
+ SEAT_COMMAND="${SEAT_OTRO#terminal:}"
399
+ lanza "$SESSION:seat" seat "$EQUIPO" \
400
+ "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE $SEAT_COMMAND"
401
+ "$RUNTIME" fallback seat "$SESSION:seat" "${SEAT_COMMAND%% *}"
402
+ else
403
+ tmux send-keys -t "$SESSION:seat" \
404
+ "echo 'No native Agents City gateway for: $SEAT_OTRO. Use terminal:$SEAT_OTRO only if you explicitly accept terminal injection.'" C-m
405
+ fi
406
+ else
407
+ CLAUDE_SEAT="claude --name $SEAT_NAME$(motor_de seat) --settings '$SETTINGS'$NO_PEER_TOOLS$SEAT_YOLO_FLAG"
408
+ lanza "$SESSION:seat" seat "$EQUIPO" \
409
+ "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE ${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$CLAUDE_SEAT" "$SEAT_AUTO")"
410
+ fi
411
+ fi
412
+
413
+ # The broker, opt-in with CITY_BROKER=1: caged windows cannot read the gh
414
+ # token — on purpose — so PRs and pushes go through a small owner-side process
415
+ # that validates (declared repo only, never the default branch) and audits.
416
+ # Each window gets its own token file, and its cage re-allows exactly that one.
417
+ BROKER_URL=""
418
+ if [ "${CITY_BROKER:-0}" = "1" ]; then
419
+ python3 "$BROKERPY" stop --data "$EQUIPO" >/dev/null 2>&1 || true
420
+ nohup python3 "$BROKERPY" serve --data "$EQUIPO" >/dev/null 2>&1 &
421
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
422
+ BROKER_URL="$(python3 "$BROKERPY" url --data "$EQUIPO" 2>/dev/null || true)"
423
+ [ -n "$BROKER_URL" ] && break
424
+ sleep 0.3
425
+ done
426
+ [ -z "$BROKER_URL" ] && echo "WARNING: the credential broker did not start; windows launch without it." >&2
427
+ fi
428
+
429
+ # ── One window per repo, reachable only through its chaired bus identity ───
430
+ #
431
+ # Claude by default, with its engine keys and its yolo flag. But a window can
432
+ # run a supported agent CLI instead — `runs.dbt: codex` on the card. Claude uses
433
+ # its persistent stream-json process; Codex, OpenCode and Kimi use their native
434
+ # gateways. None receives assignments through tmux. An unknown CLI must be
435
+ # prefixed with `terminal:` to opt into the visibly labelled compatibility path.
436
+ i=0
437
+ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
438
+ win="${NOMBRES[$i]}"
439
+ i=$((i + 1))
440
+ # Professional specialty is separate from bus authority. Every repo actor is
441
+ # still a member; only `seat` is chair. A malformed or legacy value resolves
442
+ # to the explicit blank role in read-card.py.
443
+ ROL_REPO="$(python3 "$LEER" --actor-role "$FICHA" "$win")"
444
+ # This window's cage and, when the broker runs, its own single-repo token.
445
+ # The token travels as a file path, never on the command line, and the cage
446
+ # profile re-allows reading exactly that file and nothing else in the broker.
447
+ TOKEN_FILE=""
448
+ BROKER_ENV=""
449
+ if [ -n "$BROKER_URL" ]; then
450
+ TOKEN_FILE="$(python3 "$BROKERPY" mint --data "$EQUIPO" "$win" --repo "$path" --file-only 2>/dev/null || true)"
451
+ [ -n "$TOKEN_FILE" ] && BROKER_ENV="CITY_BROKER_URL=$BROKER_URL CITY_BROKER_TOKEN_FILE=$TOKEN_FILE "
452
+ fi
453
+ MOUNTS_WIN="${MONTAJES[$((i - 1))]:-}"
454
+ JAULA="$(jaula_de "$win" "$path" "$TOKEN_FILE" "$MOUNTS_WIN")"
455
+ CAGE_RUNTIME_ENV=""
456
+ [ -n "$JAULA" ] && CAGE_RUNTIME_ENV="env CITY_OUTER_CAGE=1 "
457
+ tmux new-window -t "$SESSION" -n "$win" -c "$path"
458
+ if [ "$RUN_CLAUDE" -eq 1 ]; then
459
+ OTRO="$(python3 "$LEER" "$FICHA" "runs.$win")"
460
+ if [ -n "$OTRO" ]; then
461
+ KIND="$(runtime_de "$OTRO")"
462
+ if [ "$KIND" = claude ]; then
463
+ espera="$(retraso "$turno")"
464
+ turno=$((turno + 1))
465
+ if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
466
+ CLAUDE_REPO="$OTRO --name $SESSION-$win --settings '$SETTINGS'$NO_PEER_TOOLS$YOLO_FLAG"
467
+ lanza "$SESSION:$win" "$win" "$path" \
468
+ "$espera$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV$JAULA$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
469
+ elif [ "$KIND" = codex ] || [ "$KIND" = opencode ] || [ "$KIND" = kimi ]; then
470
+ # Native servers have their own credentials and do not share Claude's OAuth race.
471
+ # Codex must remain outside the outer macOS seatbelt: its node_repl MCP
472
+ # applies its own sandbox and macOS rejects that nested sandbox_apply.
473
+ # Codex's app-server receives workspace-write below; OpenCode and Kimi
474
+ # still use the per-repo outer cage.
475
+ RUNTIME_CAGE="$JAULA"
476
+ RUNTIME_CAGE_ENV="$CAGE_RUNTIME_ENV"
477
+ if [ "$KIND" = codex ]; then
478
+ RUNTIME_CAGE=""
479
+ RUNTIME_CAGE_ENV=""
480
+ fi
481
+ lanza "$SESSION:$win" "$win" "$path" \
482
+ "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV$RUNTIME_CAGE$RUNTIME_CAGE_ENV$(gateway_line "$win" "$path" "$OTRO")"
483
+ elif [ "$KIND" = terminal ]; then
484
+ FALLBACK_COMMAND="${OTRO#terminal:}"
485
+ lanza "$SESSION:$win" "$win" "$path" \
486
+ "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV$JAULA$FALLBACK_COMMAND"
487
+ "$RUNTIME" fallback "$win" "$SESSION:$win" "${FALLBACK_COMMAND%% *}"
488
+ else
489
+ tmux send-keys -t "$SESSION:$win" \
490
+ "echo 'No native Agents City gateway for: $OTRO. Use terminal:$OTRO only if you explicitly accept terminal injection.'" C-m
491
+ fi
492
+ else
493
+ espera="$(retraso "$turno")"
494
+ turno=$((turno + 1))
495
+ if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
496
+ CLAUDE_REPO="claude --name $SESSION-$win$(motor_de "$win") --settings '$SETTINGS'$NO_PEER_TOOLS$YOLO_FLAG"
497
+ lanza "$SESSION:$win" "$win" "$path" \
498
+ "$espera$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV${JAULA}$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
499
+ fi
500
+ fi
501
+ done
502
+
503
+ if [ ${#faltan[@]-0} -gt 0 ]; then
504
+ echo >&2
505
+ echo "I could not find these repos on this machine (window skipped):" >&2
506
+ printf ' %s\n' "${faltan[@]}" >&2
507
+ echo >&2
508
+ echo "If they live somewhere unusual: CITY_SEARCH_IN=/where/they/are $(basename "$0")" >&2
509
+ echo "If you simply do not have them, clone them anywhere and run this again." >&2
510
+ fi
511
+
512
+ if [ "${#RUTAS[@]}" -eq 0 ]; then
513
+ # Not "yet". A role that owns no folders is a real answer, not an unfinished
514
+ # setup: the architect, the surveyor and the land surveyor own a property of
515
+ # everybody else's folders and none of their own. What they are for begins when
516
+ # there are other cities to reach. Keyed on the windows actually opened, so an
517
+ # agent-first card with agents but no `repos:` field does not trip it.
518
+ echo "You own no folders, so this is the seat window on its own — which is what" >&2
519
+ echo "a $AGENTE day looks like. Your role's work starts when there are other" >&2
520
+ echo "cities to reach: that is the bus." >&2
521
+ echo >&2
522
+ echo "If that is wrong and you do own folders: ./bin/seat --repos" >&2
523
+ fi
524
+
525
+ tmux select-window -t "$SESSION:seat"
526
+ exec tmux attach -d -t "$SESSION"
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """Where a seat's settings come from, for the Python side of the plugin.
3
+
4
+ The shell has scripts/city-env.sh; this is the same order for the same keys, so
5
+ there is one answer to "which city, which token, which data" and not one per
6
+ script. Both reporters used to carry their own copy, and a copy is a thing that
7
+ drifts.
8
+
9
+ Order: the environment wins — that is how the MCP server passes user config —
10
+ then the .env the setup writes, then the OS keychain for the token alone.
11
+ """
12
+ import os
13
+ import subprocess
14
+
15
+ import cities
16
+
17
+ CANAL = os.environ.get('CITY_DIR', os.path.expanduser('~/.claude/channels/city-bus'))
18
+ LLAVERO = 'city@agents-city'
19
+
20
+
21
+ def fichero():
22
+ """The keys in ~/.claude/channels/city-bus/.env, or an empty dict."""
23
+ d = {}
24
+ try:
25
+ with open(os.path.join(CANAL, '.env')) as f:
26
+ for linea in f:
27
+ if '=' in linea and not linea.strip().startswith('#'):
28
+ k, v = linea.split('=', 1)
29
+ d[k.strip()] = v.strip().strip('"').strip("'")
30
+ except OSError:
31
+ pass
32
+ return d
33
+
34
+
35
+ def ajuste(clave, defecto=''):
36
+ return os.environ.get(clave) or fichero().get(clave) or defecto
37
+
38
+
39
+ def url():
40
+ """The city's URL. Falls back to the bus's, because unless somebody split
41
+ them they are the same Worker."""
42
+ return ajuste('AGENTS_CITY_URL') or ajuste('CITY_BUS_URL')
43
+
44
+
45
+ def datos():
46
+ """The selected personal city; an explicit external folder still wins."""
47
+ puesto = ajuste('AGENTS_CITY_DATA')
48
+ if puesto and os.path.isdir(os.path.expanduser(puesto)):
49
+ return os.path.expanduser(puesto)
50
+ return cities.actual(os.environ.get('AGENTS_CITY_USER') or cities.usuario_actual())
51
+
52
+
53
+ def token(dado=''):
54
+ """The bus token: given, from the environment, from .env, or the keychain."""
55
+ if dado:
56
+ return dado
57
+ v = ajuste('CITY_BUS_TOKEN')
58
+ if v:
59
+ return v
60
+ try:
61
+ r = subprocess.run(['security', 'find-generic-password', '-s', LLAVERO, '-w'],
62
+ capture_output=True, text=True)
63
+ if r.returncode == 0:
64
+ return r.stdout.strip()
65
+ except (OSError, FileNotFoundError):
66
+ pass
67
+ return ''