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,1363 @@
1
+ // city/web/src/activity.ts
2
+ var MAP_ACTIVITY_PROTOCOL = "agents-city-map-activity/1";
3
+ function isMapNavMessage(value) {
4
+ if (!value || typeof value !== "object") return false;
5
+ const message = value;
6
+ return message.protocol === MAP_ACTIVITY_PROTOCOL && message.type === "map.nav" && typeof message.view === "string";
7
+ }
8
+ function isActivityEvent(value) {
9
+ if (!value || typeof value !== "object") return false;
10
+ const event = value;
11
+ return event.protocol === "agents-city-activity/1" && typeof event.id === "string" && typeof event.seq === "number" && typeof event.kind === "string" && typeof event.actor === "string" && typeof event.summary === "string" && typeof event.title === "string" && Array.isArray(event.details);
12
+ }
13
+ function isPresenceEvent(event) {
14
+ return event.kind === "conversation.user" || event.kind === "conversation.agent" || event.kind === "runtime.session.started" || event.kind === "runtime.session.ended";
15
+ }
16
+ function isSpeechEvent(event) {
17
+ if (event.kind === "conversation.agent" || event.kind === "conversation.agent.commentary") {
18
+ return Boolean(event.summary.trim());
19
+ }
20
+ return event.kind === "committee.opened" || event.kind === "committee.position.revealed" || event.kind === "committee.synthesis.published" || event.kind.startsWith("committee.floor.") || event.kind === "committee.decision.recorded" || event.kind.startsWith("committee.verification.") || event.kind === "committee.replanned" || event.kind === "committee.closed" || event.kind === "committee.command.rejected" && event.tone === "error";
21
+ }
22
+
23
+ // city/web/src/hall.ts
24
+ var PASE = window.PASE;
25
+ var CIUDAD = new URLSearchParams(location.search).get("city") ?? "";
26
+ async function api(ruta, opts) {
27
+ let u = ruta + (ruta.includes("?") ? "&" : "?") + "PASE=" + encodeURIComponent(PASE);
28
+ if (CIUDAD) u += "&city=" + encodeURIComponent(CIUDAD);
29
+ const r = await fetch(u, {
30
+ headers: { "X-City-Pase": PASE, "Content-Type": "application/json" },
31
+ ...opts
32
+ });
33
+ return r.json();
34
+ }
35
+ function q(sel, raiz = document) {
36
+ const el = raiz.querySelector(sel);
37
+ if (!el) throw new Error(`no element matches ${sel}`);
38
+ return el;
39
+ }
40
+ function todos(sel, raiz = document) {
41
+ return [...raiz.querySelectorAll(sel)];
42
+ }
43
+ function esc(s) {
44
+ return String(s ?? "").replace(
45
+ /[&<>"']/g,
46
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
47
+ );
48
+ }
49
+ var corto = (p) => p.replace(E.casa, "~");
50
+ var toastTimer = 0;
51
+ function toast(msg, mal = false) {
52
+ const t = q("#toast");
53
+ t.textContent = msg;
54
+ t.className = "ver" + (mal ? " mal" : "");
55
+ clearTimeout(toastTimer);
56
+ toastTimer = window.setTimeout(() => {
57
+ t.className = "";
58
+ }, 3200);
59
+ }
60
+ var espera = (ms) => new Promise((r) => setTimeout(r, ms));
61
+ var E;
62
+ var SECCION = "mapa";
63
+ var SECCIONES = [
64
+ ["resumen", "Overview"],
65
+ ["mapa", "The map"],
66
+ ["puesto", "My seat"],
67
+ ["barrios", "Districts & houses"],
68
+ ["red", "Roads"],
69
+ ["committee", "Committee"],
70
+ ["gente", "Agents & skills"]
71
+ ];
72
+ async function refresca() {
73
+ E = await api("/api/estado");
74
+ pinta();
75
+ pintaAvisoDeVersion();
76
+ pintaDemoControles();
77
+ sincronizaActividad(E.live_bus);
78
+ }
79
+ function pintaAvisoDeVersion() {
80
+ const caja = document.getElementById("avisoVersion");
81
+ if (!caja) return;
82
+ const texto = (E.update ?? "").trim();
83
+ caja.textContent = texto;
84
+ caja.hidden = !texto;
85
+ }
86
+ function pintaDemoControles() {
87
+ const header = document.querySelector("#livePanel header");
88
+ const previa = document.getElementById("demoControles");
89
+ if (!header || !E.demo) {
90
+ previa?.remove();
91
+ return;
92
+ }
93
+ if (previa) return;
94
+ const caja = document.createElement("div");
95
+ caja.id = "demoControles";
96
+ caja.innerHTML = `<span>guided committee</span><button id="demoReplay" type="button" title="Play the guided committee from the top">\u27F3 replay</button><button id="demoPausa" type="button" title="Pause or resume mid-scene">\u23F8 pause</button>`;
97
+ header.appendChild(caja);
98
+ const pausa = q("#demoPausa", caja);
99
+ const manda = (action) => api("/api/demo", {
100
+ method: "POST",
101
+ body: JSON.stringify({ action })
102
+ });
103
+ q("#demoReplay", caja).onclick = async () => {
104
+ const r = await manda("restart");
105
+ pausa.textContent = "\u23F8 pause";
106
+ if (!r.ok) toast("Could not restart the demo", true);
107
+ };
108
+ pausa.onclick = async () => {
109
+ const antes = pausa.textContent?.includes("resume") ?? false;
110
+ const r = await manda(antes ? "resume" : "pause");
111
+ if (!r.running) {
112
+ pausa.textContent = "\u23F8 pause";
113
+ toast("Nothing playing \u2014 hit replay", true);
114
+ return;
115
+ }
116
+ pausa.textContent = r.paused ? "\u25B6 resume" : "\u23F8 pause";
117
+ };
118
+ }
119
+ var liveSocket = null;
120
+ var liveUrl = "";
121
+ var liveEvents = [];
122
+ var liveConnected = false;
123
+ var liveRetry = 0;
124
+ var liveSelectedThread = null;
125
+ var liveFilterTouched = false;
126
+ var liveShowWork = false;
127
+ var pendingMapActivity = null;
128
+ function sincronizaActividad(endpoint) {
129
+ if (!endpoint?.online || !endpoint.url) {
130
+ liveConnected = false;
131
+ pintaActividad();
132
+ programaDescubrimiento();
133
+ return;
134
+ }
135
+ if (endpoint.url === liveUrl && liveSocket && (liveSocket.readyState === WebSocket.OPEN || liveSocket.readyState === WebSocket.CONNECTING)) {
136
+ return;
137
+ }
138
+ const anterior = liveSocket;
139
+ if (anterior) {
140
+ anterior.onclose = null;
141
+ anterior.close();
142
+ }
143
+ liveUrl = endpoint.url;
144
+ const socket = new WebSocket(endpoint.url);
145
+ liveSocket = socket;
146
+ liveConnected = false;
147
+ pintaActividad();
148
+ socket.onopen = () => {
149
+ if (liveSocket !== socket) return;
150
+ liveConnected = true;
151
+ liveRetry = 0;
152
+ pintaActividad();
153
+ };
154
+ socket.onmessage = (message) => {
155
+ if (liveSocket !== socket) return;
156
+ let value;
157
+ try {
158
+ value = JSON.parse(String(message.data));
159
+ } catch {
160
+ return;
161
+ }
162
+ if (value.type === "activity.state") {
163
+ liveEvents = Array.isArray(value.events) ? value.events.filter(isActivityEvent) : [];
164
+ liveEvents.sort((a, b) => a.seq - b.seq);
165
+ if (liveSelectedThread === null) liveSelectedThread = newestConversationThread();
166
+ pintaActividad(true);
167
+ } else if (value.type === "activity.event" && isActivityEvent(value.event)) {
168
+ const event = value.event;
169
+ if (!liveEvents.some((item) => item.id === event.id)) liveEvents.push(event);
170
+ liveEvents = liveEvents.sort((a, b) => a.seq - b.seq).slice(-200);
171
+ if (!liveFilterTouched && event.kind === "committee.opened" && event.thread) {
172
+ liveSelectedThread = event.thread;
173
+ }
174
+ pintaActividad(true);
175
+ enviaActividadAlMapa(event);
176
+ }
177
+ };
178
+ socket.onclose = () => {
179
+ if (liveSocket !== socket) return;
180
+ liveSocket = null;
181
+ liveConnected = false;
182
+ pintaActividad();
183
+ programaDescubrimiento();
184
+ };
185
+ socket.onerror = () => {
186
+ };
187
+ }
188
+ function enviaActividadAlMapa(event) {
189
+ const esComite = event.kind.startsWith("committee.");
190
+ if (!isSpeechEvent(event) && !isPresenceEvent(event) && !esComite) return;
191
+ const frame = document.querySelector("#cityMapFrame");
192
+ if (!frame) return;
193
+ if (frame.dataset.ready !== "1") {
194
+ pendingMapActivity = event;
195
+ return;
196
+ }
197
+ const origin = new URL(frame.src, location.href).origin;
198
+ frame.contentWindow?.postMessage(
199
+ { protocol: MAP_ACTIVITY_PROTOCOL, type: "activity.event", event },
200
+ origin
201
+ );
202
+ }
203
+ function programaDescubrimiento() {
204
+ if (liveRetry) return;
205
+ liveRetry = window.setTimeout(async () => {
206
+ liveRetry = 0;
207
+ try {
208
+ sincronizaActividad(await api("/api/live"));
209
+ } catch {
210
+ programaDescubrimiento();
211
+ }
212
+ }, 1500);
213
+ }
214
+ function pintaActividad(seguir = false) {
215
+ const dot = document.querySelector("#liveDot");
216
+ const state = document.querySelector("#liveState");
217
+ const list = document.querySelector("#liveEvents");
218
+ const filter = document.querySelector("#liveFilter");
219
+ const context = document.querySelector("#liveContext");
220
+ const workToggle = document.querySelector("#liveWorkToggle");
221
+ if (!dot || !state || !list || !filter || !context || !workToggle) return;
222
+ dot.classList.toggle("on", liveConnected);
223
+ state.textContent = liveConnected ? "websocket live" : liveUrl ? "reconnecting" : "session offline";
224
+ const opened = /* @__PURE__ */ new Map();
225
+ for (const event of liveEvents) {
226
+ if (event.thread && (event.kind === "committee.opened" || event.kind === "conversation.user") && !esPromptDeTransporte(event) && !opened.has(event.thread)) {
227
+ opened.set(event.thread, event.summary);
228
+ }
229
+ }
230
+ filter.innerHTML = '<option value="">all conversations</option>' + [...opened.entries()].reverse().map(
231
+ ([thread, question]) => `<option value="${esc(thread)}">${esc(question.slice(0, 54) || thread)}</option>`
232
+ ).join("");
233
+ const selected = liveSelectedThread || "";
234
+ filter.value = [...filter.options].some((option) => option.value === selected) ? selected : "";
235
+ filter.onchange = () => {
236
+ liveSelectedThread = filter.value;
237
+ liveFilterTouched = true;
238
+ pintaActividad(true);
239
+ };
240
+ workToggle.classList.toggle("on", liveShowWork);
241
+ workToggle.setAttribute("aria-pressed", String(liveShowWork));
242
+ workToggle.textContent = liveShowWork ? "hide work" : "show work";
243
+ workToggle.onclick = () => {
244
+ liveShowWork = !liveShowWork;
245
+ pintaActividad(true);
246
+ };
247
+ pintaContexto(filter.value, context);
248
+ const scoped = filter.value ? liveEvents.filter((event) => event.thread === filter.value) : liveEvents;
249
+ const visible = scoped.filter(
250
+ (event) => !esPromptDeTransporte(event) && (liveShowWork || !esRuidoDeTrabajo(event) || event.tone === "error")
251
+ );
252
+ if (!visible.length) {
253
+ list.innerHTML = `<li class="liveEmpty">${liveConnected ? "The bus is live. Questions, positions and moderated replies will appear here." : "Start the city session. Its visible conversation will appear here as it happens."}</li>`;
254
+ return;
255
+ }
256
+ const wasNearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 90;
257
+ list.innerHTML = visible.map(renderActividad).join("");
258
+ if (seguir || wasNearBottom) list.scrollTop = list.scrollHeight;
259
+ }
260
+ function renderActividad(event) {
261
+ const when = new Date(event.at);
262
+ const time = Number.isNaN(when.getTime()) ? "" : when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
263
+ const side = event.role === "chair" ? "chair" : event.role === "member" ? "member" : "system";
264
+ const role = rolOperativo(event.actor);
265
+ const route = event.target && !["committee", "seat"].includes(event.target) ? `to ${event.target}` : event.target === "committee" ? "to the committee" : "";
266
+ const summary = event.summary.trim();
267
+ const compact = resumenVisible(summary);
268
+ const long = compact !== summary;
269
+ const details = event.details.length ? `<details class="liveEvidence"><summary>${event.details.length} ${event.details.length === 1 ? "detail" : "details"}</summary><ul>${event.details.map((detail) => `<li>${esc(detail)}</li>`).join("")}</ul></details>` : "";
270
+ const full = long ? `<details class="liveFull"><summary>read full message</summary><p>${esc(summary)}</p></details>` : "";
271
+ return `<li class="liveTurn ${side} ${esc(event.tone)}">
272
+ <div class="liveAvatar${E?.avatars?.[event.actor] ? " conCara" : ""}"
273
+ style="--actor-hue:${tonoActor(event.actor)}" aria-hidden="true">
274
+ ${cara(event.actor, event.role === "chair")}
275
+ </div>
276
+ <article class="liveBubble">
277
+ <div class="liveMeta"><span class="liveActor">${esc(event.actor)}</span>
278
+ ${role ? `<span class="liveRole">${esc(role)}</span>` : ""}
279
+ ${route ? `<span>${esc(route)}</span>` : ""}<span class="liveWhen">${esc(time)}</span></div>
280
+ <div class="liveHeading">${icono(event.kind)}<h3>${esc(tituloVisible(event))}</h3></div>
281
+ <p>${esc(compact)}</p>${full}${details}
282
+ </article>
283
+ </li>`;
284
+ }
285
+ function newestConversationThread() {
286
+ return [...liveEvents].reverse().find(
287
+ (event) => Boolean(event.thread) && (event.kind === "committee.opened" || event.kind === "conversation.user" && !esPromptDeTransporte(event))
288
+ )?.thread || "";
289
+ }
290
+ function pintaContexto(thread, container) {
291
+ const deliberation = thread ? E?.deliberations?.find((item) => item.id === thread) : void 0;
292
+ const conversation = thread ? liveEvents.filter((event) => event.thread === thread) : [];
293
+ const opened = conversation.find((event) => event.kind === "committee.opened");
294
+ if (!deliberation && !opened) {
295
+ container.innerHTML = "";
296
+ container.hidden = true;
297
+ return;
298
+ }
299
+ const invited = opened?.details.find((detail) => detail.startsWith("Invited:"))?.slice("Invited:".length).split(",").map((actor) => actor.trim()).filter(Boolean) || [];
300
+ const participants = deliberation?.participants || invited;
301
+ const received = deliberation?.received ?? new Set(
302
+ conversation.filter((event) => event.kind === "committee.position.submitted").map((event) => event.actor)
303
+ ).size;
304
+ const total = deliberation?.total ?? participants.length;
305
+ const status = conversation.at(-1)?.phase || deliberation?.status || "starting";
306
+ const actors = ["seat", ...participants];
307
+ container.hidden = false;
308
+ container.innerHTML = `<div class="livePeople">${actors.map(
309
+ (actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${esc(
310
+ actor === "seat" ? "seat \xB7 chair" : `${actor} \xB7 ${rolOperativo(actor) || "member"}`
311
+ )}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === "seat")}</span>`
312
+ ).join("")}</div><span class="liveContextText"><b>seat moderates</b> \xB7 ${esc(
313
+ received
314
+ )}/${esc(total)} positions \xB7 ${esc(status)}</span>`;
315
+ }
316
+ function esPromptDeTransporte(event) {
317
+ if (event.kind !== "conversation.user") return false;
318
+ const summary = event.summary.trimStart();
319
+ return summary.startsWith("[Agents City authenticated local bus]") || summary.startsWith("<channel") && summary.slice(0, 500).includes("plugin:city:city-bus");
320
+ }
321
+ function esRuidoDeTrabajo(event) {
322
+ return event.kind === "work.command.started" || event.kind === "work.command.completed" || event.kind === "runtime.turn.started" || event.kind === "runtime.turn.completed" || event.kind === "runtime.gateway.ready" || event.kind === "runtime.session.started" || event.kind === "runtime.session.ended";
323
+ }
324
+ function rolOperativo(actor) {
325
+ if (actor === "seat") return "chair";
326
+ return E?.skills?.[actor]?.role || "";
327
+ }
328
+ function cara(actor, esChair) {
329
+ const uri = E?.avatars?.[actor];
330
+ if (uri && uri.startsWith("data:image/svg+xml;base64,")) {
331
+ return `<img class="liveCara" src="${esc(uri)}" alt="">`;
332
+ }
333
+ return esChair ? icono("chair") : esc(iniciales(actor));
334
+ }
335
+ function iniciales(actor) {
336
+ const parts = actor.split(/[-_.\s]+/).filter(Boolean);
337
+ const initials = parts.length > 1 ? parts.slice(0, 2).map((part) => part[0]).join("") : actor.slice(0, 2);
338
+ return initials.toUpperCase();
339
+ }
340
+ function tonoActor(actor) {
341
+ let hash = 17;
342
+ for (const character of actor) hash = (hash * 31 + character.charCodeAt(0)) % 360;
343
+ return hash;
344
+ }
345
+ function resumenVisible(summary) {
346
+ if (summary.length <= 520) return summary;
347
+ const boundary = summary.slice(0, 520).lastIndexOf(" ");
348
+ return summary.slice(0, boundary > 360 ? boundary : 520).trimEnd() + "\u2026";
349
+ }
350
+ function tituloVisible(event) {
351
+ if (event.kind === "conversation.user")
352
+ return event.actor === "seat" ? "Question" : "Brief received";
353
+ if (event.kind === "conversation.agent.commentary") return "Working note";
354
+ if (event.kind === "conversation.agent") return "Response";
355
+ if (event.kind === "committee.opened") return "Question to the committee";
356
+ if (event.kind === "committee.position.submitted") return "Position sealed";
357
+ if (event.kind === "committee.positions.revealed") return "Blind round complete";
358
+ if (event.kind === "committee.position.revealed") return "Position";
359
+ if (event.kind === "committee.synthesis.published") return "Chair synthesis";
360
+ if (event.kind === "committee.floor.requested") return "Requests the floor";
361
+ if (event.kind === "committee.floor.granted") return "Floor granted";
362
+ if (event.kind === "committee.floor.denied") return "Floor denied";
363
+ if (event.kind === "committee.floor.spoke") return "Intervention";
364
+ if (event.kind === "committee.decision.recorded") return "Decision";
365
+ if (event.kind.startsWith("committee.verification.")) return "Verification";
366
+ if (event.kind === "committee.closed") return "Committee closed";
367
+ return event.title;
368
+ }
369
+ function icono(kind) {
370
+ if (kind === "chair") {
371
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17h16l-1.4-9-4.1 3L12 5l-2.5 6-4.1-3L4 17Zm1 2h14"/></svg>';
372
+ }
373
+ if (kind.includes("question") || kind === "committee.opened" || kind === "conversation.user") {
374
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v10H9l-4 4V5Zm5 4a2 2 0 1 1 3.4 1.4c-.9.6-1.4 1-1.4 2.1M12 14.7v.1"/></svg>';
375
+ }
376
+ if (kind.includes("decision") || kind.includes("verification") || kind === "committee.closed") {
377
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>';
378
+ }
379
+ if (kind.includes("floor")) {
380
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 11V5a1.5 1.5 0 0 1 3 0v5-7a1.5 1.5 0 0 1 3 0v7-5a1.5 1.5 0 0 1 3 0v7l1-2a1.5 1.5 0 0 1 2.6 1.5L17 18a4 4 0 0 1-3.5 2H10a5 5 0 0 1-5-5v-3.5a1.5 1.5 0 0 1 3 0V13"/></svg>';
381
+ }
382
+ if (kind.includes("command") || kind.includes("work.")) {
383
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8 4 4-4 4m6 0h6"/></svg>';
384
+ }
385
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>';
386
+ }
387
+ function rail() {
388
+ q("#rail").innerHTML = SECCIONES.map(([id, et]) => {
389
+ const n = id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
390
+ return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
391
+ <span>${et}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
392
+ }).join("");
393
+ todos("#rail li").forEach((li) => {
394
+ li.onclick = () => {
395
+ SECCION = li.dataset.s ?? "mapa";
396
+ pinta();
397
+ };
398
+ });
399
+ q("#dondeDatos").textContent = corto(E.datos);
400
+ const cs = q("#ciudades");
401
+ if (E.ciudades.length > 1) {
402
+ cs.innerHTML = `<select id="cambiaCiudad" title="which city this hall manages">
403
+ ${E.ciudades.map(
404
+ (c) => `<option value="${esc(c.ruta)}" ${c.actual ? "selected" : ""}>
405
+ ${esc(c.nombre)}</option>`
406
+ ).join("")}</select>`;
407
+ q("#cambiaCiudad").onchange = (ev) => {
408
+ const ruta = ev.target.value;
409
+ location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(ruta);
410
+ };
411
+ } else {
412
+ cs.innerHTML = "";
413
+ }
414
+ const nueva = q("#alWizard");
415
+ nueva.onclick = async (ev) => {
416
+ ev.preventDefault();
417
+ const name = window.prompt("Name of the new city");
418
+ if (!name?.trim()) return;
419
+ const r = await api("/api/ciudades", {
420
+ method: "POST",
421
+ body: JSON.stringify({ name })
422
+ });
423
+ if (r.error || !r.city) return toast(r.error ?? "Could not create the city", true);
424
+ location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(r.city);
425
+ };
426
+ }
427
+ function pinta() {
428
+ rail();
429
+ q(".cuerpo").style.padding = "";
430
+ q("#lienzo").style.maxWidth = "";
431
+ VISTAS[SECCION]();
432
+ q(".cuerpo").scrollTop = 0;
433
+ }
434
+ var VISTAS = {};
435
+ VISTAS.resumen = () => {
436
+ const mia = E.tarjetas.find((t) => t.user === E.yo);
437
+ const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
438
+ const tareas = [
439
+ {
440
+ ok: !!mia,
441
+ txt: mia ? `Your card exists \u2014 you are <b>${esc(mia.agent)}</b>` : "Take your seat: role, folders, one goal",
442
+ ir: "puesto"
443
+ },
444
+ {
445
+ ok: !!(mia && mia.goals_defined),
446
+ txt: mia && mia.goals_defined ? `Your goal: <b>${esc(mia.objetivo ? mia.objetivo.title : "")}</b>` : "Set <b>one goal</b>, with the command that measures it",
447
+ ir: "puesto"
448
+ },
449
+ {
450
+ ok: E.parcelas.length > 0 && sinUnidad === 0,
451
+ txt: E.parcelas.length === 0 ? "No houses yet \u2014 pick your folders and they appear" : sinUnidad ? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district \u2014 assign them` : "Every house has its district",
452
+ ir: "barrios"
453
+ }
454
+ ];
455
+ const sesionArriba = E.tmux.includes(E.sesion);
456
+ q("#lienzo").innerHTML = `
457
+ <div><span class="sub">your city</span>
458
+ <h1 style="margin-top:6px">${esc(E.city_name)}</h1>
459
+ <p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
460
+ <p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
461
+ counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p></div>
462
+ <div class="cifras">
463
+ <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
464
+ <div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
465
+ <div class="cifra"><b>${E.roads.length}</b><span>roads</span></div>
466
+ <div class="cifra"><b>${E.deliberations.length}</b><span>committee acts</span></div>
467
+ <div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span>skills recognised</span></div>
468
+ </div>
469
+ <div class="luces">
470
+ <span class="luz ${E.tarjetas.length ? "on" : ""}">data repo</span>
471
+ <span class="luz ${E.gh ? "on" : "neutra"}">github${E.gh ? "" : " \u2014 optional"}</span>
472
+ <span class="luz ${E.plugin ? "on" : ""}">plugin installed</span>
473
+ <span class="luz ${sesionArriba ? "on" : "neutra"}">tmux session${sesionArriba ? " up" : ""}</span>
474
+ </div>
475
+ <div><span class="sub">what is left</span>
476
+ <div class="tareas" style="margin-top:9px">${tareas.map(
477
+ (t) => `
478
+ <div class="tarea ${t.ok ? "hecha" : ""}">${t.txt}
479
+ ${t.ok ? "" : `<button class="bt mini ir" data-ir="${t.ir}">go</button>`}</div>`
480
+ ).join("")}
481
+ </div></div>
482
+ <div><span class="sub">work</span>
483
+ <div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
484
+ <div class="orden"><span class="et2">your day</span>
485
+ <code>${sesionArriba ? "tmux attach -t " + esc(E.sesion) : "one window per folder, an agent in each"}</code>
486
+ ${sesionArriba ? `<button class="bt mini" data-copia="tmux attach -t ${esc(E.sesion)}">copy</button>` : '<button class="bt mini ppal" id="abreSesion">open my session</button>'}
487
+ </div>
488
+ <div class="orden"><span class="et2">the map</span>
489
+ <code>${E.mapa ? esc(E.mapa) : "not drawn yet"}</code>
490
+ <button class="bt mini ${E.mapa ? "" : "ppal"}" data-ir="mapa">${E.mapa ? "open" : "draw it"}</button></div>
491
+ </div></div>`;
492
+ enlaza();
493
+ const b = document.querySelector("#abreSesion");
494
+ if (b)
495
+ b.onclick = async () => {
496
+ b.disabled = true;
497
+ b.textContent = "building\u2026";
498
+ const r = await api("/api/sesion", {
499
+ method: "POST",
500
+ body: JSON.stringify({ user: E.yo })
501
+ });
502
+ await espera(1500);
503
+ await refresca();
504
+ toast(
505
+ r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : "Could not build it",
506
+ !r.ok
507
+ );
508
+ };
509
+ };
510
+ VISTAS.mapa = () => {
511
+ if (E.mapa) {
512
+ q(".cuerpo").style.padding = "0";
513
+ q("#lienzo").style.maxWidth = "none";
514
+ const mapUrl = new URL(E.mapa, location.href);
515
+ mapUrl.searchParams.set("embed", "1");
516
+ mapUrl.searchParams.set("parent_origin", location.origin);
517
+ q("#lienzo").innerHTML = `<iframe id="cityMapFrame" src="${esc(mapUrl.toString())}" title="the map"
518
+ allow="fullscreen" allowfullscreen
519
+ style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;
520
+ const frame = q("#cityMapFrame");
521
+ frame.onload = () => {
522
+ frame.dataset.ready = "1";
523
+ const origin = new URL(frame.src, location.href).origin;
524
+ frame.contentWindow?.postMessage(
525
+ {
526
+ protocol: MAP_ACTIVITY_PROTOCOL,
527
+ type: "map.config",
528
+ roads: E.roads.map((r) => ({ name: r.name, address: r.address })),
529
+ agents: (E.agents ?? []).map((a) => ({ name: a.name, kind: a.kind })),
530
+ avatars: E.avatars ?? {}
531
+ },
532
+ origin
533
+ );
534
+ frame.contentWindow?.postMessage(
535
+ { type: "agents-city-map-theme/1", theme: temaActual() },
536
+ origin
537
+ );
538
+ if (pendingMapActivity) {
539
+ const event = pendingMapActivity;
540
+ pendingMapActivity = null;
541
+ enviaActividadAlMapa(event);
542
+ }
543
+ };
544
+ return;
545
+ }
546
+ q("#lienzo").innerHTML = `<div><span class="sub">the map</span>
547
+ <h1 style="margin-top:6px">Not drawn yet</h1>
548
+ <p class="prosa" style="margin-top:8px">The map can be served independently.
549
+ Locally the hall runs it for you: first time bakes the front end
550
+ and seeds the local database, which takes about a minute.</p></div>
551
+ <div><button class="bt ppal" id="arrancaMapa">Draw my city</button></div>
552
+ <p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`;
553
+ q("#arrancaMapa").onclick = async () => {
554
+ q("#arrancaMapa").disabled = true;
555
+ q("#mapaEspera").style.display = "";
556
+ await api("/api/mapa", { method: "POST", body: "{}" });
557
+ for (let i = 0; i < 60; i++) {
558
+ await espera(3e3);
559
+ E = await api("/api/estado");
560
+ if (E.mapa) {
561
+ pinta();
562
+ return;
563
+ }
564
+ }
565
+ toast("It did not come up \u2014 run ./bin/city by hand and look at its output", true);
566
+ pinta();
567
+ };
568
+ };
569
+ VISTAS.puesto = () => {
570
+ void puesto();
571
+ };
572
+ async function puesto() {
573
+ const mia = E.tarjetas.find((t) => t.user === E.yo);
574
+ q("#lienzo").innerHTML = `<div><span class="sub">my seat</span>
575
+ <h1 style="margin-top:6px">${esc(E.yo)}</h1>
576
+ <p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
577
+ role inside it, and one goal. The seat stays the boss even when its
578
+ professional role is blank. Saving writes your card \u2014
579
+ <code class="mono">${esc(corto(E.datos))}/${esc(E.yo)}.md</code> \u2014 the same file
580
+ every other door writes, and it never touches your roster.</p></div>
581
+ <div><span class="sub">work domain</span><div class="rolejilla" id="domains" style="margin-top:9px">
582
+ <p class="cargando">reading the domain packs</p></div></div>
583
+ <div><span class="sub">role</span><div class="rolejilla" id="roles" style="margin-top:9px">
584
+ <p class="cargando">reading the role files</p></div></div>
585
+ <div><span class="sub">the agents</span>
586
+ <p class="prosa" style="margin-top:8px">Who works in this city \u2014 and what each
587
+ one works on \u2014 lives in <b>Agents &amp; skills</b>, where an agent is asked for
588
+ whole: its kind, its role, its repositories and document folders, its engine
589
+ and its skills. One place, so the terminal and this page cannot disagree.
590
+ <button class="bt" type="button" data-ir="gente" style="margin-left:8px">Open the roster</button></p></div>
591
+ <div><span class="sub">one goal \u2014 optional</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
592
+ <div style="display:flex;gap:10px;align-items:center">
593
+ <button class="bt ppal" id="guardaPuesto">Save my seat</button>
594
+ <span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
595
+ const domainsR = (await api("/api/domains")).domains;
596
+ let domain = E.domain || E.kind || "software";
597
+ let rol = mia ? mia.role : "";
598
+ let rolesR = [];
599
+ const pintaRoles = () => {
600
+ q("#roles").innerHTML = rolesR.map(
601
+ (r) => `
602
+ <button class="rol ${r.id === rol ? "on" : ""}" data-r="${esc(r.id)}">
603
+ <h3>${esc(r.name)}</h3><span class="de">${esc(r.id)} \xB7 ${esc(r.trade.toLowerCase())}</span>
604
+ <p>${esc(r.summary)}</p></button>`
605
+ ).join("");
606
+ todos("#roles .rol").forEach((b) => {
607
+ b.onclick = () => {
608
+ rol = b.dataset.r ?? "";
609
+ pintaRoles();
610
+ };
611
+ });
612
+ };
613
+ const cargaRoles = async () => {
614
+ rolesR = (await api("/api/roles?domain=" + encodeURIComponent(domain))).roles;
615
+ if (!rolesR.some((r) => r.id === rol)) rol = "";
616
+ pintaRoles();
617
+ };
618
+ const pintaDominios = () => {
619
+ q("#domains").innerHTML = domainsR.map(
620
+ (d) => `
621
+ <button class="rol ${d.id === domain ? "on" : ""}" data-d="${esc(d.id)}">
622
+ <h3>${esc(d.name)}</h3><span class="de">${esc(d.id)}</span>
623
+ <p>${esc(d.summary)}</p></button>`
624
+ ).join("");
625
+ todos("#domains .rol").forEach((b) => {
626
+ b.onclick = () => {
627
+ domain = b.dataset.d ?? "software";
628
+ pintaDominios();
629
+ void cargaRoles();
630
+ };
631
+ });
632
+ };
633
+ pintaDominios();
634
+ await cargaRoles();
635
+ const o = mia && mia.objetivo || {};
636
+ q("#meta").innerHTML = `
637
+ <div class="campo"><label>The goal, in one line</label>
638
+ <input type="text" id="g_title" value="${esc(o.title)}" placeholder="Concrete enough to argue with \u2014 empty skips it">
639
+ </div>
640
+ <div class="dos">
641
+ <div class="campo"><label>How it is measured</label>
642
+ <input type="text" id="g_signal" value="${esc(o.signal)}"></div>
643
+ <div class="campo"><label>The command that returns it, if a command can</label>
644
+ <input type="text" class="mono" id="g_command" value="${esc(o.command)}"
645
+ placeholder="empty for a qualitative goal"></div>
646
+ </div>
647
+ <div class="campo"><label>\u2026or who judges it, and how often</label>
648
+ <input type="text" id="g_manual" value="${esc(o.manual)}"
649
+ placeholder="in prose: the architect reads the AGENTS.md files on Fridays">
650
+ <p class="pista">A goal judged by a person is a real goal \u2014 plenty of quality
651
+ is prose, not a number. Used when the command is empty.</p></div>
652
+ <div class="dos">
653
+ <div class="campo"><label>What it returns today</label>
654
+ <input type="text" class="mono" id="g_baseline" value="${esc(o.baseline)}"></div>
655
+ <div class="campo"><label>Where it has to get to</label>
656
+ <input type="text" class="mono" id="g_target" value="${esc(o.target)}"></div>
657
+ </div>
658
+ <div class="campo" style="max-width:240px"><label>By when</label>
659
+ <input type="text" id="g_by" value="${esc(o.by ?? "this quarter")}"></div>`;
660
+ const val = (id) => q(id).value.trim();
661
+ q("#guardaPuesto").onclick = async () => {
662
+ if (!rol) {
663
+ toast("Pick a role \u2014 it is the name your seat answers to", true);
664
+ return;
665
+ }
666
+ q("#puestoEstado").style.display = "";
667
+ const r = await api("/api/ficha", {
668
+ method: "POST",
669
+ body: JSON.stringify({
670
+ user: E.yo,
671
+ domain,
672
+ role: rol,
673
+ objetivo: {
674
+ title: val("#g_title"),
675
+ signal: val("#g_signal"),
676
+ command: val("#g_command"),
677
+ manual: val("#g_manual"),
678
+ baseline: val("#g_baseline"),
679
+ target: val("#g_target"),
680
+ by: val("#g_by") || "this quarter"
681
+ }
682
+ })
683
+ });
684
+ await refresca();
685
+ toast(
686
+ r.ok ? `Card written \u2014 you are ${r.user ?? E.yo}` : r.error ?? "It could not write",
687
+ !r.ok
688
+ );
689
+ };
690
+ }
691
+ VISTAS.barrios = () => {
692
+ const unidades = E.unidades.map((u) => ({ ...u }));
693
+ const lab = new Set(E.lab);
694
+ const porRepo = {};
695
+ for (const p of E.parcelas) {
696
+ (porRepo[p.repo] = porRepo[p.repo] ?? []).push({
697
+ ruta: p.ruta,
698
+ unidad: p.unidad,
699
+ nombre: p.nombre
700
+ });
701
+ }
702
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 20);
703
+ const render = () => {
704
+ const idsValidos = /* @__PURE__ */ new Set([...unidades.map((u) => u.id), "none"]);
705
+ q("#lienzo").innerHTML = `<div><span class="sub">districts & houses</span>
706
+ <h1 style="margin-top:6px">The modelling no tool can do for you</h1>
707
+ <p class="prosa" style="margin-top:8px">A house is <b>not a repo</b> \u2014 it is a
708
+ parcel, a slice of one serving a single business unit. Split the interesting
709
+ repos, give every house its district, and the map can say
710
+ \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.</p></div>
711
+
712
+ <div><span class="sub">districts</span>
713
+ <div class="tabla" id="unidades" style="margin-top:9px"></div>
714
+ <div style="display:flex;gap:9px;margin-top:9px">
715
+ <input type="text" id="nuevaU" placeholder="another district\u2026" style="flex:1">
716
+ <button class="bt" id="masU">Add</button></div></div>
717
+
718
+ <div><span class="sub">houses</span><div id="parcelas" style="margin-top:9px"></div></div>
719
+
720
+ <div style="display:flex;gap:10px;align-items:center">
721
+ <button class="bt ppal" id="guardaBarrios">Save districts & houses</button>
722
+ <span style="font-size:12px;color:var(--tinta3);font-style:italic">
723
+ writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div>`;
724
+ q("#unidades").innerHTML = unidades.map(
725
+ (u, i) => `
726
+ <div class="tr">
727
+ <input type="text" value="${esc(u.name)}" data-i="${i}" style="flex:1">
728
+ <span class="id">${esc(u.id)}</span>
729
+ <span class="pips" data-i="${i}">${E.paleta.map(
730
+ (p) => `
731
+ <button class="pip ${p.hex === u.color ? "on" : ""}" data-c="${p.hex}"
732
+ style="background:#${p.hex}" title="${esc(p.nombre)}"></button>`
733
+ ).join("")}</span>
734
+ <button class="x" data-x="${i}">\xD7</button></div>`
735
+ ).join("") || '<div class="tr" style="color:var(--tinta3);font-size:12.5px">No districts yet \u2014 everything sits in \u201Cno unit\u201D.</div>';
736
+ q("#parcelas").innerHTML = Object.keys(porRepo).sort().map(
737
+ (repo) => `
738
+ <div class="repoBloque" data-repo="${esc(repo)}">
739
+ <div class="repoCab"><span>${esc(repo)}</span>
740
+ <button class="bt mini" data-split="${esc(repo)}">split</button>
741
+ <label class="lab"><input type="checkbox" data-lab="${esc(repo)}"
742
+ ${lab.has(repo) ? "checked" : ""}> lab</label></div>
743
+ ${(porRepo[repo] ?? []).map(
744
+ (f, j) => `
745
+ <div class="parcela">
746
+ <input type="text" value="${esc(f.nombre)}" data-k="nombre" data-r="${esc(repo)}" data-j="${j}" title="name on the map">
747
+ <input type="text" class="mono" value="${esc(f.ruta)}" data-k="ruta" data-r="${esc(repo)}" data-j="${j}"
748
+ placeholder="glob inside the repo \u2014 empty = the whole repo">
749
+ <select data-k="unidad" data-r="${esc(repo)}" data-j="${j}">
750
+ ${[...unidades.map((u) => u.id), "none"].map(
751
+ (id) => `
752
+ <option value="${esc(id)}" ${(idsValidos.has(f.unidad) ? f.unidad : "none") === id ? "selected" : ""}>${esc(id)}</option>`
753
+ ).join("")}
754
+ </select>
755
+ ${(porRepo[repo] ?? []).length > 1 ? `<button class="x" data-quita="${esc(repo)}" data-j="${j}">\xD7</button>` : "<span></span>"}
756
+ </div>`
757
+ ).join("")}
758
+ </div>`
759
+ ).join("") || '<p class="prosa">No houses yet. Pick your folders in <b>My seat</b> and each becomes one.</p>';
760
+ todos("#unidades input[data-i]").forEach((inp) => {
761
+ inp.oninput = () => {
762
+ const u = unidades[Number(inp.dataset.i)];
763
+ if (!u) return;
764
+ u.name = inp.value;
765
+ u.id = slug(inp.value) || u.id;
766
+ const idEl = inp.closest(".tr")?.querySelector(".id");
767
+ if (idEl) idEl.textContent = u.id;
768
+ };
769
+ });
770
+ todos("#unidades .pip").forEach((b) => {
771
+ b.onclick = () => {
772
+ const grupo = b.closest(".pips");
773
+ const u = unidades[Number(grupo?.dataset.i)];
774
+ if (u) {
775
+ u.color = b.dataset.c ?? u.color;
776
+ render();
777
+ }
778
+ };
779
+ });
780
+ todos("#unidades .x[data-x]").forEach((b) => {
781
+ b.onclick = () => {
782
+ unidades.splice(Number(b.dataset.x), 1);
783
+ render();
784
+ };
785
+ });
786
+ q("#masU").onclick = () => {
787
+ const n = q("#nuevaU").value.trim();
788
+ if (!n) return;
789
+ const usados = unidades.map((u) => u.color);
790
+ const libre = E.paleta.find((p) => !usados.includes(p.hex)) ?? E.paleta[0];
791
+ unidades.push({ id: slug(n), name: n, color: libre ? libre.hex : "c8b48a" });
792
+ render();
793
+ };
794
+ q("#nuevaU").onkeydown = (e) => {
795
+ if (e.key === "Enter") q("#masU").click();
796
+ };
797
+ todos("#parcelas [data-k]").forEach((el) => {
798
+ el.onchange = () => {
799
+ const fila = porRepo[el.dataset.r ?? ""]?.[Number(el.dataset.j)];
800
+ if (!fila) return;
801
+ const k = el.dataset.k;
802
+ fila[k] = el.value;
803
+ };
804
+ });
805
+ todos("#parcelas [data-split]").forEach((b) => {
806
+ b.onclick = () => {
807
+ const r = b.dataset.split ?? "";
808
+ (porRepo[r] = porRepo[r] ?? []).push({
809
+ ruta: "",
810
+ unidad: "none",
811
+ nombre: `${r} \xB7 new slice`
812
+ });
813
+ render();
814
+ };
815
+ });
816
+ todos("#parcelas [data-quita]").forEach((b) => {
817
+ b.onclick = () => {
818
+ porRepo[b.dataset.quita ?? ""]?.splice(Number(b.dataset.j), 1);
819
+ render();
820
+ };
821
+ });
822
+ todos("#parcelas [data-lab]").forEach((c) => {
823
+ c.onchange = () => {
824
+ const r = c.dataset.lab ?? "";
825
+ if (c.checked) lab.add(r);
826
+ else lab.delete(r);
827
+ };
828
+ });
829
+ q("#guardaBarrios").onclick = async () => {
830
+ const r1 = await api("/api/unidades", {
831
+ method: "POST",
832
+ body: JSON.stringify({ unidades })
833
+ });
834
+ const r2 = await api("/api/parcelas", {
835
+ method: "POST",
836
+ body: JSON.stringify({ repos: porRepo, lab: [...lab] })
837
+ });
838
+ await refresca();
839
+ const mal = r1.error ?? r2.error;
840
+ toast(mal ?? "Districts and houses written", !!mal);
841
+ };
842
+ };
843
+ render();
844
+ };
845
+ VISTAS.red = () => {
846
+ const conectadas = new Set(E.roads.map((r) => r.id));
847
+ const locales = E.ciudades.filter((c) => !c.actual && c.id && !conectadas.has(c.id));
848
+ const invitation = JSON.stringify(E.invitation);
849
+ q("#lienzo").innerHTML = `<div><span class="sub">roads</span>
850
+ <h1 style="margin-top:6px">Cities this one may reach</h1>
851
+ <p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
852
+ this machine or continue over the remote bus; the city sees one explicit
853
+ connection either way.</p></div>
854
+ <div class="gente">${E.roads.map(
855
+ (r) => `<div class="persona"><h3>${esc(r.name)}</h3>
856
+ <span class="ag">${esc(r.address)}</span>
857
+ <span class="rp">${r.local ? "local road" : "remote road"}</span>
858
+ <button class="bt mini" data-road-close="${esc(r.id)}">close</button></div>`
859
+ ).join("") || '<p class="prosa">No roads yet. This city is isolated on purpose.</p>'}</div>
860
+ <div><span class="sub">other cities on this machine</span>
861
+ <div class="gente" style="margin-top:9px">${locales.map(
862
+ (c) => `<div class="persona"><h3>${esc(c.nombre)}</h3>
863
+ <span class="ag">${esc(c.slug ?? c.ruta)}</span>
864
+ <button class="bt mini ppal" data-road-open="${esc(c.id)}">open road</button></div>`
865
+ ).join("") || '<p class="prosa">No unconnected local cities.</p>'}</div></div>
866
+ <div class="orden"><span class="et2">remote invitation \xB7 public, no token</span>
867
+ <code>${esc(invitation)}</code>
868
+ <button class="bt mini" data-copia="${esc(invitation)}">copy</button>
869
+ </div>`;
870
+ enlaza();
871
+ todos("[data-road-open]").forEach((b) => {
872
+ b.onclick = async () => {
873
+ const r = await api("/api/roads", {
874
+ method: "POST",
875
+ body: JSON.stringify({ action: "connect", target: b.dataset.roadOpen })
876
+ });
877
+ if (r.error) return toast(r.error, true);
878
+ await refresca();
879
+ toast("Road open at both local cities");
880
+ };
881
+ });
882
+ todos("[data-road-close]").forEach((b) => {
883
+ b.onclick = async () => {
884
+ const r = await api("/api/roads", {
885
+ method: "POST",
886
+ body: JSON.stringify({ action: "disconnect", target: b.dataset.roadClose })
887
+ });
888
+ if (r.error) return toast(r.error, true);
889
+ await refresca();
890
+ toast("Road closed");
891
+ };
892
+ });
893
+ };
894
+ VISTAS.committee = () => {
895
+ q("#lienzo").innerHTML = `<div><span class="sub">committee</span>
896
+ <h1 style="margin-top:6px">Decisions with a visible chain of custody</h1>
897
+ <p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
898
+ gathers isolated positions, controls the floor, decides and assigns an
899
+ independent verification. This view is read-only; the vendor-neutral CLI
900
+ drives the protocol.</p></div>
901
+ <div class="orden"><span class="et2">start</span>
902
+ <code>agents-city committee schema open</code>
903
+ <button class="bt mini" data-copia="agents-city committee schema open">copy</button>
904
+ </div>
905
+ <div class="gente">${E.deliberations.map(
906
+ (act) => `<div class="persona">
907
+ <h3>${esc(act.question)}</h3>
908
+ <span class="ag">${esc(act.status)} \xB7 revision ${act.revision} \xB7
909
+ ${act.received}/${act.total} positions</span>
910
+ <span class="rp">${esc(act.decision || act.desired_outcome)}</span>
911
+ <span class="de">${esc(act.participants.join(", ") || "no participants")}
912
+ ${act.contributors.length ? ` \xB7 decisive ${esc(act.contributors.join(", "))}` : ""}
913
+ ${act.verifier ? ` \xB7 verifier ${esc(act.verifier)} ${esc(act.verification)}` : ""}</span>
914
+ <code class="mono">${esc(corto(act.act))}</code>
915
+ </div>`
916
+ ).join("") || '<p class="prosa">No committee acts yet. Open one only when the seat needs specialised evidence.</p>'}</div>`;
917
+ enlaza();
918
+ };
919
+ function nivelDeMotor(model) {
920
+ const alias = model.toLowerCase();
921
+ if (!alias) return { ancho: 0.5, texto: "default", defecto: true };
922
+ for (const [clave, ancho] of [
923
+ ["fable", 1],
924
+ ["opus", 0.8],
925
+ ["sonnet", 0.55],
926
+ ["haiku", 0.35]
927
+ ]) {
928
+ if (alias.includes(clave)) return { ancho, texto: alias, defecto: false };
929
+ }
930
+ return { ancho: 0.6, texto: alias, defecto: false };
931
+ }
932
+ var NIVEL_ESFUERZO = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 };
933
+ var MODELOS = ["haiku", "sonnet", "opus", "fable"];
934
+ function opciones(valores, actual) {
935
+ return valores.concat(actual && !valores.includes(actual) ? [actual] : []).map(
936
+ (v) => `<option value="${esc(v)}"${v === actual ? " selected" : ""}>${esc(v || "default")}</option>`
937
+ ).join("");
938
+ }
939
+ function caraRPG(uri) {
940
+ return uri?.startsWith("data:image/svg+xml;base64,") ? `<img class="rpgCara" src="${esc(uri)}" alt="">` : "";
941
+ }
942
+ function barra(ancho, clase = "") {
943
+ const pct = Math.round(Math.max(0, Math.min(1, ancho)) * 100);
944
+ return `<span class="barra${clase ? " " + clase : ""}"><span class="llena" style="width:${pct}%"></span></span>`;
945
+ }
946
+ VISTAS.gente = () => {
947
+ const mia = E.tarjetas.find((t) => t.user === E.yo);
948
+ const agentes = E.agents ?? [];
949
+ const fichaDe = (a) => {
950
+ const motor = nivelDeMotor(a.model);
951
+ const esfuerzo = NIVEL_ESFUERZO[a.effort] ?? 0;
952
+ const g = a.growth;
953
+ const crecido = Math.min(1, Math.log2(g.floors + 1) / 8);
954
+ const skills = E.skills[a.name]?.skills ?? [];
955
+ const esClaude = a.runtime === "claude";
956
+ const opcionesModelo = opciones([""].concat(MODELOS), a.model) + '<option value="__otro__">custom\u2026</option>';
957
+ const opcionesEsfuerzo = opciones([""].concat(Object.keys(NIVEL_ESFUERZO)), a.effort);
958
+ const opcionesRuntime = opciones(["claude", "codex", "opencode", "kimi"], a.runtime);
959
+ const montajes = a.mounts ?? [];
960
+ return `
961
+ <div class="fichaRPG">
962
+ <div class="rpgCab">${caraRPG(a.avatar)}
963
+ <div><h3>${esc(a.name)}</h3>
964
+ <span class="rpgTags"><span class="kindChip ${esc(a.kind)}">${esc(a.kind)}</span>
965
+ <span class="rpgRol">${esc(a.role)}</span>
966
+ <span class="rpgHogar">${a.legacy ? "repo" : `workspace \xB7 ${montajes.length} ${montajes.length === 1 ? "mount" : "mounts"}`}</span></span></div>
967
+ <span class="rpgBotones">
968
+ <button class="rpgMini rpgDado" type="button" data-agente="${esc(a.slug)}"
969
+ title="Reroll this agent's face \u2014 deterministic, persisted on the card">\u{1F3B2}</button>
970
+ <button class="rpgMini rpgIns" type="button" data-agente="${esc(a.slug)}"
971
+ data-file="CLAUDE.md" title="Instructions the Claude runtime reads">CLAUDE.md</button>
972
+ <button class="rpgMini rpgIns" type="button" data-agente="${esc(a.slug)}"
973
+ data-file="AGENTS.md" title="Instructions Codex, OpenCode and Kimi read">AGENTS.md</button>
974
+ </span>
975
+ </div>
976
+ <div class="rpgFila"><label>engine</label>
977
+ ${barra(motor.ancho, motor.defecto ? "defecto" : "")}
978
+ <select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="model"
979
+ ${esClaude ? "" : 'disabled title="This window runs its own CLI; its model lives in its own flags"'}>
980
+ ${opcionesModelo}</select></div>
981
+ <div class="rpgFila"><label>effort</label>
982
+ ${barra(esfuerzo / 5, esfuerzo ? "" : "defecto")}
983
+ <select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="effort"
984
+ ${esClaude ? "" : 'disabled title="This window runs its own CLI; its effort lives in its own flags"'}>
985
+ ${opcionesEsfuerzo}</select></div>
986
+ <div class="rpgFila"><label>provider</label>
987
+ <span class="rpgDato"><span class="cliDot ${a.cli.connected ? "on" : a.cli.installed ? "idle" : "off"}" title="${esc(
988
+ a.cli.connected ? "connected: this agent\u2019s window is alive right now" : a.cli.installed ? "installed on this machine, not in use by this agent" : `${a.cli.binary} is not installed`
989
+ )}"></span>${esc(
990
+ a.cli.connected ? "connected" : a.cli.installed ? "idle" : "missing"
991
+ )} \xB7 ${esc(a.cli.binary)}
992
+ <button class="rpgMini rpgTest" type="button" data-agente="${esc(a.slug)}"
993
+ title="Run the engine for real: --version, and the login state on Claude">test</button></span>
994
+ <select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="runtime">
995
+ ${opcionesRuntime}</select></div>
996
+ <div class="rpgSkills"><label>works on \xB7 ${montajes.length}</label>
997
+ ${montajes.length ? montajes.map(
998
+ (m) => `<code class="mono" title="${esc(m.target)}">${esc(m.label)}${m.fixed ? "" : `<button class="rpgQuitar rpgDesmonta" type="button"
999
+ data-agente="${esc(a.slug)}" data-mount="${esc(m.label)}"
1000
+ title="Stop this agent working on ${esc(m.target)}">\xD7</button>`}</code>`
1001
+ ).join(" ") : '<span class="rpgDato">nothing mounted yet</span>'}
1002
+ ${a.legacy ? '<span class="rpgDato">a legacy repo agent works on its own repo</span>' : `<button class="rpgMini rpgMonta" type="button" data-agente="${esc(a.slug)}"
1003
+ title="A repo, a worktree or a folder of documents \u2014 this agent works on all of them">+ folder</button>`}</div>
1004
+ <div class="rpgFila"><label>growth</label>
1005
+ ${barra(crecido)}
1006
+ <span class="rpgDato">${g.floors | 0} floors \xB7 ${g.bricks | 0} bricks \xB7 ${g.activity30 | 0}/30d</span></div>
1007
+ <div class="rpgSkills"><label>skills \xB7 ${skills.length}</label>
1008
+ ${skills.length ? skills.map(
1009
+ (s) => `<code class="mono" title="${esc(s.description)}">${esc(s.name)}${s.removable && s.dir ? `<button class="rpgQuitar" type="button" data-agente="${esc(a.slug)}"
1010
+ data-skill="${esc(s.dir)}" title="Remove this skill from the agent's home">\xD7</button>` : ""}</code>`
1011
+ ).join(" ") : '<span class="rpgDato">none discovered</span>'}
1012
+ <label class="rpgMini rpgSubir"
1013
+ title="Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them">
1014
+ + zip<input type="file" accept=".zip" data-agente="${esc(a.slug)}"></label></div>
1015
+ </div>`;
1016
+ };
1017
+ q("#lienzo").innerHTML = `<div><span class="sub">agents &amp; skills</span>
1018
+ <h1 style="margin-top:6px">One seat, the agents behind it</h1>
1019
+ <p class="prosa" style="margin-top:8px">A city is its agents, and each one is
1020
+ whole here: the kind of work it does, its role, everything it works on \u2014 any
1021
+ number of repositories and document folders at once \u2014 the engine and effort
1022
+ that run it, and the skills in its own home. Every number is real: growth
1023
+ from what the agent actually produced, skills from live read-only
1024
+ recognition. Changes persist to the card and apply the next time the session
1025
+ opens.</p>
1026
+ <button class="rpgAlta" type="button" id="altaAgente">+ Add an agent</button></div>
1027
+ <div class="fichasRPG">
1028
+ <div class="fichaRPG rpgSeat">
1029
+ <div class="rpgCab">${caraRPG(E.avatars?.["seat"])}
1030
+ <div><h3>seat</h3>
1031
+ <span class="rpgTags"><span class="kindChip coordinator">chair</span>
1032
+ <span class="rpgRol">${esc(mia?.role ?? "not configured")}</span></span></div>
1033
+ </div>
1034
+ <div class="rpgFila"><span class="rpgDato">${esc(E.address)}</span></div>
1035
+ <div class="rpgFila"><span class="rpgDato">${esc(mia?.objetivo?.title ?? "No goal yet")}</span></div>
1036
+ </div>
1037
+ ${agentes.map(fichaDe).join("")}
1038
+ </div>`;
1039
+ const guardaAgente = async (carga, mensaje) => {
1040
+ let r;
1041
+ try {
1042
+ r = await api("/api/agente", { method: "POST", body: JSON.stringify(carga) });
1043
+ } catch (e) {
1044
+ toast(String(e), true);
1045
+ pinta();
1046
+ return;
1047
+ }
1048
+ if (!r.ok || !r.agent) {
1049
+ toast(r.error || "Could not save", true);
1050
+ pinta();
1051
+ return;
1052
+ }
1053
+ E.agents = (E.agents ?? []).map((x) => x.slug === r.agent.slug ? r.agent : x);
1054
+ if (E.avatars) E.avatars[r.agent.name] = r.agent.avatar;
1055
+ toast(mensaje);
1056
+ pinta();
1057
+ };
1058
+ todos(".rpgSel").forEach((sel) => {
1059
+ sel.onchange = async () => {
1060
+ let valor = sel.value;
1061
+ if (valor === "__otro__") {
1062
+ const propio = window.prompt("Model alias (resolved by the CLI at launch):", "");
1063
+ if (propio === null) {
1064
+ pinta();
1065
+ return;
1066
+ }
1067
+ valor = propio.trim().toLowerCase();
1068
+ }
1069
+ await guardaAgente(
1070
+ { agent: sel.dataset.agente ?? "", [sel.dataset.campo ?? "model"]: valor },
1071
+ "Saved \u2014 applies next session"
1072
+ );
1073
+ };
1074
+ });
1075
+ todos(".rpgDado").forEach((boton) => {
1076
+ boton.onclick = () => guardaAgente(
1077
+ { agent: boton.dataset.agente ?? "", avatar: Math.random().toString(36).slice(2, 8) },
1078
+ "New look \u2014 same identity, everywhere, forever"
1079
+ );
1080
+ });
1081
+ todos(".rpgTest").forEach((boton) => {
1082
+ boton.onclick = async () => {
1083
+ boton.disabled = true;
1084
+ try {
1085
+ const r = await api("/api/motor", {
1086
+ method: "POST",
1087
+ body: JSON.stringify({ agent: boton.dataset.agente ?? "" })
1088
+ });
1089
+ if (r.error) {
1090
+ toast(r.error, true);
1091
+ return;
1092
+ }
1093
+ const partes = [r.version, r.detail].filter(Boolean).join(" \xB7 ");
1094
+ toast(
1095
+ r.ok ? `${r.binary} works \u2014 ${partes}` : `${r.binary}: ${r.detail || "failed"}`,
1096
+ !r.ok
1097
+ );
1098
+ } catch (e) {
1099
+ toast(String(e), true);
1100
+ } finally {
1101
+ boton.disabled = false;
1102
+ }
1103
+ };
1104
+ });
1105
+ todos(".rpgQuitar").forEach((boton) => {
1106
+ boton.onclick = async () => {
1107
+ const nombre = boton.dataset.skill ?? "";
1108
+ if (!window.confirm(`Remove the skill ${nombre} from this agent's home?`)) return;
1109
+ const r = await api("/api/skill", {
1110
+ method: "POST",
1111
+ body: JSON.stringify({ agent: boton.dataset.agente ?? "", remove: nombre })
1112
+ });
1113
+ if (!r.ok) {
1114
+ toast(r.error || "Could not remove", true);
1115
+ return;
1116
+ }
1117
+ toast(`Skill ${nombre} removed`);
1118
+ void refresca();
1119
+ };
1120
+ });
1121
+ const alta = document.getElementById("altaAgente");
1122
+ if (alta)
1123
+ alta.onclick = async () => {
1124
+ const nombre = window.prompt("The agent's name:", "");
1125
+ if (nombre === null || !nombre.trim()) return;
1126
+ const clase = window.prompt("What kind of work does it do? code, knowledge or coordinator", "code") ?? "";
1127
+ if (!clase.trim()) return;
1128
+ const rol = window.prompt("Its role (its specialty, not authority):", "blank") ?? "";
1129
+ if (!rol.trim()) return;
1130
+ try {
1131
+ const r = await api("/api/agentes", {
1132
+ method: "POST",
1133
+ body: JSON.stringify({ name: nombre.trim(), kind: clase.trim(), role: rol.trim() })
1134
+ });
1135
+ if (!r.ok) {
1136
+ toast(r.error || "Could not add the agent", true);
1137
+ return;
1138
+ }
1139
+ toast(`${nombre.trim()} joined this city \u2014 give it something to work on`);
1140
+ void refresca();
1141
+ } catch (e) {
1142
+ toast(String(e), true);
1143
+ }
1144
+ };
1145
+ const montaje = async (carga, hecho) => {
1146
+ try {
1147
+ const r = await api("/api/montaje", {
1148
+ method: "POST",
1149
+ body: JSON.stringify(carga)
1150
+ });
1151
+ if (!r.ok) {
1152
+ toast(r.error || "Could not change the mounts", true);
1153
+ return;
1154
+ }
1155
+ toast(hecho);
1156
+ void refresca();
1157
+ } catch (e) {
1158
+ toast(String(e), true);
1159
+ }
1160
+ };
1161
+ todos(".rpgMonta").forEach((boton) => {
1162
+ boton.onclick = () => {
1163
+ const ruta = window.prompt("A repo, a worktree or a folder of documents:", "~/");
1164
+ if (ruta === null || !ruta.trim()) return;
1165
+ void montaje({ agent: boton.dataset.agente ?? "", add: ruta.trim() }, "Mounted");
1166
+ };
1167
+ });
1168
+ todos(".rpgDesmonta").forEach((boton) => {
1169
+ boton.onclick = () => {
1170
+ const etiqueta = boton.dataset.mount ?? "";
1171
+ if (!window.confirm(`Stop this agent working on ${etiqueta}?`)) return;
1172
+ void montaje(
1173
+ { agent: boton.dataset.agente ?? "", remove: etiqueta },
1174
+ `${etiqueta} unmounted \u2014 the folder itself is untouched`
1175
+ );
1176
+ };
1177
+ });
1178
+ todos(".rpgIns").forEach((boton) => {
1179
+ boton.onclick = () => void abreInstrucciones(boton.dataset.agente ?? "", boton.dataset.file ?? "");
1180
+ });
1181
+ todos(".rpgSubir input").forEach((entrada) => {
1182
+ entrada.onchange = async () => {
1183
+ const archivo = entrada.files?.[0];
1184
+ if (!archivo) return;
1185
+ entrada.disabled = true;
1186
+ try {
1187
+ const zip = await aBase64(archivo);
1188
+ const nombre = archivo.name.replace(/\.zip$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1189
+ const r = await api("/api/skill", {
1190
+ method: "POST",
1191
+ body: JSON.stringify({ agent: entrada.dataset.agente ?? "", name: nombre, zip })
1192
+ });
1193
+ if (!r.ok) {
1194
+ toast(r.error || "Could not install", true);
1195
+ return;
1196
+ }
1197
+ toast(`Skill ${r.skill} installed \u2014 the Claude runtime reads it next session`);
1198
+ void refresca();
1199
+ } catch (e) {
1200
+ toast(String(e), true);
1201
+ } finally {
1202
+ entrada.disabled = false;
1203
+ entrada.value = "";
1204
+ }
1205
+ };
1206
+ });
1207
+ };
1208
+ function aBase64(archivo) {
1209
+ return new Promise((listo, falla) => {
1210
+ const lector = new FileReader();
1211
+ lector.onerror = () => falla(lector.error);
1212
+ lector.onload = () => listo(String(lector.result).split(",", 2)[1] ?? "");
1213
+ lector.readAsDataURL(archivo);
1214
+ });
1215
+ }
1216
+ var editorAbierto = null;
1217
+ async function abreInstrucciones(agente, archivo) {
1218
+ const r = await api(`/api/instrucciones?agent=${encodeURIComponent(agente)}&file=${encodeURIComponent(archivo)}`);
1219
+ if (r.error) {
1220
+ toast(r.error, true);
1221
+ return;
1222
+ }
1223
+ editorAbierto = { agent: agente, file: archivo };
1224
+ q("#edTitulo").textContent = `${agente} \xB7 ${archivo}`;
1225
+ q("#edLector").textContent = `read by ${r.reader ?? "?"}${r.exists ? "" : " \xB7 new file"}`;
1226
+ q("#edRuta").textContent = r.home ? `${r.home}/${archivo}` : "";
1227
+ q("#edTexto").value = r.content ?? "";
1228
+ q("#editorIns").hidden = false;
1229
+ q("#edTexto").focus();
1230
+ }
1231
+ document.getElementById("edCerrar")?.addEventListener("click", () => {
1232
+ q("#editorIns").hidden = true;
1233
+ editorAbierto = null;
1234
+ });
1235
+ document.getElementById("edGuardar")?.addEventListener("click", async () => {
1236
+ if (!editorAbierto) return;
1237
+ const r = await api("/api/instrucciones", {
1238
+ method: "POST",
1239
+ body: JSON.stringify({ ...editorAbierto, content: q("#edTexto").value })
1240
+ });
1241
+ if (!r.ok) {
1242
+ toast(r.error || "Could not save", true);
1243
+ return;
1244
+ }
1245
+ toast("Saved");
1246
+ q("#editorIns").hidden = true;
1247
+ editorAbierto = null;
1248
+ });
1249
+ function enlaza() {
1250
+ todos("[data-copia]").forEach((b) => {
1251
+ b.onclick = async () => {
1252
+ try {
1253
+ await navigator.clipboard.writeText(b.dataset.copia ?? "");
1254
+ toast("Copied");
1255
+ } catch {
1256
+ toast("Could not copy \u2014 select it by hand", true);
1257
+ }
1258
+ };
1259
+ });
1260
+ todos("[data-ir]").forEach((b) => {
1261
+ b.onclick = () => {
1262
+ SECCION = b.dataset.ir ?? "mapa";
1263
+ pinta();
1264
+ };
1265
+ });
1266
+ }
1267
+ function arrastreDelRail() {
1268
+ const app = document.getElementById("app");
1269
+ const asa = document.getElementById("liveResize");
1270
+ const rail2 = document.getElementById("livePanel");
1271
+ if (!app || !asa || !rail2) return;
1272
+ const CLAVE = "hall-live-width";
1273
+ const aplica = (px) => {
1274
+ if (px === null) app.style.removeProperty("--live-ancho");
1275
+ else app.style.setProperty("--live-ancho", `${Math.round(px)}px`);
1276
+ };
1277
+ const tope = (px) => Math.max(340, Math.min(px, Math.max(420, window.innerWidth * 0.6)));
1278
+ try {
1279
+ const guardado = Number(localStorage.getItem(CLAVE));
1280
+ if (guardado > 0) aplica(tope(guardado));
1281
+ } catch {
1282
+ }
1283
+ asa.addEventListener("pointerdown", (e) => {
1284
+ e.preventDefault();
1285
+ const desdeX = e.clientX;
1286
+ const desdeAncho = rail2.getBoundingClientRect().width;
1287
+ app.classList.add("liveResizing");
1288
+ asa.setPointerCapture(e.pointerId);
1289
+ const mueve = (ev) => aplica(tope(desdeAncho + (desdeX - ev.clientX)));
1290
+ const suelta = () => {
1291
+ app.classList.remove("liveResizing");
1292
+ asa.removeEventListener("pointermove", mueve);
1293
+ asa.removeEventListener("pointerup", suelta);
1294
+ asa.removeEventListener("pointercancel", suelta);
1295
+ try {
1296
+ localStorage.setItem(CLAVE, String(Math.round(rail2.getBoundingClientRect().width)));
1297
+ } catch {
1298
+ }
1299
+ };
1300
+ asa.addEventListener("pointermove", mueve);
1301
+ asa.addEventListener("pointerup", suelta);
1302
+ asa.addEventListener("pointercancel", suelta);
1303
+ });
1304
+ asa.addEventListener("dblclick", () => {
1305
+ aplica(null);
1306
+ try {
1307
+ localStorage.removeItem(CLAVE);
1308
+ } catch {
1309
+ }
1310
+ });
1311
+ }
1312
+ window.addEventListener("message", (message) => {
1313
+ if (!isMapNavMessage(message.data)) return;
1314
+ const frame = document.querySelector("#cityMapFrame");
1315
+ if (!frame || message.source !== frame.contentWindow) return;
1316
+ if (message.origin !== new URL(frame.src, location.href).origin) return;
1317
+ const view = message.data.view;
1318
+ if (view !== "committee" && view !== "red") return;
1319
+ SECCION = view;
1320
+ pinta();
1321
+ });
1322
+ function temaActual() {
1323
+ const elegido = document.documentElement.getAttribute("data-tema");
1324
+ if (elegido) return elegido === "claro" ? "light" : "dark";
1325
+ return window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark";
1326
+ }
1327
+ function tema() {
1328
+ const boton = document.getElementById("temaBoton");
1329
+ const guardado = (() => {
1330
+ try {
1331
+ return localStorage.getItem("hall-tema") ?? "";
1332
+ } catch {
1333
+ return "";
1334
+ }
1335
+ })();
1336
+ const claroDelSistema = window.matchMedia?.("(prefers-color-scheme: light)").matches ?? false;
1337
+ const pon = (elegido) => {
1338
+ const raiz = document.documentElement;
1339
+ if (elegido) raiz.setAttribute("data-tema", elegido);
1340
+ else raiz.removeAttribute("data-tema");
1341
+ const claro = elegido ? elegido === "claro" : claroDelSistema;
1342
+ if (boton) boton.textContent = claro ? "\u2600 Day" : "\u263E Night";
1343
+ const frame = document.querySelector("#cityMapFrame");
1344
+ frame?.contentWindow?.postMessage(
1345
+ { type: "agents-city-map-theme/1", theme: claro ? "light" : "dark" },
1346
+ new URL(frame.src, location.href).origin
1347
+ );
1348
+ };
1349
+ pon(guardado);
1350
+ if (boton)
1351
+ boton.onclick = () => {
1352
+ const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : claroDelSistema;
1353
+ const elegido = claroAhora ? "oscuro" : "claro";
1354
+ try {
1355
+ localStorage.setItem("hall-tema", elegido);
1356
+ } catch {
1357
+ }
1358
+ pon(elegido);
1359
+ };
1360
+ }
1361
+ arrastreDelRail();
1362
+ tema();
1363
+ void refresca();