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
package/bin/setup.py ADDED
@@ -0,0 +1,753 @@
1
+ #!/usr/bin/env python3
2
+ """Agents City setup — create or open one personal city.
3
+
4
+ ./bin/setup.py create/open home in the town hall
5
+ ./bin/setup.py --city lab create/open another local city
6
+ ./bin/setup.py --tui configure its owner seat in the terminal
7
+ ./bin/setup.py --demo load the example map instead
8
+
9
+ A city is one autonomous owner seat: identity, role, one goal, support repo
10
+ agents, explicit roads, and the skills those repos already provide. Legacy v1
11
+ parsers remain below for importing old map fixtures, but the public setup command
12
+ cannot create a multiple-person city.
13
+
14
+ The Hall is the default. ``--tui`` hands the selected city to the same seat
15
+ onboarding used everywhere else. Both resolve identity and storage through
16
+ ``cities.py`` so opening one city cannot silently write another.
17
+
18
+ Functions below ``main``'s dependencies still parse v1 map templates and fixtures;
19
+ they are not a second public setup path.
20
+ """
21
+
22
+ import argparse
23
+ import glob
24
+ import os
25
+ import re
26
+ import sys
27
+
28
+ AQUI = os.path.dirname(os.path.abspath(__file__))
29
+ # The shared core lives in plugin/scripts, because that is the folder that gets
30
+ # installed on everybody's machine. bin/* always runs from a clone, so it can
31
+ # reach in; the plugin can never reach out.
32
+ GUIONES = os.path.join(AQUI, "..", "plugin", "scripts")
33
+ sys.path.insert(0, GUIONES)
34
+ sys.path.insert(0, AQUI)
35
+ import ui # noqa: E402
36
+ import gh # noqa: E402 identity and GitHub, shared with the seat
37
+ import roles # noqa: E402 the role catalogue and the bus suffixes
38
+ import domains # noqa: E402 domain-first catalogue shared with the seat
39
+ import card # noqa: E402 one reader and writer of a card
40
+ import units # noqa: E402 one writer of the districts file
41
+ import cities # noqa: E402 personal city ownership and selection
42
+
43
+ PASOS = 6
44
+ PALETA = [
45
+ ("3fb8a0", "teal"),
46
+ ("8f7ae6", "violet"),
47
+ ("e08a3c", "amber"),
48
+ ("4a9ede", "blue"),
49
+ ("d1728f", "rose"),
50
+ ("7fb069", "green"),
51
+ ("c9a227", "gold"),
52
+ ("6b8cae", "slate"),
53
+ ]
54
+
55
+
56
+ def plantillas():
57
+ """Compatibility shape over the domain packs used by every public door."""
58
+ return [
59
+ {
60
+ k: p[k]
61
+ for k in (
62
+ "id",
63
+ "name",
64
+ "summary",
65
+ "parcel",
66
+ "source",
67
+ "grows",
68
+ "grow_cmd",
69
+ "roles",
70
+ "units",
71
+ )
72
+ }
73
+ for p in domains.catalogo()
74
+ ]
75
+
76
+
77
+ def leeRol(ident):
78
+ """The one-line domain of a role, straight from its own file: the catalogue and
79
+ the file cannot disagree if only one of them exists."""
80
+ f = os.path.join(AQUI, "..", "plugin", "roles", "examples", f"{ident}.md")
81
+ if not os.path.exists(f):
82
+ return ""
83
+ t = open(f).read()
84
+ m = re.search(r"## Domain\n\n(.+?)(?:\n\n|$)", t, re.S)
85
+ return " ".join(re.sub(r"\*\*|`", "", m.group(1)).split())[:70] if m else ""
86
+
87
+
88
+ # The catalogue and the suffixes come from roles.py, which both doors read.
89
+ ROLES = roles.CATALOGO
90
+ # Module level, not inside recoge(): that function has a local called `roles`
91
+ # (the user's selection), which shadows the module and made this line raise
92
+ # UnboundLocalError there — the same shadowing family as the `marcadas |=` bug.
93
+ ARQUITECTOS = roles.ARQUITECTOS
94
+
95
+
96
+ # Identity and GitHub live in gh.py, which both doors read. These are the names the
97
+ # rest of this file uses; the logic is not here any more.
98
+ sh = gh.sh
99
+ orgs_de_gh = gh.orgs
100
+ repos_de_org = gh.repos
101
+ gente_de_org = gh.miembros
102
+ usuario_de_correo = gh.usuario_de_correo
103
+ es_maquina = gh.es_maquina
104
+ gente_de_repos = gh.gente_de_repos
105
+
106
+
107
+ def repos_del_disco(raiz):
108
+ """Every git repo under a path, by its remote. Two levels is enough in
109
+ practice and keeps this from crawling a whole home directory."""
110
+ fuera = []
111
+ for base, dirs, _ in os.walk(raiz):
112
+ prof = base[len(raiz) :].count(os.sep)
113
+ if prof > 3:
114
+ dirs[:] = []
115
+ continue
116
+ es_worktree = os.path.isfile(os.path.join(base, ".git"))
117
+ if ".git" in dirs or es_worktree:
118
+ dirs[:] = []
119
+ remoto = sh(["git", "-C", base, "remote", "get-url", "origin"]).strip()
120
+ nombre = (
121
+ remoto.rsplit("/", 1)[-1].removesuffix(".git") if remoto else os.path.basename(base)
122
+ )
123
+ if es_worktree:
124
+ # A linked worktree: one repo, several working folders — each its
125
+ # own parcel-to-be. Named repo@branch so they stay distinct.
126
+ rama = sh(["git", "-C", base, "branch", "--show-current"]).strip()
127
+ nombre = f"{nombre}@{rama or os.path.basename(base)}"
128
+ ultimo = sh(["git", "-C", base, "log", "-1", "--format=%ad", "--date=short"]).strip()
129
+ fuera.append((nombre, base.replace(os.path.expanduser("~"), "~"), ultimo))
130
+ return sorted(fuera)
131
+
132
+
133
+ def carpetas_del_disco(raiz):
134
+ """Any folder, not only git repos.
135
+
136
+ A city does not have to be made of code. A folder per campaign, per client,
137
+ per process — the map treats them the same. What each one needs is its own
138
+ descriptor, which the wizard writes into it.
139
+ """
140
+ fuera = []
141
+ candidatas = glob.glob(os.path.join(raiz, "*")) + glob.glob(os.path.join(raiz, "*", "*"))
142
+ for base in sorted(candidatas):
143
+ if not os.path.isdir(base) or os.path.basename(base).startswith("."):
144
+ continue
145
+ hijos = [x for x in os.listdir(base) if not x.startswith(".")]
146
+ if not hijos:
147
+ continue
148
+ es_git = os.path.isdir(os.path.join(base, ".git"))
149
+ detalle = f"{len(hijos)} items" + (" · git" if es_git else "")
150
+ fuera.append((os.path.basename(base), base.replace(os.path.expanduser("~"), "~"), detalle))
151
+ # Deduplicate by name, keeping the shallowest: a folder and its child with the
152
+ # same name is almost always the same thing seen twice.
153
+ vistos, limpio = set(), []
154
+ for n, ruta, det in fuera:
155
+ if n in vistos:
156
+ continue
157
+ vistos.add(n)
158
+ limpio.append((n, ruta, det))
159
+ return limpio[:300]
160
+
161
+
162
+ # ── the wizard ─────────────────────────────────────────────────────────────
163
+ def paso_tipo(d):
164
+ # 1. work domain
165
+ plants = plantillas()
166
+ if not plants:
167
+ return False
168
+ kind = ui.una(
169
+ "What domain does this city work in?",
170
+ [(p["id"], p["name"], p["summary"][:70]) for p in plants],
171
+ 1,
172
+ PASOS,
173
+ "This decides which roles you are offered, what a parcel is, and what "
174
+ '"growth" means. Nothing here is permanent — every piece of it is a file '
175
+ "you can edit afterwards.",
176
+ )
177
+ if kind is None:
178
+ return False
179
+ tpl = next(p for p in plants if p["id"] == kind)
180
+ d["tpl"] = tpl
181
+ d["kind"] = kind # compatibility input; `escribe` persists it as `domain`
182
+ d["grow_cmd"] = tpl["grow_cmd"]
183
+
184
+ if not ui.pantalla(
185
+ tpl["name"],
186
+ f"{tpl['summary']}\n\n"
187
+ f"A parcel here is {tpl['parcel']}. A house grows with {tpl['grows']}.\n\n"
188
+ "The map does not care what the number counts — merged pull requests, published "
189
+ "pieces, filed documents, closed periods. It cares that the number is real and "
190
+ "that nobody has to be asked for it. Which is why each folder ends up carrying "
191
+ "its own descriptor: the unit it serves, who answers for it, and the one command "
192
+ "that returns its number.",
193
+ 1,
194
+ PASOS,
195
+ ):
196
+ return False
197
+ return True
198
+
199
+
200
+ def paso_fuente(d):
201
+ tpl = d["tpl"]
202
+ # 2. where the parcels come from
203
+ fuentes = []
204
+ if "github" in tpl["source"]:
205
+ fuentes.append(("org", "From a GitHub organisation", "needs the gh CLI, logged in"))
206
+ fuentes += [
207
+ ("disco", "From folders on my disk", "any folder, not only git repos"),
208
+ ("saltar", "Skip for now", "you can add parcels later by hand"),
209
+ ]
210
+ # Not "Where are your {parcel}s?": pluralising whatever the template calls a
211
+ # parcel produced "Where are your a slice of a repos?". The project has its own
212
+ # word for this, so use it and say underneath what it means here.
213
+ cosa = re.sub(r"\s*\(.*\)\s*$", "", tpl["parcel"]).strip()
214
+ fuente = ui.una(
215
+ "Where do your parcels come from?",
216
+ fuentes,
217
+ 2,
218
+ PASOS,
219
+ f"One house per parcel, and here a parcel is {cosa}. Pick where to "
220
+ "read the list from — and it does not have to be code: a folder per "
221
+ "campaign, per client or per process works exactly the same.",
222
+ )
223
+ if fuente is None:
224
+ return False
225
+
226
+ candidatos = []
227
+ if fuente == "org":
228
+ orgs = orgs_de_gh()
229
+ if not orgs:
230
+ ui.pantalla(
231
+ "No organisations found",
232
+ "The gh CLI did not return any organisation. Either it is not "
233
+ "logged in (`gh auth login`) or your account has none.\n\n"
234
+ "Falling back to reading from disk.",
235
+ 2,
236
+ PASOS,
237
+ )
238
+ fuente = "disco"
239
+ else:
240
+ org = ui.una("Which organisation?", [(o, o, "") for o in orgs], 2, PASOS)
241
+ if org is None:
242
+ return False
243
+ d["org"] = org
244
+ candidatos = [(n, f"{desc}", f"pushed {p}") for n, desc, p in repos_de_org(org)]
245
+ if fuente == "disco":
246
+ raiz = ui.pide(
247
+ "Folder to scan",
248
+ os.path.expanduser("~/code"),
249
+ ayuda="Looks for git repos, up to three levels deep.",
250
+ )
251
+ if raiz is None:
252
+ return False
253
+ raiz = os.path.expanduser(raiz)
254
+ hallados = repos_del_disco(raiz) if "github" in tpl["source"] else carpetas_del_disco(raiz)
255
+ candidatos = [(n, ruta, f"last commit {f}" if f else "") for n, ruta, f in hallados]
256
+ d["rutas"] = {n: os.path.expanduser(ruta) for n, ruta, _ in hallados}
257
+
258
+ if candidatos:
259
+ elegidos = ui.elige(
260
+ "Which parcels are part of your city?",
261
+ candidatos,
262
+ step=2,
263
+ of=PASOS,
264
+ ayuda="Space to pick, / to filter, a for all. You can trim this later.",
265
+ )
266
+ if elegidos is None:
267
+ return False
268
+ d["repos"] = elegidos
269
+ return True
270
+
271
+
272
+ def paso_unidades(d):
273
+ tpl = d["tpl"]
274
+ # 2. business units
275
+ if not ui.pantalla(
276
+ "Your business units",
277
+ "These become the districts of the map: each one gets its own patch of "
278
+ "ground, its own colour and its own banner.\n\n"
279
+ "Two districts come for free and you should keep both. The lab holds "
280
+ "research that does not ship yet and is needed — left among the shared "
281
+ 'repos it reads as waste. And "no unit" holds the code that serves several '
282
+ "units at once, which in most companies is the biggest district of all.\n\n"
283
+ "If your company has no units, keep just those two and everything lands in "
284
+ "the right place anyway.",
285
+ 3,
286
+ PASOS,
287
+ ):
288
+ return False
289
+
290
+ if tpl["units"]:
291
+ sugeridas = ui.elige(
292
+ "Suggested units for a " + tpl["name"].lower(),
293
+ [(u, u, f"#{c}") for u, c in tpl["units"]],
294
+ marcadas=[u for u, _ in tpl["units"]],
295
+ step=3,
296
+ of=PASOS,
297
+ ayuda="Keep the ones that fit. You add your own on the next screen.",
298
+ )
299
+ if sugeridas is None:
300
+ return False
301
+ for u, c in tpl["units"]:
302
+ if u in sugeridas:
303
+ d["unidades"].append(
304
+ {"id": re.sub(r"[^a-z0-9]+", "-", u.lower()).strip("-"), "name": u, "color": c}
305
+ )
306
+
307
+ while True:
308
+ nombre = ui.pide("Another unit (empty to finish)", "")
309
+ if not nombre:
310
+ break
311
+ ident = re.sub(r"[^a-z0-9]+", "-", nombre.lower()).strip("-")[:20]
312
+ color = ui.una(f"Colour for {nombre}", [(c, f"{n} #{c}", "") for c, n in PALETA], 3, PASOS)
313
+ d["unidades"].append({"id": ident, "name": nombre, "color": color or "c8b48a"})
314
+ if len(d["unidades"]) >= 8:
315
+ break
316
+ return True
317
+
318
+
319
+ def paso_roles(d):
320
+ tpl = d["tpl"]
321
+ # 3. roles
322
+ if not ui.pantalla(
323
+ "Which roles exist here?",
324
+ "You already chose the work domain. A role is one responsibility inside "
325
+ "it: what that seat asks others, and what should reach it when a change "
326
+ "touches its property.\n\n"
327
+ "The roles below come from that domain pack, not a universal schema. "
328
+ "Pick the ones that map "
329
+ "to your team, drop the rest, and rename them afterwards in your data "
330
+ "repo: they are markdown files.\n\n"
331
+ "The ones marked cross-cutting own a property of every repo. The rest own "
332
+ "repos.",
333
+ 4,
334
+ PASOS,
335
+ ):
336
+ return False
337
+ catalogo = {r: (t, d2) for r, t, d2, _ in ROLES}
338
+ ofrecidos = [
339
+ (r, catalogo.get(r, (r, ""))[0] + f" ({r})", catalogo.get(r, ("", ""))[1] or leeRol(r))
340
+ for r, _ in tpl["roles"]
341
+ ]
342
+ while True:
343
+ roles = ui.elige(
344
+ "Roles",
345
+ ofrecidos,
346
+ marcadas=[r for r, m in tpl["roles"] if m],
347
+ step=4,
348
+ of=PASOS,
349
+ minimo=1,
350
+ ayuda="One of them has to be the architect — whoever sets the goals.",
351
+ )
352
+ if roles is None:
353
+ return False
354
+ if set(roles) & ARQUITECTOS or not ui.TTY:
355
+ break
356
+ # Not a style rule. Goals are the one thing this system keeps whatever kind
357
+ # of city you build, and they are agreed between the architect and each
358
+ # person. Without that seat there is nobody to agree them with, and every
359
+ # round degrades into a status report.
360
+ ui.pantalla(
361
+ "You need an architect",
362
+ "The goals are the part that does not change: whatever your city is "
363
+ "made of, what holds it together is a goal per person, with the "
364
+ "command that measures it, agreed between the architect and them.\n\n"
365
+ "Without that seat there is nobody to agree them with, and every round "
366
+ "turns into a status report — which is the thing this was built to "
367
+ "avoid.\n\n"
368
+ "Pick one of: " + ", ".join(sorted(ARQUITECTOS & {r for r, _ in tpl["roles"]})),
369
+ 4,
370
+ PASOS,
371
+ )
372
+ d["roles"] = roles
373
+ return True
374
+
375
+
376
+ def paso_gente(d):
377
+ # 4. people
378
+ if not ui.pantalla(
379
+ "Who is on the team?",
380
+ "One card per person: their role, the repos they answer for, and their "
381
+ "goals. The card is what a round reads to know who to ask about what.\n\n"
382
+ "Each person should run /city:join themselves later — the repos they own "
383
+ "is the one thing worth confirming first-hand. What you do here is create "
384
+ "the cards so the team exists.",
385
+ 5,
386
+ PASOS,
387
+ ):
388
+ return False
389
+
390
+ posibles = []
391
+ if d["org"]:
392
+ posibles = [(u, u, "org member") for u in gente_de_org(d["org"])]
393
+ if not posibles and d["rutas"]:
394
+ posibles = [
395
+ (u, f"{n} ({u})", f"{v} commits")
396
+ for u, n, v in gente_de_repos([d["rutas"][r] for r in d["repos"] if r in d["rutas"]])
397
+ ]
398
+ if posibles:
399
+ quienes = ui.elige(
400
+ "Pick the people",
401
+ posibles,
402
+ step=5,
403
+ of=PASOS,
404
+ ayuda="From your org, or from the commit history of the repos you picked.",
405
+ )
406
+ if quienes is None:
407
+ return False
408
+ else:
409
+ quienes = []
410
+ while True:
411
+ u = ui.pide("Username (empty to finish)", "")
412
+ if not u:
413
+ break
414
+ quienes.append(u)
415
+
416
+ for u in quienes:
417
+ rol = ui.una(
418
+ f"Role for {u}",
419
+ [(r, dict((x[0], x[1]) for x in ROLES).get(r, r), "") for r in d["roles"]],
420
+ 5,
421
+ PASOS,
422
+ )
423
+ if rol is None:
424
+ return False
425
+ d["gente"].append({"user": u, "role": rol})
426
+ return True
427
+
428
+
429
+ def paso_objetivo(d):
430
+ # 6. the first goal
431
+ if d["gente"] and ui.pantalla(
432
+ "The one thing that does not change",
433
+ "Whatever your city is made of — repos, campaigns, matters, processes — what "
434
+ "holds it together is the same: one goal per person, with the command that "
435
+ "measures it, agreed between the architect and them.\n\n"
436
+ 'A goal you cannot measure is not a goal here. "Improve performance" cannot be '
437
+ 'accepted or refused with any judgement; "the p95 drops from 800 ms to 400 ms" '
438
+ "can. And the measure is written, not described: if the signal can be pulled "
439
+ "with a command, that command is part of the goal, and it gets run before "
440
+ "anything is agreed.\n\n"
441
+ "Set the first one now and the rest have a shape to follow. Skip it and the "
442
+ "city still works — it just cannot tell you whether anything is going well.",
443
+ 6,
444
+ PASOS,
445
+ "enter to set one · q to skip",
446
+ ):
447
+ quien = ui.una(
448
+ "Whose goal?", [(g["user"], g["user"], g["role"]) for g in d["gente"]], 6, PASOS
449
+ )
450
+ if quien:
451
+ titulo = ui.pide(
452
+ "The goal, in one line",
453
+ "",
454
+ ayuda="What has to be achieved. Concrete enough to argue with.",
455
+ )
456
+ if titulo:
457
+ d["objetivo"] = {
458
+ "user": quien,
459
+ "title": titulo,
460
+ "signal": ui.pide(
461
+ "How is it measured",
462
+ "",
463
+ ayuda="The signal that says whether it is going well.",
464
+ ),
465
+ "command": ui.pide(
466
+ "The command that returns it",
467
+ "",
468
+ ayuda="Runnable as written. Leave empty for "
469
+ '"manual — who looks at what, and how often".',
470
+ ),
471
+ "baseline": ui.pide(
472
+ "What it returns today",
473
+ "",
474
+ ayuda="Run it. This is the starting point nobody "
475
+ "can argue with in three months.",
476
+ ),
477
+ "target": ui.pide("Where it has to get to", ""),
478
+ "by": ui.pide("By when", "this quarter"),
479
+ }
480
+ return True
481
+
482
+
483
+ PASOS_DEL_WIZARD = (paso_tipo, paso_fuente, paso_unidades, paso_roles, paso_gente, paso_objetivo)
484
+
485
+
486
+ def recoge(destino):
487
+ """The wizard: six screens, each its own function, quit anywhere.
488
+
489
+ This was one 200-line function with complexity 38 — the god of the repo. Now
490
+ the sequence is the only thing this function says, which is all it ever knew.
491
+ """
492
+ d = {
493
+ "destino": destino,
494
+ "unidades": [],
495
+ "roles": [],
496
+ "repos": [],
497
+ "gente": [],
498
+ "org": "",
499
+ "rutas": {},
500
+ "kind": "product",
501
+ "grow_cmd": "",
502
+ }
503
+
504
+ if not ui.pantalla(
505
+ "Your city, in six steps",
506
+ "Agents City connects the coding agents of people who do not share an "
507
+ "account, so a change that touches somebody else's work reaches them "
508
+ "instead of shipping unnoticed.\n\n"
509
+ "This wizard writes the data it runs on: your business units, your roles, "
510
+ "which repos belong to which unit, and one card per person. All of it is "
511
+ "plain files you can edit afterwards — nothing here is baked into the "
512
+ "code.\n\n"
513
+ "You can stop at any step. What is written stays written, and running this "
514
+ "again picks up where you left off.",
515
+ None,
516
+ None,
517
+ "enter to start · q to quit",
518
+ ):
519
+ return None
520
+
521
+ for paso in PASOS_DEL_WIZARD:
522
+ if not paso(d):
523
+ return None
524
+ d.pop("tpl", None)
525
+ return d
526
+
527
+
528
+ # ── writing ────────────────────────────────────────────────────────────────
529
+ oficio = roles.oficio
530
+ AGENTE = roles.SUFIJOS
531
+
532
+
533
+ def escribe(d):
534
+ destino = d["destino"]
535
+ os.makedirs(f"{destino}/roles", exist_ok=True)
536
+ hechos = []
537
+
538
+ unidades = list(d["unidades"])
539
+ # Written by units.py — the same writer the seat and the hall use. This was a
540
+ # third inline copy of the format, and three writers of one file is how the
541
+ # parcels reader ended up hunting a key no writer wrote.
542
+ units.escribe(f"{destino}/units.yml", unidades)
543
+ hechos.append(f"units.yml — {len(unidades)} of your own, plus lab and none")
544
+
545
+ ids = [u["id"] for u in unidades] + ["lab", "none"]
546
+ p = [
547
+ "# Parcels — which slice of which repo serves which unit.",
548
+ "#",
549
+ "# A house is not a repo: it is a **parcel**, a slice of a repo serving one",
550
+ "# unit. One repo can hold several, and that is the central fact of most",
551
+ "# codebases — a monorepo holds products from every unit, the main site holds",
552
+ "# three at once.",
553
+ "#",
554
+ "# Until you split them, every repo below is one house in whatever unit you",
555
+ "# wrote next to it. That works; it just cannot tell you which unit a change",
556
+ "# touches. When you are ready, split the interesting ones like this:",
557
+ "#",
558
+ "# repos:",
559
+ "# main-site:",
560
+ '# - ruta: "content/**/deposits/**" ; unidad: banking ; nombre: "main · deposits"',
561
+ '# - ruta: "" ; unidad: none ; nombre: "main · platform"',
562
+ "#",
563
+ f"# units available: {', '.join(ids)}",
564
+ "",
565
+ "repos:",
566
+ ]
567
+ for r in d["repos"]:
568
+ p.append(f" {r}:")
569
+ p.append(f' - ruta: "" ; unidad: none ; nombre: "{r}"')
570
+ p += [
571
+ "",
572
+ "# Research that does not ship yet. Add repos here and they get their own",
573
+ "# district instead of landing among the shared ones.",
574
+ "lab: []",
575
+ ]
576
+ open(f"{destino}/parcels.yml", "w").write("\n".join(p) + "\n")
577
+ hechos.append(f"parcels.yml — {len(d['repos'])} repos, one house each, ready to split")
578
+
579
+ ejemplos = os.path.join(AQUI, "..", "plugin", "roles", "examples")
580
+ copiados = 0
581
+ for r in d["roles"]:
582
+ src = os.path.join(ejemplos, f"{r}.md")
583
+ if os.path.exists(src):
584
+ open(f"{destino}/roles/{r}.md", "w").write(open(src).read())
585
+ copiados += 1
586
+ hechos.append(f"roles/ — {copiados} role files, yours to edit")
587
+
588
+ for g in d["gente"]:
589
+ u, rol = g["user"], g["role"]
590
+ mios = [r for r in d["repos"]] if rol == "dev" and len(d["gente"]) == 1 else []
591
+ fm = [
592
+ f"user: {u}",
593
+ f"name: {u}",
594
+ f"role: {rol}",
595
+ f"agent: {u}/{roles.sufijo(rol)}",
596
+ "repos: [" + ", ".join(mios) + "]",
597
+ *(f"role.{card.ventana(repo)}: blank" for repo in sorted(mios)),
598
+ "goals_defined: false",
599
+ ]
600
+ cuerpo = [
601
+ f"# {u}",
602
+ "",
603
+ card.linea_rol(rol, oficio(rol)),
604
+ "",
605
+ "## Repos",
606
+ "",
607
+ (
608
+ "Not filled in yet. Run `/city:join` and pick them from a list — the repos "
609
+ "you answer for is the one thing worth confirming yourself."
610
+ if not mios
611
+ else "Answers for: " + ", ".join(f"`{r}`" for r in mios) + "."
612
+ ),
613
+ "",
614
+ "## Current goals",
615
+ "",
616
+ "> **Pending.** A round works without them, but it cannot contrast the work "
617
+ "against anything.",
618
+ "",
619
+ "> To fill them in: `/city:goals`",
620
+ "",
621
+ "## Round history",
622
+ "",
623
+ "Rounds leave their summary here, most recent first.",
624
+ "",
625
+ ]
626
+ obj = d.get("objetivo")
627
+ if obj and obj["user"] == u:
628
+ # The goal block comes from card.py, which is also where ./bin/seat gets
629
+ # it. A round reads whichever door wrote the card and must not be able to
630
+ # tell — the two used to emit it separately, so keeping them identical
631
+ # was somebody remembering to.
632
+ fm[-1] = "goals_defined: true"
633
+ cuerpo = (
634
+ cuerpo[: cuerpo.index("## Current goals") + 2]
635
+ + card.bloque_objetivo(obj)
636
+ + ["_Agreed with the architect during setup._", ""]
637
+ + cuerpo[cuerpo.index("## Round history") :]
638
+ )
639
+ open(f"{destino}/{u}.md", "w").write(
640
+ "---\n" + "\n".join(fm) + "\n---\n\n" + "\n".join(cuerpo)
641
+ )
642
+ hechos.append(f"{len(d['gente'])} cards — one per person, goals empty on purpose")
643
+
644
+ # The work domain, so the seeder and every runtime share one context.
645
+ dominio = domains.canonico(d.get("domain") or d.get("kind") or "software")
646
+ open(f"{destino}/city.yml", "w").write(
647
+ "# The work domain of this city. Written by the wizard; edit freely.\n"
648
+ "#\n"
649
+ "# `grows_with` is prose, for humans. `grow_command` is the one that matters:\n"
650
+ "# a command returning a single number, run inside each parcel. The map does\n"
651
+ "# not care whether that number counts merged pull requests, published pieces,\n"
652
+ "# filed documents or closed periods — it cares that it is real and that\n"
653
+ "# nobody has to be asked for it.\n"
654
+ f"domain: {dominio}\n"
655
+ f"grow_command: {d.get('grow_cmd', '')}\n"
656
+ )
657
+ hechos.append("city.yml — the work domain and how growth is counted")
658
+
659
+ # The selected packs are visible, editable city knowledge. This does not
660
+ # install or alter any skill in a referenced repo.
661
+ for g in d["gente"]:
662
+ domains.materializa(destino, dominio, g["role"])
663
+
664
+ # And one descriptor per folder: the info the agent working there reads.
665
+ escritos = 0
666
+ for nombre in d["repos"]:
667
+ ruta = d["rutas"].get(nombre)
668
+ if not ruta or not os.path.isdir(ruta):
669
+ continue
670
+ dueno = next((g["user"] for g in d["gente"] if g["role"] == "dev"), "")
671
+ open(os.path.join(ruta, ".city.yml"), "w").write(
672
+ f"# This folder in the city. Read by the agent that works here.\n"
673
+ f"parcel: {nombre}\n"
674
+ f"unit: none # which business unit this serves\n"
675
+ f"owner: {dueno}\n"
676
+ f"grow_command: {d.get('grow_cmd', '')}\n"
677
+ )
678
+ agentes = os.path.join(ruta, "AGENTS.md")
679
+ if not os.path.exists(agentes):
680
+ open(agentes, "w").write(
681
+ f"# {nombre}\n\n"
682
+ f"This folder is a parcel in our Agents City. What that means in practice:\n\n"
683
+ f"- It serves one business unit. Which one is in `.city.yml`, next to this file.\n"
684
+ f"- Somebody answers for it. Also in `.city.yml`.\n"
685
+ f"- When a change here touches a property somebody else owns — security, "
686
+ f"measurement, URLs, cost, the look of a shared thing — **tell them**. "
687
+ f"Do not decide on their behalf and do not stay quiet: run `/city:notice`.\n\n"
688
+ f"What belongs in this file, and what nobody else can write for you: why "
689
+ f"things here are the way they are. What is deliberate, what is debt taken "
690
+ f"on knowingly, what looks wrong and is not. That is the context an agent "
691
+ f"cannot deduce from the files, and it is the whole reason the city works.\n"
692
+ )
693
+ escritos += 1
694
+ if escritos:
695
+ hechos.append(f"{escritos} folders got a .city.yml and an AGENTS.md")
696
+ return hechos
697
+
698
+
699
+ def main():
700
+ ap = argparse.ArgumentParser(description="Create or open one personal Agents City")
701
+ ap.add_argument("--city", default="", help="local city name (creates it if needed)")
702
+ ap.add_argument(
703
+ "--out", default="", help="explicit city folder (advanced/import compatibility)"
704
+ )
705
+ ap.add_argument("--demo", action="store_true", help="load the example studio instead")
706
+ ap.add_argument("--tui", action="store_true", help="ask in the terminal instead of the browser")
707
+ ap.add_argument(
708
+ "--no-browser", action="store_true", help="print the Hall address instead of opening it"
709
+ )
710
+ a = ap.parse_args()
711
+
712
+ if a.demo:
713
+ guion = os.path.join(AQUI, "demo")
714
+ os.execv(guion, [guion])
715
+
716
+ usuario = cities.usuario_actual()
717
+ if a.out:
718
+ datos = os.path.realpath(os.path.expanduser(a.out))
719
+ owner_previo = cities.lee_clave(datos, "owner")
720
+ if owner_previo and owner_previo != usuario:
721
+ print(f" This city belongs to {owner_previo!r}, not {usuario!r}.", file=sys.stderr)
722
+ return 1
723
+ cities.asegura_metadata(datos, usuario, a.city or os.path.basename(datos) or "home")
724
+ cities.registra(datos, usuario)
725
+ cities.selecciona(usuario, datos)
726
+ elif a.city:
727
+ datos = cities.resuelve(a.city, usuario) or cities.crea(usuario, a.city)
728
+ cities.selecciona(usuario, datos)
729
+ else:
730
+ datos = cities.actual(usuario)
731
+
732
+ owner = cities.lee_clave(datos, "owner")
733
+ if owner and owner != usuario:
734
+ print(f" This city belongs to {owner!r}, not {usuario!r}.", file=sys.stderr)
735
+ return 1
736
+
737
+ env = dict(
738
+ os.environ,
739
+ AGENTS_CITY_DATA=datos,
740
+ AGENTS_CITY_HOME=cities.raiz(),
741
+ AGENTS_CITY_USER=usuario,
742
+ CITY_ADDRESS=cities.direccion(usuario, datos),
743
+ )
744
+ if a.tui:
745
+ guion = os.path.join(GUIONES, "seat.py")
746
+ os.execve(guion, [guion, "--city", datos], env)
747
+ guion = os.path.join(AQUI, "hall")
748
+ argumentos = [guion] + (["--no-browser"] if a.no_browser else [])
749
+ os.execve(guion, argumentos, env)
750
+
751
+
752
+ if __name__ == "__main__":
753
+ sys.exit(main())