agents-city 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2226 -0
  5. package/README.md +2196 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +119 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/doctor +3 -0
  28. package/bin/exit +5 -0
  29. package/bin/hall +73 -0
  30. package/bin/hall.html +498 -0
  31. package/bin/logs +3 -0
  32. package/bin/navegador.mjs +243 -0
  33. package/bin/report.py +14 -0
  34. package/bin/reset +3 -0
  35. package/bin/road +3 -0
  36. package/bin/seat +10 -0
  37. package/bin/serve.py +1579 -0
  38. package/bin/setup.py +753 -0
  39. package/bin/shortcut +3 -0
  40. package/bin/skills +3 -0
  41. package/bin/test +133 -0
  42. package/bin/test-actualiza.py +130 -0
  43. package/bin/test-adapter.py +183 -0
  44. package/bin/test-admision.py +99 -0
  45. package/bin/test-atajos.py +301 -0
  46. package/bin/test-avatar.py +58 -0
  47. package/bin/test-benchmark.py +40 -0
  48. package/bin/test-broker.py +334 -0
  49. package/bin/test-cage.py +374 -0
  50. package/bin/test-card.py +402 -0
  51. package/bin/test-channel.py +379 -0
  52. package/bin/test-cities.py +453 -0
  53. package/bin/test-claude-runtime.py +252 -0
  54. package/bin/test-committee.py +340 -0
  55. package/bin/test-contracts.py +551 -0
  56. package/bin/test-crecimiento.py +164 -0
  57. package/bin/test-demo.py +308 -0
  58. package/bin/test-doctor.py +139 -0
  59. package/bin/test-domains.py +151 -0
  60. package/bin/test-evidencia.py +50 -0
  61. package/bin/test-exit.py +128 -0
  62. package/bin/test-hall-protocol.py +93 -0
  63. package/bin/test-launch.py +169 -0
  64. package/bin/test-live-feed.py +508 -0
  65. package/bin/test-navegador.py +140 -0
  66. package/bin/test-pairing.py +99 -0
  67. package/bin/test-parcels.py +283 -0
  68. package/bin/test-runtime-failures.py +252 -0
  69. package/bin/test-runtime-ui.py +488 -0
  70. package/bin/test-runtime.py +268 -0
  71. package/bin/test-rutas.py +80 -0
  72. package/bin/test-seat.py +1598 -0
  73. package/bin/test-security.py +150 -0
  74. package/bin/test-serve.py +1251 -0
  75. package/bin/test-stress.py +64 -0
  76. package/bin/test-widgets.py +143 -0
  77. package/bin/test-workspace.py +251 -0
  78. package/bin/testlib.py +118 -0
  79. package/bin/tokens.py +14 -0
  80. package/bin/update +3 -0
  81. package/bus/scripts/deploy.sh +60 -0
  82. package/bus/scripts/mint-token.sh +68 -0
  83. package/bus/scripts/setup-dev.sh +79 -0
  84. package/bus/scripts/test-channel.ts +8 -0
  85. package/bus/scripts/test-hub.ts +266 -0
  86. package/bus/scripts/test-local.sh +40 -0
  87. package/bus/scripts/test-queue.ts +132 -0
  88. package/bus/worker/package-lock.json +1971 -0
  89. package/bus/worker/package.json +17 -0
  90. package/bus/worker/src/index.ts +485 -0
  91. package/bus/worker/tsconfig.json +13 -0
  92. package/bus/worker/wrangler.toml +32 -0
  93. package/city/oven/README.md +59 -0
  94. package/city/oven/collect.py +43 -0
  95. package/city/oven/oven.html +4 -0
  96. package/city/scripts/history.py +120 -0
  97. package/city/scripts/seed.py +442 -0
  98. package/city/web/assets/modelos/License.txt +28 -0
  99. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  100. package/city/web/assets/modelos/building-a.glb +0 -0
  101. package/city/web/assets/modelos/building-b.glb +0 -0
  102. package/city/web/assets/modelos/building-c.glb +0 -0
  103. package/city/web/assets/modelos/building-d.glb +0 -0
  104. package/city/web/assets/modelos/building-e.glb +0 -0
  105. package/city/web/assets/modelos/building-f.glb +0 -0
  106. package/city/web/assets/modelos/building-g.glb +0 -0
  107. package/city/web/assets/modelos/building-h.glb +0 -0
  108. package/city/web/assets/sprites/building-a.png +0 -0
  109. package/city/web/assets/sprites/building-b.png +0 -0
  110. package/city/web/assets/sprites/building-c.png +0 -0
  111. package/city/web/assets/sprites/building-d.png +0 -0
  112. package/city/web/assets/sprites/building-e.png +0 -0
  113. package/city/web/assets/sprites/building-f.png +0 -0
  114. package/city/web/assets/sprites/building-g.png +0 -0
  115. package/city/web/assets/sprites/building-h.png +0 -0
  116. package/city/web/assets/sprites/building-i.png +0 -0
  117. package/city/web/assets/sprites/building-j.png +0 -0
  118. package/city/web/assets/sprites/building-k.png +0 -0
  119. package/city/web/assets/sprites/building-l.png +0 -0
  120. package/city/web/assets/sprites/building-m.png +0 -0
  121. package/city/web/assets/sprites/building-n.png +0 -0
  122. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  123. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  124. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  125. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  126. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  127. package/city/web/assets/sprites/catalogo.json +47 -0
  128. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  129. package/city/web/assets/sprites/detail-awning.png +0 -0
  130. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  131. package/city/web/assets/sprites/detail-overhang.png +0 -0
  132. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  133. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  143. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  144. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  145. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  146. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  147. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  148. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  149. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  150. package/city/web/assets/sprites/medidas.json +248 -0
  151. package/city/web/dist/city.js +939 -0
  152. package/city/web/dist/index.html +294 -0
  153. package/city/web/dist/modelos/License.txt +28 -0
  154. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  155. package/city/web/dist/modelos/building-a.glb +0 -0
  156. package/city/web/dist/modelos/building-b.glb +0 -0
  157. package/city/web/dist/modelos/building-c.glb +0 -0
  158. package/city/web/dist/modelos/building-d.glb +0 -0
  159. package/city/web/dist/modelos/building-e.glb +0 -0
  160. package/city/web/dist/modelos/building-f.glb +0 -0
  161. package/city/web/dist/modelos/building-g.glb +0 -0
  162. package/city/web/dist/modelos/building-h.glb +0 -0
  163. package/city/web/dist/sprites/building-a.png +0 -0
  164. package/city/web/dist/sprites/building-b.png +0 -0
  165. package/city/web/dist/sprites/building-c.png +0 -0
  166. package/city/web/dist/sprites/building-d.png +0 -0
  167. package/city/web/dist/sprites/building-e.png +0 -0
  168. package/city/web/dist/sprites/building-f.png +0 -0
  169. package/city/web/dist/sprites/building-g.png +0 -0
  170. package/city/web/dist/sprites/building-h.png +0 -0
  171. package/city/web/dist/sprites/building-i.png +0 -0
  172. package/city/web/dist/sprites/building-j.png +0 -0
  173. package/city/web/dist/sprites/building-k.png +0 -0
  174. package/city/web/dist/sprites/building-l.png +0 -0
  175. package/city/web/dist/sprites/building-m.png +0 -0
  176. package/city/web/dist/sprites/building-n.png +0 -0
  177. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  178. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  179. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  180. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  181. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  182. package/city/web/dist/sprites/catalogo.json +47 -0
  183. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  184. package/city/web/dist/sprites/detail-awning.png +0 -0
  185. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  186. package/city/web/dist/sprites/detail-overhang.png +0 -0
  187. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  188. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  198. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  199. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  200. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  201. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  202. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  203. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  204. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  205. package/city/web/dist/sprites/medidas.json +248 -0
  206. package/city/web/dist-hall/hall.js +1363 -0
  207. package/city/web/index.html +294 -0
  208. package/city/web/package.json +18 -0
  209. package/city/web/sella.py +21 -0
  210. package/city/web/src/activity-actors.ts +47 -0
  211. package/city/web/src/activity.ts +172 -0
  212. package/city/web/src/ayuntamiento.ts +580 -0
  213. package/city/web/src/draw.ts +640 -0
  214. package/city/web/src/game-speech.ts +79 -0
  215. package/city/web/src/hall.ts +1959 -0
  216. package/city/web/src/main.ts +2252 -0
  217. package/city/web/src/oven.ts +124 -0
  218. package/city/web/src/people.ts +244 -0
  219. package/city/web/src/presencia.ts +193 -0
  220. package/city/web/src/puertas.ts +180 -0
  221. package/city/web/src/repo-roles.ts +59 -0
  222. package/city/worker/package.json +16 -0
  223. package/city/worker/schema.sql +117 -0
  224. package/city/worker/src/index.ts +702 -0
  225. package/city/worker/src/square.ts +218 -0
  226. package/city/worker/wrangler.toml +58 -0
  227. package/demo/ada.md +50 -0
  228. package/demo/bruno.md +62 -0
  229. package/demo/camila.md +56 -0
  230. package/demo/city/ada.md +40 -0
  231. package/demo/city/city.yml +8 -0
  232. package/demo/city/parcels.yml +15 -0
  233. package/demo/city/roads.json +4 -0
  234. package/demo/city/units.yml +10 -0
  235. package/demo/clinica/city.yml +8 -0
  236. package/demo/clinica/parcels.yml +15 -0
  237. package/demo/clinica/roads.json +4 -0
  238. package/demo/clinica/units.yml +9 -0
  239. package/demo/clinica/vera.md +46 -0
  240. package/demo/dante.md +54 -0
  241. package/demo/despacho/city.yml +8 -0
  242. package/demo/despacho/marta.md +46 -0
  243. package/demo/despacho/parcels.yml +15 -0
  244. package/demo/despacho/roads.json +4 -0
  245. package/demo/despacho/units.yml +9 -0
  246. package/demo/elsa.md +50 -0
  247. package/demo/farid.md +52 -0
  248. package/demo/greta.md +50 -0
  249. package/demo/hugo.md +49 -0
  250. package/demo/iris.md +49 -0
  251. package/demo/jonas.md +51 -0
  252. package/demo/kira.md +51 -0
  253. package/demo/luca.md +49 -0
  254. package/demo/parcels.yml +92 -0
  255. package/demo/seed.py +20 -0
  256. package/demo/show.py +161 -0
  257. package/demo/stories.py +824 -0
  258. package/demo/units.yml +33 -0
  259. package/docs/agents-first.md +106 -0
  260. package/docs/glossary.md +55 -0
  261. package/docs/map-live-layers.md +73 -0
  262. package/docs/security.md +265 -0
  263. package/docs/self-host.md +151 -0
  264. package/docs/testing.md +205 -0
  265. package/package.json +73 -0
  266. package/plugin/.claude-plugin/plugin.json +62 -0
  267. package/plugin/.mcp.json +15 -0
  268. package/plugin/channel/activity-cli.ts +16 -0
  269. package/plugin/channel/adapter-prompts.ts +113 -0
  270. package/plugin/channel/adapter.js +4292 -0
  271. package/plugin/channel/adapter.ts +133 -0
  272. package/plugin/channel/bus.js +20188 -0
  273. package/plugin/channel/bus.ts +131 -0
  274. package/plugin/channel/city-config.ts +125 -0
  275. package/plugin/channel/claude-channel.ts +58 -0
  276. package/plugin/channel/cli-args.ts +75 -0
  277. package/plugin/channel/client.js +4200 -0
  278. package/plugin/channel/client.ts +38 -0
  279. package/plugin/channel/committee/activity.ts +256 -0
  280. package/plugin/channel/committee/collection.ts +154 -0
  281. package/plugin/channel/committee/decision.ts +175 -0
  282. package/plugin/channel/committee/floor.ts +135 -0
  283. package/plugin/channel/committee/guards.ts +30 -0
  284. package/plugin/channel/committee/history.ts +38 -0
  285. package/plugin/channel/committee/render.ts +102 -0
  286. package/plugin/channel/committee/service.ts +72 -0
  287. package/plugin/channel/committee/storage.ts +92 -0
  288. package/plugin/channel/committee/types.ts +163 -0
  289. package/plugin/channel/committee/view.ts +63 -0
  290. package/plugin/channel/committee-cli.ts +109 -0
  291. package/plugin/channel/delivery-metrics.ts +61 -0
  292. package/plugin/channel/delivery-queue.ts +118 -0
  293. package/plugin/channel/hub/activity-controller.ts +72 -0
  294. package/plugin/channel/hub/activity-feed.ts +169 -0
  295. package/plugin/channel/hub/committee-controller.ts +60 -0
  296. package/plugin/channel/hub/connections.ts +48 -0
  297. package/plugin/channel/hub/diagnostics.ts +67 -0
  298. package/plugin/channel/hub/envelope-validity.ts +109 -0
  299. package/plugin/channel/hub/envelopes.ts +66 -0
  300. package/plugin/channel/hub/lifecycle.ts +91 -0
  301. package/plugin/channel/hub/local-roads.ts +89 -0
  302. package/plugin/channel/hub/remote-roads.ts +165 -0
  303. package/plugin/channel/hub/road-controller.ts +99 -0
  304. package/plugin/channel/hub-client.ts +149 -0
  305. package/plugin/channel/local-hub.js +5995 -0
  306. package/plugin/channel/local-hub.ts +349 -0
  307. package/plugin/channel/map-reporter.ts +48 -0
  308. package/plugin/channel/package-lock.json +1731 -0
  309. package/plugin/channel/package.json +19 -0
  310. package/plugin/channel/protocol.ts +98 -0
  311. package/plugin/channel/road-cli.ts +18 -0
  312. package/plugin/channel/run.sh +15 -0
  313. package/plugin/channel/runtime/claude.ts +458 -0
  314. package/plugin/channel/runtime/codex-config.ts +77 -0
  315. package/plugin/channel/runtime/codex.ts +669 -0
  316. package/plugin/channel/runtime/command.ts +62 -0
  317. package/plugin/channel/runtime/factory.ts +15 -0
  318. package/plugin/channel/runtime/json-rpc.ts +154 -0
  319. package/plugin/channel/runtime/kimi.ts +277 -0
  320. package/plugin/channel/runtime/opencode.ts +248 -0
  321. package/plugin/channel/runtime/process.ts +84 -0
  322. package/plugin/channel/runtime/types.ts +45 -0
  323. package/plugin/channel/runtime-files.ts +73 -0
  324. package/plugin/channel/runtime-gateway.js +6396 -0
  325. package/plugin/channel/runtime-gateway.ts +358 -0
  326. package/plugin/channel/runtime-metrics.ts +52 -0
  327. package/plugin/channel/runtime-subscription.ts +180 -0
  328. package/plugin/channel/terminal-delivery.ts +131 -0
  329. package/plugin/channel/untrusted.ts +60 -0
  330. package/plugin/commands/committee.md +31 -0
  331. package/plugin/commands/exit.md +32 -0
  332. package/plugin/commands/goals.md +20 -0
  333. package/plugin/commands/join.md +18 -0
  334. package/plugin/commands/notice.md +20 -0
  335. package/plugin/commands/propose.md +19 -0
  336. package/plugin/commands/round.md +17 -0
  337. package/plugin/commands/session.md +22 -0
  338. package/plugin/commands/settings.md +25 -0
  339. package/plugin/commands/setup.md +21 -0
  340. package/plugin/commands/team.md +23 -0
  341. package/plugin/domains/custom.md +27 -0
  342. package/plugin/domains/finance.md +31 -0
  343. package/plugin/domains/healthcare.md +34 -0
  344. package/plugin/domains/legal.md +31 -0
  345. package/plugin/domains/marketing.md +33 -0
  346. package/plugin/domains/operations.md +31 -0
  347. package/plugin/domains/research.md +32 -0
  348. package/plugin/domains/sales.md +30 -0
  349. package/plugin/domains/software.md +37 -0
  350. package/plugin/hooks/activity.sh +6 -0
  351. package/plugin/hooks/digging.sh +68 -0
  352. package/plugin/hooks/growth.sh +42 -0
  353. package/plugin/hooks/hooks.json +106 -0
  354. package/plugin/hooks/notice-on-pr.sh +35 -0
  355. package/plugin/hooks/notice-on-stop.sh +80 -0
  356. package/plugin/hooks/notice-pending.sh +58 -0
  357. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  358. package/plugin/hooks/tokens.sh +47 -0
  359. package/plugin/roles/examples/account-executive.md +25 -0
  360. package/plugin/roles/examples/ai-manager.md +34 -0
  361. package/plugin/roles/examples/associate.md +24 -0
  362. package/plugin/roles/examples/brand-lead.md +20 -0
  363. package/plugin/roles/examples/cfo.md +20 -0
  364. package/plugin/roles/examples/city-lead.md +25 -0
  365. package/plugin/roles/examples/clinical-director.md +26 -0
  366. package/plugin/roles/examples/clinical-ops.md +25 -0
  367. package/plugin/roles/examples/clinician.md +25 -0
  368. package/plugin/roles/examples/compliance.md +27 -0
  369. package/plugin/roles/examples/content.md +24 -0
  370. package/plugin/roles/examples/controller.md +27 -0
  371. package/plugin/roles/examples/cpto.md +41 -0
  372. package/plugin/roles/examples/customer-success.md +25 -0
  373. package/plugin/roles/examples/data-engineer.md +37 -0
  374. package/plugin/roles/examples/data.md +60 -0
  375. package/plugin/roles/examples/dev.md +37 -0
  376. package/plugin/roles/examples/devops.md +39 -0
  377. package/plugin/roles/examples/enablement.md +24 -0
  378. package/plugin/roles/examples/ethics.md +24 -0
  379. package/plugin/roles/examples/fin-analytics.md +27 -0
  380. package/plugin/roles/examples/health-compliance.md +25 -0
  381. package/plugin/roles/examples/health-data.md +26 -0
  382. package/plugin/roles/examples/knowledge.md +25 -0
  383. package/plugin/roles/examples/lifecycle.md +26 -0
  384. package/plugin/roles/examples/llm-engineer.md +34 -0
  385. package/plugin/roles/examples/managing-partner.md +20 -0
  386. package/plugin/roles/examples/methods.md +23 -0
  387. package/plugin/roles/examples/operations-lead.md +23 -0
  388. package/plugin/roles/examples/ops.md +24 -0
  389. package/plugin/roles/examples/patient-safety.md +26 -0
  390. package/plugin/roles/examples/performance.md +27 -0
  391. package/plugin/roles/examples/po.md +36 -0
  392. package/plugin/roles/examples/process-owner.md +23 -0
  393. package/plugin/roles/examples/product-design.md +39 -0
  394. package/plugin/roles/examples/program-manager.md +24 -0
  395. package/plugin/roles/examples/quality.md +25 -0
  396. package/plugin/roles/examples/research-director.md +24 -0
  397. package/plugin/roles/examples/research-ops.md +25 -0
  398. package/plugin/roles/examples/researcher.md +25 -0
  399. package/plugin/roles/examples/revenue-lead.md +25 -0
  400. package/plugin/roles/examples/revops.md +26 -0
  401. package/plugin/roles/examples/seo.md +27 -0
  402. package/plugin/roles/examples/specialist.md +24 -0
  403. package/plugin/scripts/actualiza.py +198 -0
  404. package/plugin/scripts/admision.py +165 -0
  405. package/plugin/scripts/apaga.py +232 -0
  406. package/plugin/scripts/atajos.py +506 -0
  407. package/plugin/scripts/avatar.py +211 -0
  408. package/plugin/scripts/broker.py +531 -0
  409. package/plugin/scripts/cage.py +492 -0
  410. package/plugin/scripts/capabilities.py +210 -0
  411. package/plugin/scripts/card.py +322 -0
  412. package/plugin/scripts/cities.py +520 -0
  413. package/plugin/scripts/city-env.sh +90 -0
  414. package/plugin/scripts/city-runtime.sh +87 -0
  415. package/plugin/scripts/city-session.sh +552 -0
  416. package/plugin/scripts/city_env.py +67 -0
  417. package/plugin/scripts/crecimiento.py +140 -0
  418. package/plugin/scripts/deliberations.py +48 -0
  419. package/plugin/scripts/doctor.py +294 -0
  420. package/plugin/scripts/domains.py +201 -0
  421. package/plugin/scripts/evidencia.py +64 -0
  422. package/plugin/scripts/find-repos.sh +111 -0
  423. package/plugin/scripts/gh.py +159 -0
  424. package/plugin/scripts/hall_protocol.py +147 -0
  425. package/plugin/scripts/hook_activity.py +139 -0
  426. package/plugin/scripts/launch.py +94 -0
  427. package/plugin/scripts/logs.py +98 -0
  428. package/plugin/scripts/pairing.py +185 -0
  429. package/plugin/scripts/parcels.py +132 -0
  430. package/plugin/scripts/read-card.py +55 -0
  431. package/plugin/scripts/report.py +201 -0
  432. package/plugin/scripts/reset.py +180 -0
  433. package/plugin/scripts/roads.py +230 -0
  434. package/plugin/scripts/roles.py +274 -0
  435. package/plugin/scripts/runtime_log.py +69 -0
  436. package/plugin/scripts/runtime_processes.py +132 -0
  437. package/plugin/scripts/rutas.py +83 -0
  438. package/plugin/scripts/seat.py +1696 -0
  439. package/plugin/scripts/tokens.py +194 -0
  440. package/plugin/scripts/trust-repos.py +59 -0
  441. package/plugin/scripts/ui.py +280 -0
  442. package/plugin/scripts/units.py +80 -0
  443. package/plugin/scripts/workspace.py +561 -0
  444. package/plugin/skills/city/SKILL.md +191 -0
  445. package/templates/blank.md +39 -0
  446. package/templates/finance.md +44 -0
  447. package/templates/legal.md +45 -0
  448. package/templates/marketing.md +48 -0
  449. package/templates/product.md +45 -0
@@ -0,0 +1,552 @@
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
+ # On Linux the cage is a bubblewrap namespace, and deciding whether the kernel
164
+ # will grant one means actually building one. `cage.py` runs as a fresh process
165
+ # per window, so without this the probe is paid once per agent instead of once
166
+ # per city — and on a kernel that refuses namespaces, paid slowly. Ask once,
167
+ # hand the answer to every window.
168
+ if [ -z "${CITY_CAGE_BWRAP:-}" ] && [ "$(uname -s)" = "Linux" ]; then
169
+ if python3 "$CAGE" check >/dev/null 2>&1; then
170
+ CITY_CAGE_BWRAP=1
171
+ else
172
+ CITY_CAGE_BWRAP=0
173
+ fi
174
+ export CITY_CAGE_BWRAP
175
+ fi
176
+
177
+ jaula_de() { # window, cwd, [broker token file], [colon-joined mount targets] -> prefix or ''
178
+ local args=(line --window "$1" --repo "$2") salida estado
179
+ [ -n "${3:-}" ] && args+=(--token-file "$3")
180
+ [ -n "${4:-}" ] && args+=(--mounts "$4")
181
+ salida="$(python3 "$CAGE" "${args[@]}" 2>&1)"; estado=$?
182
+ # An empty prefix is a normal answer: no cage on this machine, or CITY_CAGE=0.
183
+ # A FAILURE is not — a working directory that covers a sealed root is refused
184
+ # on purpose, and swallowing that refusal launched the window uncaged and
185
+ # silent, which is the one outcome nobody would notice.
186
+ if [ $estado -ne 0 ]; then
187
+ printf ' %s launches WITHOUT a cage: %s\n' "$1" "$salida" >&2
188
+ return 0
189
+ fi
190
+ printf '%s' "$salida"
191
+ }
192
+
193
+ # Which model and effort a window's agent starts with. Three voices, in order:
194
+ # this launch's flags (CITY_MODEL/CITY_EFFORT, set by `seat --model/--effort`),
195
+ # then the card's per-window key (`model.dbt:`), then the card's default
196
+ # (`model:`). Nothing set means no flag at all — the owner's Claude default,
197
+ # which is the right silence.
198
+ motor_de() { # $1 = window name -> prints the extra claude flags
199
+ local ventana="$1" modelo esfuerzo salida=""
200
+ modelo="${CITY_MODEL:-$(python3 "$LEER" "$FICHA" "model.$ventana")}"
201
+ [ -z "$modelo" ] && modelo="$(python3 "$LEER" "$FICHA" model)"
202
+ esfuerzo="${CITY_EFFORT:-$(python3 "$LEER" "$FICHA" "effort.$ventana")}"
203
+ [ -z "$esfuerzo" ] && esfuerzo="$(python3 "$LEER" "$FICHA" effort)"
204
+ [ -n "$modelo" ] && salida=" --model $modelo"
205
+ [ -n "$esfuerzo" ] && salida="$salida --effort $esfuerzo"
206
+ printf '%s' "$salida"
207
+ }
208
+
209
+ runtime_de() { # configured command -> claude | codex | opencode | kimi | terminal | unknown
210
+ local raw="$1" first
211
+ case "$raw" in terminal:*) printf 'terminal'; return ;; esac
212
+ first="${raw%% *}"
213
+ first="${first##*/}"
214
+ case "$first" in
215
+ claude|claude-code) printf 'claude' ;;
216
+ codex) printf 'codex' ;;
217
+ opencode) printf 'opencode' ;;
218
+ kimi|kimi-code) printf 'kimi' ;;
219
+ *) printf 'unknown' ;;
220
+ esac
221
+ }
222
+
223
+ gateway_line() { # actor, cwd, full runtime command, optional auto-approve
224
+ printf '%q ' "$RUNTIME" gateway "$1" "$2" "$3" "${4:-$YOLO}"
225
+ }
226
+
227
+ # Terminal emulators and tmux both have finite input queues. Sending a 1-2 KB
228
+ # shell program as simulated keystrokes can cut it at an arbitrary byte (we saw
229
+ # `--da`, `--dangerously`, and even a lone `-`). Put the exact command in a
230
+ # private launcher and type only its short path into the pane.
231
+ lanza() { # tmux target, actor, cwd, full command
232
+ local target="$1" actor="$2" cwd="$3" command="$4" launcher typed
233
+ launcher="$(python3 "$LAUNCHPY" create --data "$EQUIPO" --actor "$actor" \
234
+ --cwd "$cwd" --client "$CHANNEL_CLIENT" --command "$command")" || return 1
235
+ printf -v typed '%q' "$launcher"
236
+ tmux send-keys -t "$target" -l -- "$typed"
237
+ tmux send-keys -t "$target" C-m
238
+ }
239
+
240
+ # ── Claude windows start one at a time ─────────────────────────────────────
241
+ # Every Claude session on a machine shares one OAuth credential, and refreshing
242
+ # it rotates a single-use refresh token: the first process to refresh wins and
243
+ # the rest are left holding one the server has already invalidated. It does not
244
+ # read as an auth problem — it reads as "you have no quota left" on an account
245
+ # with plenty, and nothing but logging out and back in clears it. Upstream has
246
+ # had it reported many times over (claude-code#24317, #25609, #27933, #48786).
247
+ #
248
+ # One window per repo makes this the worst possible caller: eighteen sessions
249
+ # starting in the same millisecond, all refreshing at once. So the seat's Claude
250
+ # goes first and alone, and every repo window holds its own shell for a moment
251
+ # before starting. The wait happens INSIDE the window, so attaching is still
252
+ # instant and you watch the agents wake one by one; it also spaces out eighteen
253
+ # simultaneous `git fetch`es, which were their own small storm.
254
+ #
255
+ # What this does not fix: a token that expires at four in the afternoon expires
256
+ # for every session already open. That one is upstream's, not ours.
257
+ #
258
+ # CITY_SETTLE=0 CITY_STAGGER=0 everything at once, as it was before
259
+ entero() { # $1 = value, $2 = fallback. A typo must not crash somebody's day.
260
+ case "$1" in
261
+ '' | *[!0-9]*) printf '%s' "$2" ;;
262
+ *) printf '%s' "$1" ;;
263
+ esac
264
+ }
265
+ SETTLE="$(entero "${CITY_SETTLE-}" 8)"
266
+ STAGGER="$(entero "${CITY_STAGGER-}" 1)"
267
+
268
+ # Seconds the Nth repo window waits. The first carries the whole settle, because
269
+ # it is the one that would collide with the seat's refresh; after that a token is
270
+ # already fresh in the store and the rest only need to not arrive as a herd. A
271
+ # negative turn means nobody has refreshed yet — the seat runs something else, so
272
+ # this window IS the first Claude and has nothing to wait for.
273
+ retraso() { # $1 = claude repo windows already scheduled
274
+ if [ "$1" -lt 0 ]; then printf '0'; else printf '%s' "$((SETTLE + $1 * STAGGER))"; fi
275
+ }
276
+
277
+ sync_line() {
278
+ [ "$DO_SYNC" -eq 1 ] || return 0
279
+ 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; '
280
+ }
281
+
282
+ # ── tmux comforts ──────────────────────────────────────────────────────────
283
+ # Applied to the tmux server, not to anybody's ~/.tmux.conf: this does not touch
284
+ # your configuration. They are additions, so they sit alongside whatever you run.
285
+ comodidades() {
286
+ # Mouse: click a window name in the bar to switch, click a pane to focus it,
287
+ # wheel for the scrollback.
288
+ tmux set-option -g mouse on 2>/dev/null
289
+
290
+ # Windows numbered from 1, so Alt+1 is the first one and not the second.
291
+ tmux set-option -g base-index 1 2>/dev/null
292
+ tmux set-option -g renumber-windows on 2>/dev/null
293
+
294
+ # Alt+1..9 jumps to that window, no prefix.
295
+ for n in 1 2 3 4 5 6 7 8 9; do
296
+ tmux bind-key -n "M-$n" select-window -t "$n" 2>/dev/null
297
+ done
298
+ # Alt+left / Alt+right to move in order.
299
+ tmux bind-key -n M-Left previous-window 2>/dev/null
300
+ tmux bind-key -n M-Right next-window 2>/dev/null
301
+
302
+ # Make the status bar say where you are.
303
+ tmux set-option -g status on 2>/dev/null
304
+ tmux set-option -g status-left '[#S] ' 2>/dev/null
305
+ tmux set-option -g status-left-length 20 2>/dev/null
306
+ tmux set-option -g window-status-current-style 'bg=colour4,fg=black,bold' 2>/dev/null
307
+
308
+ # Which window wants you. With one agent per repo you are looking at one window
309
+ # and five are working, so the useful signal is not "what is on screen" but
310
+ # "which tab needs me". Claude rings the terminal bell when it is waiting on you,
311
+ # so bell is what means come here: that tab turns red and stays red until you
312
+ # visit it. Activity is the softer one — output moved — and only underlines.
313
+ #
314
+ # No plugin manager and no third-party plugins for this: it is a handful of tmux
315
+ # options, and installing somebody else's tmux config to get a coloured tab is a
316
+ # bad trade.
317
+ #
318
+ # `-gw`, not `-g`. These four are *window* options, and setting them as server
319
+ # options fails — silently, because of the 2>/dev/null. Half of this block was
320
+ # doing nothing at all: monitor-activity stayed off and the styles stayed default.
321
+ tmux set-option -gw monitor-bell on 2>/dev/null
322
+ tmux set-option -gw monitor-activity on 2>/dev/null
323
+ tmux set-option -gw window-status-bell-style 'fg=colour231,bg=colour160,bold' 2>/dev/null
324
+ tmux set-option -gw window-status-activity-style 'fg=colour222,underscore' 2>/dev/null
325
+ # Session options: where the bell is allowed to come from, and no flashing bar
326
+ # over the pane you are reading.
327
+ tmux set-option -g bell-action other 2>/dev/null
328
+ tmux set-option -g visual-bell off 2>/dev/null
329
+ tmux set-option -g visual-activity off 2>/dev/null
330
+ # A clock, so a frozen status bar is visibly frozen. tmux expands strftime itself,
331
+ # so this needs no shell — `#(date ...)` here spawns a process every interval and
332
+ # was being written with a printf-escaped %% that tmux never understood.
333
+ tmux set-option -g status-interval 5 2>/dev/null
334
+ tmux set-option -g status-right ' %H:%M ' 2>/dev/null
335
+ tmux set-option -g status-right-length 12 2>/dev/null
336
+ }
337
+
338
+ # ── Work out where each agent works ────────────────────────────────────────
339
+ # Agents come first. A card that declares `agents:` drives the agent-first
340
+ # model: each agent's cwd is its workspace folder, and its mounts (symlinks to
341
+ # repos, worktrees or document folders) become the extra writable roots the
342
+ # cage allows. A legacy card with only `repos:` takes the path below, unchanged
343
+ # — every repo is still an agent whose one mount is that repo.
344
+ LOCALIZA="$(dirname "$0")/find-repos.sh"
345
+ faltan=()
346
+ RUTAS=()
347
+ NOMBRES=()
348
+ MONTAJES=()
349
+ AGENTS_RAW="$(python3 "$LEER" "$FICHA" agents 2>/dev/null || true)"
350
+
351
+ if [ -n "$AGENTS_RAW" ] && [ -z "$SOLO" ]; then
352
+ # Agent-first: one `sync-all` pass reads the card once, materialises every
353
+ # agent's workspace and mounts, and emits `slug<TAB>cwd<TAB>targets` per line —
354
+ # so the whole city costs one interpreter start and one card parse, not N+1.
355
+ while IFS=$'\t' read -r slug cwd targets; do
356
+ [ -z "$slug" ] && continue
357
+ RUTAS+=("$cwd")
358
+ NOMBRES+=("$slug")
359
+ MONTAJES+=("$targets")
360
+ # stderr is left attached so workspace.py's "skipping mount <x>" warnings
361
+ # reach the owner — a typo'd or missing mount source must not degrade in
362
+ # silence, the way the legacy path reports missing repos via `faltan`.
363
+ done < <(python3 "$WORKSPACE" sync-all --data "$EQUIPO" --card "$FICHA")
364
+ else
365
+ REPOS=()
366
+ # bash 3.2 — the one macOS ships — blows up on "${REPOS[@]}" when it is empty.
367
+ [ -n "$REPOS_RAW" ] && IFS=',' read -ra REPOS <<< "$REPOS_RAW"
368
+ for r in ${REPOS[@]+"${REPOS[@]}"}; do
369
+ r="$(echo "$r" | xargs)"; [ -z "$r" ] && continue
370
+ # 1) the usual place; 2) wherever you keep it, found by its remote.
371
+ path="$CODE_DIR/$r"
372
+ [ -d "$path" ] || path="$("$LOCALIZA" "$r" 2>/dev/null)"
373
+ if [ -z "$path" ] || [ ! -d "$path" ]; then faltan+=("$r"); continue; fi
374
+ RUTAS+=("$path")
375
+ # One canonical slug is also the tmux name, engine-key suffix and bus actor.
376
+ NOMBRES+=("$(python3 "$LEER" --window "$r")")
377
+ MONTAJES+=("")
378
+ done
379
+ fi
380
+
381
+ # Trust every folder up front, so no window sits waiting on the dialog.
382
+ python3 "$(dirname "$0")/trust-repos.py" "$EQUIPO" ${RUTAS[@]+"${RUTAS[@]}"}
383
+
384
+ # ── The seat window: this city's identity on its roads ─────────────────────
385
+ "$RUNTIME" ensure >/dev/null
386
+ comodidades
387
+ tmux new-session -d -s "$SESSION" -n seat -c "$EQUIPO"
388
+
389
+ # Where the cards live, told to the session rather than assumed.
390
+ #
391
+ # A window inherits the environment of the tmux *server*, not of whoever ran this.
392
+ # So on any machine where a tmux server was already up, AGENTS_CITY_DATA never
393
+ # reached the windows and the plugin inside them looked for the cards in the
394
+ # default place. Set on the session, and also written in front of each command
395
+ # below, because the two cost nothing and the failure is silent.
396
+ tmux set-environment -t "$SESSION" AGENTS_CITY_DATA "$EQUIPO" 2>/dev/null
397
+ tmux set-environment -t "$SESSION" AGENTS_CITY_HOME "${AGENTS_CITY_HOME:-$HOME/.agents-city}" 2>/dev/null
398
+ tmux set-environment -t "$SESSION" AGENTS_CITY_USER "$USUARIO" 2>/dev/null
399
+ tmux set-environment -t "$SESSION" CITY_ADDRESS "$ADDRESS" 2>/dev/null
400
+ tmux set-environment -t "$SESSION" CITY_SEAT_NAME "$SEAT_NAME" 2>/dev/null
401
+
402
+ # The seat runs Claude unless the card says otherwise. `runs.seat` is the same key
403
+ # the repo windows use, and it makes a city with no Claude in it possible at all:
404
+ # what that seat gives up is the `/city:` commands, which are Claude's, and what it
405
+ # keeps is the folder, the identity, and everything the terminal does.
406
+ SEAT_OTRO="$(python3 "$LEER" "$FICHA" "runs.seat")"
407
+ turno=0
408
+ [ -n "$SEAT_OTRO" ] && [ "$(runtime_de "$SEAT_OTRO")" != claude ] && turno=-1
409
+ # If the seat runs another engine, the first repo window is the first Claude.
410
+ if [ "$RUN_CLAUDE" -eq 1 ]; then
411
+ if [ -n "$SEAT_OTRO" ]; then
412
+ SEAT_RUNTIME="$(runtime_de "$SEAT_OTRO")"
413
+ if [ "$SEAT_RUNTIME" = claude ]; then
414
+ CLAUDE_SEAT="$SEAT_OTRO --name $SEAT_NAME --settings '$SETTINGS'$NO_PEER_TOOLS$SEAT_YOLO_FLAG"
415
+ lanza "$SESSION:seat" seat "$EQUIPO" \
416
+ "$(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")"
417
+ elif [ "$SEAT_RUNTIME" = codex ] || [ "$SEAT_RUNTIME" = opencode ] || [ "$SEAT_RUNTIME" = kimi ]; then
418
+ lanza "$SESSION:seat" seat "$EQUIPO" \
419
+ "$(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")"
420
+ elif [ "$SEAT_RUNTIME" = terminal ]; then
421
+ SEAT_COMMAND="${SEAT_OTRO#terminal:}"
422
+ lanza "$SESSION:seat" seat "$EQUIPO" \
423
+ "$(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"
424
+ "$RUNTIME" fallback seat "$SESSION:seat" "${SEAT_COMMAND%% *}"
425
+ else
426
+ tmux send-keys -t "$SESSION:seat" \
427
+ "echo 'No native Agents City gateway for: $SEAT_OTRO. Use terminal:$SEAT_OTRO only if you explicitly accept terminal injection.'" C-m
428
+ fi
429
+ else
430
+ CLAUDE_SEAT="claude --name $SEAT_NAME$(motor_de seat) --settings '$SETTINGS'$NO_PEER_TOOLS$SEAT_YOLO_FLAG"
431
+ lanza "$SESSION:seat" seat "$EQUIPO" \
432
+ "$(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")"
433
+ fi
434
+ fi
435
+
436
+ # The broker, opt-in with CITY_BROKER=1: caged windows cannot read the gh
437
+ # token — on purpose — so PRs and pushes go through a small owner-side process
438
+ # that validates (declared repo only, never the default branch) and audits.
439
+ # Each window gets its own token file, and its cage re-allows exactly that one.
440
+ BROKER_URL=""
441
+ if [ "${CITY_BROKER:-0}" = "1" ]; then
442
+ python3 "$BROKERPY" stop --data "$EQUIPO" >/dev/null 2>&1 || true
443
+ nohup python3 "$BROKERPY" serve --data "$EQUIPO" >/dev/null 2>&1 &
444
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
445
+ BROKER_URL="$(python3 "$BROKERPY" url --data "$EQUIPO" 2>/dev/null || true)"
446
+ [ -n "$BROKER_URL" ] && break
447
+ sleep 0.3
448
+ done
449
+ [ -z "$BROKER_URL" ] && echo "WARNING: the credential broker did not start; windows launch without it." >&2
450
+ fi
451
+
452
+ # ── One window per repo, reachable only through its chaired bus identity ───
453
+ #
454
+ # Claude by default, with its engine keys and its yolo flag. But a window can
455
+ # run a supported agent CLI instead — `runs.dbt: codex` on the card. Claude uses
456
+ # its persistent stream-json process; Codex, OpenCode and Kimi use their native
457
+ # gateways. None receives assignments through tmux. An unknown CLI must be
458
+ # prefixed with `terminal:` to opt into the visibly labelled compatibility path.
459
+ i=0
460
+ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
461
+ win="${NOMBRES[$i]}"
462
+ i=$((i + 1))
463
+ # Professional specialty is separate from bus authority. Every repo actor is
464
+ # still a member; only `seat` is chair. A malformed or legacy value resolves
465
+ # to the explicit blank role in read-card.py.
466
+ ROL_REPO="$(python3 "$LEER" --actor-role "$FICHA" "$win")"
467
+ # This window's cage and, when the broker runs, its own single-repo token.
468
+ # The token travels as a file path, never on the command line, and the cage
469
+ # profile re-allows reading exactly that file and nothing else in the broker.
470
+ TOKEN_FILE=""
471
+ BROKER_ENV=""
472
+ if [ -n "$BROKER_URL" ]; then
473
+ TOKEN_FILE="$(python3 "$BROKERPY" mint --data "$EQUIPO" "$win" --repo "$path" --file-only 2>/dev/null || true)"
474
+ [ -n "$TOKEN_FILE" ] && BROKER_ENV="CITY_BROKER_URL=$BROKER_URL CITY_BROKER_TOKEN_FILE=$TOKEN_FILE "
475
+ fi
476
+ MOUNTS_WIN="${MONTAJES[$((i - 1))]:-}"
477
+ JAULA="$(jaula_de "$win" "$path" "$TOKEN_FILE" "$MOUNTS_WIN")"
478
+ CAGE_RUNTIME_ENV=""
479
+ [ -n "$JAULA" ] && CAGE_RUNTIME_ENV="env CITY_OUTER_CAGE=1 "
480
+ tmux new-window -t "$SESSION" -n "$win" -c "$path"
481
+ if [ "$RUN_CLAUDE" -eq 1 ]; then
482
+ OTRO="$(python3 "$LEER" "$FICHA" "runs.$win")"
483
+ if [ -n "$OTRO" ]; then
484
+ KIND="$(runtime_de "$OTRO")"
485
+ if [ "$KIND" = claude ]; then
486
+ espera="$(retraso "$turno")"
487
+ turno=$((turno + 1))
488
+ if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
489
+ CLAUDE_REPO="$OTRO --name $SESSION-$win --settings '$SETTINGS'$NO_PEER_TOOLS$YOLO_FLAG"
490
+ lanza "$SESSION:$win" "$win" "$path" \
491
+ "$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")"
492
+ elif [ "$KIND" = codex ] || [ "$KIND" = opencode ] || [ "$KIND" = kimi ]; then
493
+ # Native servers have their own credentials and do not share Claude's OAuth race.
494
+ # Codex stays outside the outer cage ON macOS ONLY: its node_repl MCP
495
+ # applies its own sandbox and the macOS kernel rejects that nested
496
+ # sandbox_apply. That is a seatbelt constraint, not a fact about Codex —
497
+ # bubblewrap nests fine, and an unconditional exemption meant a Codex
498
+ # window on Linux could read ~/.ssh outright while every other window in
499
+ # the same city had it sealed. Codex's app-server still receives
500
+ # workspace-write below, which bounds writes but never reads.
501
+ RUNTIME_CAGE="$JAULA"
502
+ RUNTIME_CAGE_ENV="$CAGE_RUNTIME_ENV"
503
+ if [ "$KIND" = codex ] && [ "$(uname -s)" = "Darwin" ]; then
504
+ RUNTIME_CAGE=""
505
+ RUNTIME_CAGE_ENV=""
506
+ fi
507
+ lanza "$SESSION:$win" "$win" "$path" \
508
+ "$(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")"
509
+ elif [ "$KIND" = terminal ]; then
510
+ FALLBACK_COMMAND="${OTRO#terminal:}"
511
+ lanza "$SESSION:$win" "$win" "$path" \
512
+ "$(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"
513
+ "$RUNTIME" fallback "$win" "$SESSION:$win" "${FALLBACK_COMMAND%% *}"
514
+ else
515
+ tmux send-keys -t "$SESSION:$win" \
516
+ "echo 'No native Agents City gateway for: $OTRO. Use terminal:$OTRO only if you explicitly accept terminal injection.'" C-m
517
+ fi
518
+ else
519
+ espera="$(retraso "$turno")"
520
+ turno=$((turno + 1))
521
+ if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
522
+ CLAUDE_REPO="claude --name $SESSION-$win$(motor_de "$win") --settings '$SETTINGS'$NO_PEER_TOOLS$YOLO_FLAG"
523
+ lanza "$SESSION:$win" "$win" "$path" \
524
+ "$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")"
525
+ fi
526
+ fi
527
+ done
528
+
529
+ if [ ${#faltan[@]-0} -gt 0 ]; then
530
+ echo >&2
531
+ echo "I could not find these repos on this machine (window skipped):" >&2
532
+ printf ' %s\n' "${faltan[@]}" >&2
533
+ echo >&2
534
+ echo "If they live somewhere unusual: CITY_SEARCH_IN=/where/they/are $(basename "$0")" >&2
535
+ echo "If you simply do not have them, clone them anywhere and run this again." >&2
536
+ fi
537
+
538
+ if [ "${#RUTAS[@]}" -eq 0 ]; then
539
+ # Not "yet". A role that owns no folders is a real answer, not an unfinished
540
+ # setup: the architect, the surveyor and the land surveyor own a property of
541
+ # everybody else's folders and none of their own. What they are for begins when
542
+ # there are other cities to reach. Keyed on the windows actually opened, so an
543
+ # agent-first card with agents but no `repos:` field does not trip it.
544
+ echo "You own no folders, so this is the seat window on its own — which is what" >&2
545
+ echo "a $AGENTE day looks like. Your role's work starts when there are other" >&2
546
+ echo "cities to reach: that is the bus." >&2
547
+ echo >&2
548
+ echo "If that is wrong and you do own folders: ./bin/seat --repos" >&2
549
+ fi
550
+
551
+ tmux select-window -t "$SESSION:seat"
552
+ 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 ''