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,140 @@
1
+ #!/usr/bin/env python3
2
+ """What makes a house grow depends on who lives in it.
3
+
4
+ The map grew every building from merged pull requests. That is right for a code
5
+ agent and meaningless for everyone else — a knowledge agent whose work is a
6
+ folder of documents, or a coordinator whose work is decisions, would forever
7
+ show an empty lot no matter how much they did. So growth is polymorphic: each
8
+ agent kind declares how its floors are counted, over its own workspace, with no
9
+ assumption of git.
10
+
11
+ - **code**: floors = merged PRs (the existing signal), bricks = commits with no
12
+ PR. Counted from the mounted repos, exactly as before.
13
+ - **knowledge**: floors = documents in the workspace, bricks = recent edits.
14
+ A person with no git at all now has a house that grows as their knowledge does.
15
+ - **coordinator**: floors = recorded decisions (deliberations), bricks = notices.
16
+
17
+ Every counter takes the same `(agente, data)` and returns the same
18
+ `{floors, bricks, activity30, signal}` shape, so the map renders one model and
19
+ the reporter pushes one row regardless of kind. Pure counting over the
20
+ filesystem; the git-backed `code` counter is injected so this module needs no
21
+ network and stays trivially testable.
22
+ """
23
+
24
+ import os
25
+ import time
26
+
27
+ import deliberations
28
+ import workspace
29
+
30
+ #: Extensions that count as a knowledge document.
31
+ DOC_EXT = ('.md', '.txt', '.rst', '.org', '.pdf', '.doc', '.docx', '.adoc')
32
+ DIAS_RECIENTE = 30
33
+ _SEG_RECIENTE = DIAS_RECIENTE * 24 * 3600
34
+
35
+
36
+ def _ahora():
37
+ # Injected via env for deterministic tests; falls back to wall clock.
38
+ forzado = os.environ.get('CITY_GROWTH_NOW')
39
+ return float(forzado) if forzado else time.time()
40
+
41
+
42
+ def _cuenta_docs(raiz):
43
+ """(total docs, docs edited within DIAS_RECIENTE) under a folder tree."""
44
+ total = recientes = 0
45
+ ahora = _ahora()
46
+ for base, dirs, files in os.walk(raiz):
47
+ dirs[:] = [d for d in dirs if d not in ('.git', 'node_modules', '.cache')]
48
+ for f in files:
49
+ if f.lower().endswith(DOC_EXT):
50
+ total += 1
51
+ try:
52
+ if ahora - os.path.getmtime(os.path.join(base, f)) < _SEG_RECIENTE:
53
+ recientes += 1
54
+ except OSError:
55
+ pass
56
+ return total, recientes
57
+
58
+
59
+ def _resultado(floors, bricks, actividad, signal):
60
+ return {'floors': floors, 'bricks': bricks, 'activity30': actividad, 'signal': signal}
61
+
62
+
63
+ def _knowledge(agente, data, contador_git=None):
64
+ total = recientes = 0
65
+ vistos = set()
66
+ # The workspace always counts and mounts add to it — `or`-falling-back made
67
+ # the agent's own documents vanish the moment it mounted anything.
68
+ for destino in [agente.workspace] + list(workspace.mount_targets(agente, data) or []):
69
+ real = os.path.realpath(destino) if destino else ''
70
+ if not real or real in vistos or not os.path.isdir(real):
71
+ continue
72
+ vistos.add(real)
73
+ t, r = _cuenta_docs(real)
74
+ total += t
75
+ recientes += r
76
+ return _resultado(total, recientes, recientes, 'documents')
77
+
78
+
79
+ def _coordinator(agente, data, contador_git=None):
80
+ # Count decisions through the canonical deliberation reader, not a re-glob:
81
+ # the store is a tree of `deliberations/<id>/` dirs, so listing `*.json`
82
+ # would always find nothing.
83
+ registros = deliberations.lista(data)
84
+ ahora = _ahora()
85
+ recientes = 0
86
+ for d in registros:
87
+ cuando = d.get('updated_at') or d.get('created_at')
88
+ try:
89
+ if cuando and ahora - _epoch(cuando) < _SEG_RECIENTE:
90
+ recientes += 1
91
+ except (ValueError, TypeError):
92
+ pass
93
+ return _resultado(len(registros), 0, recientes, 'decisions')
94
+
95
+
96
+ def _code(agente, data, contador_git=None):
97
+ """Floors from merged PRs via an injected git counter; 0 without one.
98
+
99
+ Kept injectable so this module needs no git and no network: the map/reporter
100
+ passes the existing PR counter, the tests pass a stub.
101
+ """
102
+ if contador_git is None:
103
+ return _resultado(0, 0, 0, 'pull-requests')
104
+ prs, commits, act = contador_git(agente, data)
105
+ return _resultado(prs, commits, act, 'pull-requests')
106
+
107
+
108
+ #: One counter per kind. Every counter has the same (agente, data, contador_git)
109
+ #: signature — the non-code ones ignore contador_git — so dispatch is uniform,
110
+ #: no special case. Keyed off workspace.CLASES so the kind vocabulary lives once;
111
+ #: `crece` degrades an unknown kind to _knowledge rather than crashing, so a new
112
+ #: kind added to workspace.CLASES stays safe here until a counter is written.
113
+ _CONTADORES = {
114
+ 'code': _code,
115
+ 'knowledge': _knowledge,
116
+ 'coordinator': _coordinator,
117
+ }
118
+
119
+
120
+ def _epoch(valor):
121
+ """An ISO-8601 or epoch-ish timestamp -> epoch seconds. Raises on garbage.
122
+
123
+ A timezone-naive timestamp is read as UTC (not local time), so a coordinator's
124
+ recency window does not drift by the machine's UTC offset.
125
+ """
126
+ import datetime
127
+ texto = str(valor)
128
+ try:
129
+ return float(texto)
130
+ except ValueError:
131
+ dt = datetime.datetime.fromisoformat(texto.replace('Z', '+00:00'))
132
+ if dt.tzinfo is None:
133
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
134
+ return dt.timestamp()
135
+
136
+
137
+ def crece(agente, data, contador_git=None):
138
+ """The growth of one agent's house, dispatched uniformly on its kind."""
139
+ contador = _CONTADORES.get(agente.clase, _knowledge)
140
+ return contador(agente, data, contador_git)
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only summaries of durable committee acts for the local Hall."""
3
+ import json
4
+ import os
5
+ import re
6
+
7
+ ID_RE = re.compile(r'^delib_[a-z0-9][a-z0-9_-]{5,79}$')
8
+
9
+
10
+ def lista(datos):
11
+ root = os.path.join(os.path.realpath(datos), 'deliberations')
12
+ try:
13
+ nombres = sorted(os.listdir(root), reverse=True)
14
+ except OSError:
15
+ return []
16
+ fuera = []
17
+ for ident in nombres:
18
+ if not ID_RE.match(ident):
19
+ continue
20
+ try:
21
+ state = json.load(open(os.path.join(root, ident, 'state.json'),
22
+ encoding='utf-8'))
23
+ if state.get('schema') != 'agents-city/deliberation@1':
24
+ continue
25
+ brief = state['brief']
26
+ decisions = state.get('decisions') or []
27
+ decision = decisions[-1] if decisions else {}
28
+ verification = decision.get('verification') or {}
29
+ fuera.append({
30
+ 'id': ident,
31
+ 'status': state.get('status', 'unknown'),
32
+ 'question': brief.get('question', ''),
33
+ 'desired_outcome': brief.get('desiredOutcome', ''),
34
+ 'participants': brief.get('participants', []),
35
+ 'participant_roles': state.get('participantRoles', {}),
36
+ 'received': len(state.get('positions') or {}),
37
+ 'total': len(brief.get('participants') or []),
38
+ 'revision': (state.get('progress') or {}).get('revision', 1),
39
+ 'decision': decision.get('outcome', ''),
40
+ 'contributors': decision.get('decisiveContributors', []),
41
+ 'verifier': decision.get('verifier', ''),
42
+ 'verification': verification.get('result', ''),
43
+ 'updated_at': state.get('updatedAt', ''),
44
+ 'act': os.path.join(root, ident, 'ACT.md'),
45
+ })
46
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
47
+ continue
48
+ return fuera
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env python3
2
+ """doctor: detect an old config shape, explain it, back it up, rewrite it.
3
+
4
+ The runtime reads one config schema — the current one. It keeps no long-lived
5
+ aliases that silently accept renamed or malformed keys, because a silent alias
6
+ is how two subtly different shapes drift apart. Instead, every change that can
7
+ invalidate an existing config ships a migration here: `doctor` finds the old
8
+ shape, says in one line what it is doing, writes a timestamped backup, and
9
+ rewrites the file to canonical form.
10
+
11
+ Each migration is a small object with three parts — does it apply, what does it
12
+ change, and the rewrite — so adding one is additive and never touches the
13
+ others (open for extension). `--dry-run` reports without writing; the default
14
+ writes a `.bak-<stamp>` next to the file first. Pure enough to test against a
15
+ temp file with no real config in sight.
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+
23
+ GUIONES = os.path.dirname(os.path.abspath(__file__))
24
+ sys.path.insert(0, GUIONES)
25
+ from dataclasses import dataclass
26
+ from typing import Callable
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Migracion:
31
+ id: str
32
+ explica: str
33
+ aplica: Callable[[dict], bool]
34
+ reescribe: Callable[[dict], dict]
35
+
36
+
37
+ def _renombra_bus_url(cfg):
38
+ cfg = dict(cfg)
39
+ cfg['roadsUrl'] = cfg.pop('busUrl')
40
+ return cfg
41
+
42
+
43
+ def _sella_bind(cfg):
44
+ cfg = dict(cfg)
45
+ gw = dict(cfg.get('gateway', {}))
46
+ gw.setdefault('bind', 'loopback')
47
+ cfg['gateway'] = gw
48
+ return cfg
49
+
50
+
51
+ def _cage_por_defecto(cfg):
52
+ cfg = dict(cfg)
53
+ seg = dict(cfg.get('security', {}))
54
+ if 'cage' not in seg:
55
+ seg['cage'] = True
56
+ cfg['security'] = seg
57
+ return cfg
58
+
59
+
60
+ #: Ordered so a file needing several migrations converges in one pass. Each is
61
+ #: idempotent: re-running doctor on a canonical file changes nothing.
62
+ MIGRACIONES = [
63
+ Migracion(
64
+ 'bus-url-renamed-to-roads-url',
65
+ 'the transport key `busUrl` is now `roadsUrl`',
66
+ lambda c: 'busUrl' in c,
67
+ _renombra_bus_url,
68
+ ),
69
+ Migracion(
70
+ 'gateway-bind-defaults-to-loopback',
71
+ 'a gateway block without an explicit `bind` now pins `loopback`',
72
+ lambda c: isinstance(c.get('gateway'), dict) and 'bind' not in c['gateway'],
73
+ _sella_bind,
74
+ ),
75
+ Migracion(
76
+ 'cage-on-by-default',
77
+ 'the per-window cage is recorded as on unless the owner set it',
78
+ # Guard the type like migration 2: a `security` that is present but not
79
+ # a dict (null/str/list) must not crash the tool that exists to fix it.
80
+ # Absent security counts as an empty dict, so the default is still added.
81
+ lambda c: isinstance(c.get('security', {}), dict) and 'cage' not in c.get('security', {}),
82
+ _cage_por_defecto,
83
+ ),
84
+ ]
85
+
86
+
87
+ def _aplica(cfg):
88
+ """The single migration walk: return (nuevo, [Migracion aplicadas]).
89
+
90
+ Everything else is a view of this — `diagnostica` wants the objects,
91
+ `cura` wants their ids, `cura_fichero` wants both — so the apply/rewrite
92
+ logic lives here once and never runs twice per file.
93
+ """
94
+ aplicadas = []
95
+ trabajo = dict(cfg)
96
+ for m in MIGRACIONES:
97
+ if m.aplica(trabajo):
98
+ trabajo = m.reescribe(trabajo)
99
+ aplicadas.append(m)
100
+ return trabajo, aplicadas
101
+
102
+
103
+ def diagnostica(cfg):
104
+ """Return the ordered migrations that still apply to `cfg`."""
105
+ return _aplica(cfg)[1]
106
+
107
+
108
+ def cura(cfg):
109
+ """Apply every applicable migration in order; return (nuevo, ids_aplicadas)."""
110
+ nuevo, aplicadas = _aplica(cfg)
111
+ return nuevo, [m.id for m in aplicadas]
112
+
113
+
114
+ def _stamp_backup(ruta, marca):
115
+ """Copy the file to a timestamped sibling before rewriting. Marca is passed
116
+ in (never read from the clock here) so a caller controls naming and tests
117
+ stay deterministic."""
118
+ destino = f'{ruta}.bak-{marca}'
119
+ with open(ruta, encoding='utf-8') as f:
120
+ contenido = f.read()
121
+ with open(destino, 'w', encoding='utf-8') as f:
122
+ f.write(contenido)
123
+ os.chmod(destino, 0o600)
124
+ return destino
125
+
126
+
127
+ def cura_fichero(ruta, marca, dry_run=False):
128
+ """Diagnose and (unless dry-run) rewrite a config file. Returns a report."""
129
+ with open(ruta, encoding='utf-8') as f:
130
+ cfg = json.load(f)
131
+ nuevo, pendientes = _aplica(cfg) # one walk: report and rewrite come from it
132
+ reporte = {'file': ruta, 'migrations': [m.id for m in pendientes],
133
+ 'explains': [m.explica for m in pendientes], 'wrote': False, 'backup': None}
134
+ if not pendientes or dry_run:
135
+ return reporte
136
+ reporte['backup'] = _stamp_backup(ruta, marca)
137
+ tmp = f'{ruta}.tmp'
138
+ with open(tmp, 'w', encoding='utf-8') as f:
139
+ json.dump(nuevo, f, indent=2, sort_keys=True)
140
+ # Preserve the original file's mode rather than inheriting the umask default
141
+ # (0644): a config that was 0600 must not be silently widened by a migration.
142
+ try:
143
+ os.chmod(tmp, os.stat(ruta).st_mode & 0o777)
144
+ except OSError:
145
+ os.chmod(tmp, 0o600)
146
+ os.replace(tmp, ruta)
147
+ reporte['wrote'] = True
148
+ return reporte
149
+
150
+
151
+ # ── the machine, not just the config ─────────────────────────────────────────
152
+ #
153
+ # `doctor` used to answer one narrow question — is this config file an old
154
+ # shape? — behind no command at all. What a person means by "doctor" is: tell
155
+ # me whether this thing can work here, and if not, which part. So it answers
156
+ # that too, and every line is derived: what is installed, what the kernel will
157
+ # grant, which city is selected. Nothing here writes.
158
+
159
+
160
+ def _version_de(programa, bandera='--version'):
161
+ import shutil as _sh
162
+ import subprocess as _sp
163
+
164
+ ruta = _sh.which(programa)
165
+ if not ruta:
166
+ return ''
167
+ try:
168
+ r = _sp.run([ruta, bandera], capture_output=True, text=True, timeout=10)
169
+ except (OSError, _sp.SubprocessError):
170
+ return 'installed'
171
+ primera = (r.stdout or r.stderr).strip().splitlines()
172
+ return primera[0].strip() if primera else 'installed'
173
+
174
+
175
+ def revisa_entorno():
176
+ """Every check as `(area, ok, detail)`. `ok` is None for "not applicable".
177
+
178
+ A list rather than printed lines, so the Hall could show the same answers
179
+ the terminal does without either of them re-deciding what healthy means.
180
+ """
181
+ import shutil as _sh
182
+ import sys as _sys
183
+
184
+ import cage as _cage
185
+ import cities as _cities
186
+
187
+ fuera = []
188
+ fuera.append(('python', True, _sys.version.split()[0]))
189
+ # Every one of these spells its version flag differently, and tmux answers
190
+ # `--version` with an error that reads like a working version string.
191
+ for programa, bandera, obligatorio in (('tmux', '-V', True), ('bash', '--version', True),
192
+ ('git', '--version', True),
193
+ ('node', '--version', True),
194
+ ('gh', '--version', False)):
195
+ v = _version_de(programa, bandera)
196
+ fuera.append((programa, bool(v) if obligatorio else (True if v else None),
197
+ v or ('missing' if obligatorio else 'not installed (optional)')))
198
+
199
+ motores = [m for m in ('claude', 'codex', 'opencode', 'kimi') if _sh.which(m)]
200
+ fuera.append(('runtimes', bool(motores),
201
+ ', '.join(motores) if motores else 'none — a city needs at least one CLI'))
202
+
203
+ if _sys.platform == 'darwin':
204
+ detalle = ('seatbelt' if _sh.which('sandbox-exec') else 'sandbox-exec missing')
205
+ elif _sys.platform.startswith('linux'):
206
+ if not _sh.which('bwrap'):
207
+ detalle = 'bubblewrap not installed — agents run uncaged (apt install bubblewrap)'
208
+ elif not _cage.bwrap_sirve():
209
+ detalle = ('bubblewrap present but the kernel refuses unprivileged '
210
+ 'namespaces — agents run uncaged')
211
+ else:
212
+ detalle = 'bubblewrap'
213
+ else:
214
+ detalle = f'no cage on {_sys.platform}'
215
+ fuera.append(('cage', _cage.disponible(), detalle))
216
+
217
+ try:
218
+ usuario = _cities.usuario_actual()
219
+ datos = _cities.actual(usuario, crear=False)
220
+ if datos:
221
+ fuera.append(('city', True, _cities.direccion(usuario, datos)))
222
+ ficha = os.path.join(datos, f'{usuario}.md')
223
+ fuera.append(('card', os.path.isfile(ficha),
224
+ ficha if os.path.isfile(ficha) else f'no card at {ficha}'))
225
+ else:
226
+ fuera.append(('city', None, 'none selected yet — agents-city seat creates one'))
227
+ except (OSError, ValueError) as e:
228
+ fuera.append(('city', False, str(e)))
229
+
230
+ hall = os.path.join(os.path.dirname(os.path.dirname(GUIONES)),
231
+ 'city', 'web', 'dist-hall', 'hall.js')
232
+ fuera.append(('hall bundle', os.path.isfile(hall),
233
+ 'built' if os.path.isfile(hall) else 'not built — ./bin/hall builds it'))
234
+ return fuera
235
+
236
+
237
+ def informe_entorno():
238
+ """Print the environment report. Returns 0 when nothing is broken."""
239
+ import actualiza
240
+
241
+ print('\n Agents City doctor\n')
242
+ roto = 0
243
+ for area, bien, detalle in revisa_entorno():
244
+ marca = '·' if bien is None else ('ok' if bien else 'XX')
245
+ if bien is False:
246
+ roto += 1
247
+ print(f' {marca:>2} {area:<12} {detalle}')
248
+ instalada, ultima, hay = actualiza.comprueba()
249
+ if ultima:
250
+ print(f' {"!!" if hay else "ok":>2} {"version":<12} {instalada}'
251
+ + (f' — {ultima} is out: agents-city update' if hay else ' (current)'))
252
+ else:
253
+ print(f' {"·":>2} {"version":<12} {instalada} (registry not checked)')
254
+ print()
255
+ if roto:
256
+ print(f' {roto} thing(s) need attention.\n')
257
+ return 1 if roto else 0
258
+
259
+
260
+ def main():
261
+ # `doctor` with no file is the environment report — what a person means by
262
+ # the word. The config migration keeps its own path, unchanged.
263
+ if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help', 'help'):
264
+ if len(sys.argv) > 1:
265
+ print(' usage: agents-city doctor [config.json [--fix]]\n\n'
266
+ ' With no arguments: check this machine — tools, runtimes, the\n'
267
+ ' cage, the selected city, and whether a newer version is out.\n'
268
+ ' With a config file: detect, explain and migrate an old shape.')
269
+ return 0
270
+ return informe_entorno()
271
+ p = argparse.ArgumentParser(description='Detect, explain and migrate an old config shape.')
272
+ p.add_argument('fichero', help='the config JSON file to check')
273
+ p.add_argument('--fix', action='store_true', help='rewrite (default is dry-run report)')
274
+ p.add_argument('--stamp', default='manual', help='backup suffix (tests pass a fixed value)')
275
+ args = p.parse_args()
276
+ try:
277
+ reporte = cura_fichero(args.fichero, args.stamp, dry_run=not args.fix)
278
+ except (OSError, json.JSONDecodeError) as e:
279
+ print(f'{e}', file=sys.stderr)
280
+ return 1
281
+ if not reporte['migrations']:
282
+ print('config is already canonical')
283
+ return 0
284
+ for ident, porque in zip(reporte['migrations'], reporte['explains'], strict=True):
285
+ print(f' {ident}: {porque}')
286
+ if reporte['wrote']:
287
+ print(f'rewritten; backup at {reporte["backup"]}')
288
+ else:
289
+ print('dry-run: pass --fix to apply')
290
+ return 0
291
+
292
+
293
+ if __name__ == '__main__':
294
+ sys.exit(main())
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ """Built-in work domains and the knowledge packs installed into one city.
3
+
4
+ A domain is the context of the city (software, healthcare, legal, finance...).
5
+ A role is the responsibility of its one owner seat inside that domain. Keeping
6
+ those two choices separate is important: ``data`` means something different in a
7
+ software product from a clinical-data responsibility, while neither is the city
8
+ itself.
9
+
10
+ The catalogue is plain Markdown under ``plugin/domains``. On selection, the
11
+ chosen domain pack and every non-blank seat/repo role pack are copied into the
12
+ city's own ``domains/`` and ``roles/`` folders. They are ordinary, editable files
13
+ after that. This is city knowledge, not a repo skill: Agents City never installs
14
+ or changes skills in a referenced repo.
15
+ """
16
+
17
+ import glob
18
+ import os
19
+ import re
20
+ import shutil
21
+
22
+ import cities
23
+
24
+
25
+ AQUI = os.path.dirname(os.path.abspath(__file__))
26
+ PACKS = os.path.realpath(os.path.join(AQUI, "..", "domains"))
27
+ ROLE_PACKS = os.path.realpath(os.path.join(AQUI, "..", "roles", "examples"))
28
+
29
+ # Old cities called the domain ``kind`` and used these ids. Read them forever,
30
+ # but every new write uses the explicit ``domain`` field and canonical id.
31
+ ALIASES = {"product": "software", "blank": "custom"}
32
+
33
+
34
+ def canonico(valor):
35
+ return ALIASES.get(str(valor or "").strip().lower(), str(valor or "").strip().lower())
36
+
37
+
38
+ def _frontmatter(texto):
39
+ if not texto.startswith("---\n"):
40
+ return ""
41
+ partes = texto.split("---", 2)
42
+ return partes[1] if len(partes) == 3 else ""
43
+
44
+
45
+ def _escalar(fm, clave, defecto=""):
46
+ m = re.search(rf"^{re.escape(clave)}:[ \t]*(.*)$", fm, re.M)
47
+ return (m.group(1).strip() if m else "") or defecto
48
+
49
+
50
+ def _lista(fm, clave):
51
+ """Indented ``- value`` rows below one frontmatter key."""
52
+ m = re.search(rf"^{re.escape(clave)}:[ \t]*\n((?:[ \t]+-[^\n]*\n?)*)", fm, re.M)
53
+ if not m:
54
+ return []
55
+ fuera = []
56
+ for linea in m.group(1).splitlines():
57
+ valor = re.sub(r"^[ \t]+-[ \t]*", "", linea).strip()
58
+ if not valor:
59
+ continue
60
+ base, _, nota = valor.partition("#")
61
+ fuera.append((base.strip().rstrip(), "on by default" in nota.lower()))
62
+ return fuera
63
+
64
+
65
+ def _unidades(fm):
66
+ fuera = []
67
+ for valor, _ in _lista(fm, "units"):
68
+ nombre, sep, color = valor.rpartition(";")
69
+ if sep and re.fullmatch(r"[0-9a-fA-F]{6}", color.strip()):
70
+ fuera.append((nombre.strip(), color.strip().lower()))
71
+ return fuera
72
+
73
+
74
+ def lee(ruta):
75
+ try:
76
+ texto = open(ruta, encoding="utf-8").read()
77
+ except OSError:
78
+ return {}
79
+ fm = _frontmatter(texto)
80
+ ident = canonico(_escalar(fm, "id", os.path.basename(ruta)[:-3]))
81
+ roles = _lista(fm, "roles")
82
+ return {
83
+ "id": ident,
84
+ "order": int(_escalar(fm, "order", "999")),
85
+ "name": _escalar(fm, "name", ident.replace("-", " ").title()),
86
+ "summary": _escalar(fm, "summary"),
87
+ "parcel": _escalar(fm, "parcel", "a working repository"),
88
+ "source": _escalar(fm, "source", "github|disk"),
89
+ "grows": _escalar(fm, "grows_with"),
90
+ "grow_cmd": _escalar(fm, "grow_command"),
91
+ "roles": roles,
92
+ "units": _unidades(fm),
93
+ "path": ruta,
94
+ "text": texto,
95
+ }
96
+
97
+
98
+ def catalogo():
99
+ fuera = [lee(ruta) for ruta in glob.glob(os.path.join(PACKS, "*.md"))]
100
+ return sorted((p for p in fuera if p.get("id")), key=lambda p: (p["order"], p["name"].lower()))
101
+
102
+
103
+ def obtiene(ident):
104
+ buscado = canonico(ident)
105
+ return next((p for p in catalogo() if p["id"] == buscado), None)
106
+
107
+
108
+ def roles_de(ident):
109
+ pack = obtiene(ident)
110
+ return list(pack["roles"]) if pack else []
111
+
112
+
113
+ def dominio_de_rol(rol, defecto="software"):
114
+ for pack in catalogo():
115
+ if any(r == rol for r, _ in pack["roles"]):
116
+ return pack["id"]
117
+ return defecto
118
+
119
+
120
+ def de_ciudad(datos, defecto="software"):
121
+ explicito = cities.lee_clave(datos, "domain")
122
+ legado = cities.lee_clave(datos, "kind")
123
+ elegido = canonico(explicito or legado or defecto)
124
+ return elegido if obtiene(elegido) else canonico(defecto)
125
+
126
+
127
+ def selecciona(datos, ident):
128
+ """Persist the canonical domain without rewriting city identity metadata."""
129
+ ident = canonico(ident)
130
+ if not obtiene(ident):
131
+ raise ValueError(f"unknown city domain: {ident}")
132
+ ruta = os.path.join(datos, "city.yml")
133
+ try:
134
+ texto = open(ruta, encoding="utf-8").read()
135
+ except OSError:
136
+ texto = ""
137
+ if re.search(r"^domain:", texto, re.M):
138
+ texto = re.sub(r"^domain:.*$", f"domain: {ident}", texto, count=1, flags=re.M)
139
+ else:
140
+ if texto and not texto.endswith("\n"):
141
+ texto += "\n"
142
+ texto += f"domain: {ident}\n"
143
+ cities.escribe_atomico(ruta, texto)
144
+ return ident
145
+
146
+
147
+ INSTRUCCIONES = """# Agents City seat
148
+
149
+ This folder is one autonomous city, not a source-code repository and not a team
150
+ of people. Its only public actor is the owner seat; referenced repos are private
151
+ support agents behind that chair.
152
+
153
+ Before acting for this city:
154
+
155
+ 1. Read `city.yml` for the selected `domain` and owner.
156
+ 2. Read the owner card `<owner>.md` for the role, goal and support repos.
157
+ 3. Read `domains/<domain>.md` and `roles/<role>.md` when that role file exists.
158
+ `blank` deliberately has no role file. These are transparent, editable
159
+ operating knowledge for this city, not hidden prompts.
160
+ 4. Read each support agent's `role.<repo>` assignment from the owner card before
161
+ asking it for evidence. Use a chaired committee when several matter.
162
+ Repo agents do not talk directly to each other.
163
+
164
+ Skills remain installed in the referenced repos. Agents City only recognises
165
+ them; it never copies, installs or enables a repo skill from this folder.
166
+ """
167
+
168
+
169
+ def materializa(datos, ident, rol):
170
+ """Copy selected built-in knowledge into a city, never over user edits.
171
+
172
+ A later role/domain switch gets its own file, so changing back restores the
173
+ city's edited copy. Existing files are deliberately not refreshed in place.
174
+ """
175
+ pack = obtiene(ident)
176
+ if not pack:
177
+ return []
178
+ hechos = []
179
+ carpeta_dominios = os.path.join(datos, "domains")
180
+ carpeta_roles = os.path.join(datos, "roles")
181
+ os.makedirs(carpeta_dominios, exist_ok=True)
182
+ os.makedirs(carpeta_roles, exist_ok=True)
183
+
184
+ destino_dominio = os.path.join(carpeta_dominios, f"{pack['id']}.md")
185
+ if not os.path.exists(destino_dominio):
186
+ shutil.copy2(pack["path"], destino_dominio)
187
+ hechos.append(f"domains/{pack['id']}.md")
188
+
189
+ rol_seguro = str(rol or "")
190
+ if re.fullmatch(r"[a-z0-9-]+", rol_seguro):
191
+ fuente_rol = os.path.join(ROLE_PACKS, f"{rol_seguro}.md")
192
+ destino_rol = os.path.join(carpeta_roles, f"{rol_seguro}.md")
193
+ if os.path.isfile(fuente_rol) and not os.path.exists(destino_rol):
194
+ shutil.copy2(fuente_rol, destino_rol)
195
+ hechos.append(f"roles/{rol_seguro}.md")
196
+
197
+ instrucciones = os.path.join(datos, "AGENTS.md")
198
+ if not os.path.exists(instrucciones):
199
+ cities.escribe_atomico(instrucciones, INSTRUCCIONES)
200
+ hechos.append("AGENTS.md")
201
+ return hechos