agents-city 0.3.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2021 -0
  5. package/README.md +1999 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +116 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/exit +5 -0
  28. package/bin/hall +73 -0
  29. package/bin/hall.html +426 -0
  30. package/bin/logs +3 -0
  31. package/bin/report.py +14 -0
  32. package/bin/reset +3 -0
  33. package/bin/road +3 -0
  34. package/bin/seat +10 -0
  35. package/bin/serve.py +1428 -0
  36. package/bin/setup.py +753 -0
  37. package/bin/skills +3 -0
  38. package/bin/test +131 -0
  39. package/bin/test-adapter.py +183 -0
  40. package/bin/test-admision.py +99 -0
  41. package/bin/test-avatar.py +58 -0
  42. package/bin/test-benchmark.py +40 -0
  43. package/bin/test-broker.py +334 -0
  44. package/bin/test-cage.py +206 -0
  45. package/bin/test-card.py +402 -0
  46. package/bin/test-channel.py +379 -0
  47. package/bin/test-cities.py +453 -0
  48. package/bin/test-claude-runtime.py +252 -0
  49. package/bin/test-committee.py +340 -0
  50. package/bin/test-contracts.py +544 -0
  51. package/bin/test-crecimiento.py +164 -0
  52. package/bin/test-demo.py +308 -0
  53. package/bin/test-doctor.py +106 -0
  54. package/bin/test-domains.py +151 -0
  55. package/bin/test-evidencia.py +50 -0
  56. package/bin/test-exit.py +128 -0
  57. package/bin/test-hall-protocol.py +93 -0
  58. package/bin/test-launch.py +169 -0
  59. package/bin/test-live-feed.py +508 -0
  60. package/bin/test-pairing.py +99 -0
  61. package/bin/test-parcels.py +283 -0
  62. package/bin/test-runtime-failures.py +252 -0
  63. package/bin/test-runtime-ui.py +488 -0
  64. package/bin/test-runtime.py +268 -0
  65. package/bin/test-rutas.py +80 -0
  66. package/bin/test-seat.py +1386 -0
  67. package/bin/test-security.py +150 -0
  68. package/bin/test-serve.py +1057 -0
  69. package/bin/test-stress.py +64 -0
  70. package/bin/test-widgets.py +143 -0
  71. package/bin/test-workspace.py +188 -0
  72. package/bin/testlib.py +95 -0
  73. package/bin/tokens.py +14 -0
  74. package/bus/scripts/deploy.sh +60 -0
  75. package/bus/scripts/mint-token.sh +68 -0
  76. package/bus/scripts/setup-dev.sh +79 -0
  77. package/bus/scripts/test-channel.ts +8 -0
  78. package/bus/scripts/test-hub.ts +266 -0
  79. package/bus/scripts/test-local.sh +40 -0
  80. package/bus/scripts/test-queue.ts +132 -0
  81. package/bus/worker/package-lock.json +1971 -0
  82. package/bus/worker/package.json +17 -0
  83. package/bus/worker/src/index.ts +485 -0
  84. package/bus/worker/tsconfig.json +13 -0
  85. package/bus/worker/wrangler.toml +32 -0
  86. package/city/oven/README.md +59 -0
  87. package/city/oven/collect.py +43 -0
  88. package/city/oven/oven.html +4 -0
  89. package/city/scripts/history.py +120 -0
  90. package/city/scripts/seed.py +442 -0
  91. package/city/web/assets/modelos/License.txt +28 -0
  92. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  93. package/city/web/assets/modelos/building-a.glb +0 -0
  94. package/city/web/assets/modelos/building-b.glb +0 -0
  95. package/city/web/assets/modelos/building-c.glb +0 -0
  96. package/city/web/assets/modelos/building-d.glb +0 -0
  97. package/city/web/assets/modelos/building-e.glb +0 -0
  98. package/city/web/assets/modelos/building-f.glb +0 -0
  99. package/city/web/assets/modelos/building-g.glb +0 -0
  100. package/city/web/assets/modelos/building-h.glb +0 -0
  101. package/city/web/assets/sprites/building-a.png +0 -0
  102. package/city/web/assets/sprites/building-b.png +0 -0
  103. package/city/web/assets/sprites/building-c.png +0 -0
  104. package/city/web/assets/sprites/building-d.png +0 -0
  105. package/city/web/assets/sprites/building-e.png +0 -0
  106. package/city/web/assets/sprites/building-f.png +0 -0
  107. package/city/web/assets/sprites/building-g.png +0 -0
  108. package/city/web/assets/sprites/building-h.png +0 -0
  109. package/city/web/assets/sprites/building-i.png +0 -0
  110. package/city/web/assets/sprites/building-j.png +0 -0
  111. package/city/web/assets/sprites/building-k.png +0 -0
  112. package/city/web/assets/sprites/building-l.png +0 -0
  113. package/city/web/assets/sprites/building-m.png +0 -0
  114. package/city/web/assets/sprites/building-n.png +0 -0
  115. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  116. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  117. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  118. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  119. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  120. package/city/web/assets/sprites/catalogo.json +47 -0
  121. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  122. package/city/web/assets/sprites/detail-awning.png +0 -0
  123. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  124. package/city/web/assets/sprites/detail-overhang.png +0 -0
  125. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  126. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  127. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  128. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  129. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  130. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  131. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  132. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  133. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  143. package/city/web/assets/sprites/medidas.json +248 -0
  144. package/city/web/dist/city.js +939 -0
  145. package/city/web/dist/index.html +294 -0
  146. package/city/web/dist/modelos/License.txt +28 -0
  147. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  148. package/city/web/dist/modelos/building-a.glb +0 -0
  149. package/city/web/dist/modelos/building-b.glb +0 -0
  150. package/city/web/dist/modelos/building-c.glb +0 -0
  151. package/city/web/dist/modelos/building-d.glb +0 -0
  152. package/city/web/dist/modelos/building-e.glb +0 -0
  153. package/city/web/dist/modelos/building-f.glb +0 -0
  154. package/city/web/dist/modelos/building-g.glb +0 -0
  155. package/city/web/dist/modelos/building-h.glb +0 -0
  156. package/city/web/dist/sprites/building-a.png +0 -0
  157. package/city/web/dist/sprites/building-b.png +0 -0
  158. package/city/web/dist/sprites/building-c.png +0 -0
  159. package/city/web/dist/sprites/building-d.png +0 -0
  160. package/city/web/dist/sprites/building-e.png +0 -0
  161. package/city/web/dist/sprites/building-f.png +0 -0
  162. package/city/web/dist/sprites/building-g.png +0 -0
  163. package/city/web/dist/sprites/building-h.png +0 -0
  164. package/city/web/dist/sprites/building-i.png +0 -0
  165. package/city/web/dist/sprites/building-j.png +0 -0
  166. package/city/web/dist/sprites/building-k.png +0 -0
  167. package/city/web/dist/sprites/building-l.png +0 -0
  168. package/city/web/dist/sprites/building-m.png +0 -0
  169. package/city/web/dist/sprites/building-n.png +0 -0
  170. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  171. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  172. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  173. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  174. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  175. package/city/web/dist/sprites/catalogo.json +47 -0
  176. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  177. package/city/web/dist/sprites/detail-awning.png +0 -0
  178. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  179. package/city/web/dist/sprites/detail-overhang.png +0 -0
  180. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  181. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  182. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  183. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  184. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  185. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  186. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  187. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  188. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  198. package/city/web/dist/sprites/medidas.json +248 -0
  199. package/city/web/dist-hall/hall.js +264 -0
  200. package/city/web/index.html +294 -0
  201. package/city/web/package.json +18 -0
  202. package/city/web/sella.py +21 -0
  203. package/city/web/src/activity-actors.ts +47 -0
  204. package/city/web/src/activity.ts +172 -0
  205. package/city/web/src/ayuntamiento.ts +580 -0
  206. package/city/web/src/draw.ts +640 -0
  207. package/city/web/src/game-speech.ts +79 -0
  208. package/city/web/src/hall.ts +1867 -0
  209. package/city/web/src/main.ts +2225 -0
  210. package/city/web/src/oven.ts +124 -0
  211. package/city/web/src/people.ts +244 -0
  212. package/city/web/src/presencia.ts +193 -0
  213. package/city/web/src/puertas.ts +180 -0
  214. package/city/web/src/repo-roles.ts +59 -0
  215. package/city/worker/package.json +16 -0
  216. package/city/worker/schema.sql +117 -0
  217. package/city/worker/src/index.ts +702 -0
  218. package/city/worker/src/square.ts +218 -0
  219. package/city/worker/wrangler.toml +58 -0
  220. package/demo/ada.md +50 -0
  221. package/demo/bruno.md +62 -0
  222. package/demo/camila.md +56 -0
  223. package/demo/city/ada.md +40 -0
  224. package/demo/city/city.yml +8 -0
  225. package/demo/city/parcels.yml +15 -0
  226. package/demo/city/roads.json +4 -0
  227. package/demo/city/units.yml +10 -0
  228. package/demo/clinica/city.yml +8 -0
  229. package/demo/clinica/parcels.yml +15 -0
  230. package/demo/clinica/roads.json +4 -0
  231. package/demo/clinica/units.yml +9 -0
  232. package/demo/clinica/vera.md +46 -0
  233. package/demo/dante.md +54 -0
  234. package/demo/despacho/city.yml +8 -0
  235. package/demo/despacho/marta.md +46 -0
  236. package/demo/despacho/parcels.yml +15 -0
  237. package/demo/despacho/roads.json +4 -0
  238. package/demo/despacho/units.yml +9 -0
  239. package/demo/elsa.md +50 -0
  240. package/demo/farid.md +52 -0
  241. package/demo/greta.md +50 -0
  242. package/demo/hugo.md +49 -0
  243. package/demo/iris.md +49 -0
  244. package/demo/jonas.md +51 -0
  245. package/demo/kira.md +51 -0
  246. package/demo/luca.md +49 -0
  247. package/demo/parcels.yml +92 -0
  248. package/demo/seed.py +20 -0
  249. package/demo/show.py +161 -0
  250. package/demo/stories.py +824 -0
  251. package/demo/units.yml +33 -0
  252. package/docs/agents-first.md +99 -0
  253. package/docs/glossary.md +55 -0
  254. package/docs/map-live-layers.md +73 -0
  255. package/docs/security.md +231 -0
  256. package/docs/self-host.md +151 -0
  257. package/docs/testing.md +205 -0
  258. package/package.json +73 -0
  259. package/plugin/.claude-plugin/plugin.json +62 -0
  260. package/plugin/.mcp.json +15 -0
  261. package/plugin/channel/activity-cli.ts +16 -0
  262. package/plugin/channel/adapter-prompts.ts +113 -0
  263. package/plugin/channel/adapter.js +4292 -0
  264. package/plugin/channel/adapter.ts +133 -0
  265. package/plugin/channel/bus.js +20188 -0
  266. package/plugin/channel/bus.ts +131 -0
  267. package/plugin/channel/city-config.ts +125 -0
  268. package/plugin/channel/claude-channel.ts +58 -0
  269. package/plugin/channel/cli-args.ts +75 -0
  270. package/plugin/channel/client.js +4200 -0
  271. package/plugin/channel/client.ts +38 -0
  272. package/plugin/channel/committee/activity.ts +256 -0
  273. package/plugin/channel/committee/collection.ts +154 -0
  274. package/plugin/channel/committee/decision.ts +175 -0
  275. package/plugin/channel/committee/floor.ts +135 -0
  276. package/plugin/channel/committee/guards.ts +30 -0
  277. package/plugin/channel/committee/history.ts +38 -0
  278. package/plugin/channel/committee/render.ts +102 -0
  279. package/plugin/channel/committee/service.ts +72 -0
  280. package/plugin/channel/committee/storage.ts +92 -0
  281. package/plugin/channel/committee/types.ts +163 -0
  282. package/plugin/channel/committee/view.ts +63 -0
  283. package/plugin/channel/committee-cli.ts +109 -0
  284. package/plugin/channel/delivery-metrics.ts +61 -0
  285. package/plugin/channel/delivery-queue.ts +118 -0
  286. package/plugin/channel/hub/activity-controller.ts +72 -0
  287. package/plugin/channel/hub/activity-feed.ts +169 -0
  288. package/plugin/channel/hub/committee-controller.ts +60 -0
  289. package/plugin/channel/hub/connections.ts +48 -0
  290. package/plugin/channel/hub/diagnostics.ts +67 -0
  291. package/plugin/channel/hub/envelope-validity.ts +109 -0
  292. package/plugin/channel/hub/envelopes.ts +66 -0
  293. package/plugin/channel/hub/lifecycle.ts +91 -0
  294. package/plugin/channel/hub/local-roads.ts +89 -0
  295. package/plugin/channel/hub/remote-roads.ts +165 -0
  296. package/plugin/channel/hub/road-controller.ts +99 -0
  297. package/plugin/channel/hub-client.ts +149 -0
  298. package/plugin/channel/local-hub.js +5995 -0
  299. package/plugin/channel/local-hub.ts +349 -0
  300. package/plugin/channel/map-reporter.ts +48 -0
  301. package/plugin/channel/package-lock.json +1731 -0
  302. package/plugin/channel/package.json +19 -0
  303. package/plugin/channel/protocol.ts +98 -0
  304. package/plugin/channel/road-cli.ts +18 -0
  305. package/plugin/channel/run.sh +15 -0
  306. package/plugin/channel/runtime/claude.ts +458 -0
  307. package/plugin/channel/runtime/codex-config.ts +77 -0
  308. package/plugin/channel/runtime/codex.ts +669 -0
  309. package/plugin/channel/runtime/command.ts +62 -0
  310. package/plugin/channel/runtime/factory.ts +15 -0
  311. package/plugin/channel/runtime/json-rpc.ts +154 -0
  312. package/plugin/channel/runtime/kimi.ts +277 -0
  313. package/plugin/channel/runtime/opencode.ts +248 -0
  314. package/plugin/channel/runtime/process.ts +84 -0
  315. package/plugin/channel/runtime/types.ts +45 -0
  316. package/plugin/channel/runtime-files.ts +73 -0
  317. package/plugin/channel/runtime-gateway.js +6396 -0
  318. package/plugin/channel/runtime-gateway.ts +358 -0
  319. package/plugin/channel/runtime-metrics.ts +52 -0
  320. package/plugin/channel/runtime-subscription.ts +180 -0
  321. package/plugin/channel/terminal-delivery.ts +131 -0
  322. package/plugin/channel/untrusted.ts +60 -0
  323. package/plugin/commands/committee.md +31 -0
  324. package/plugin/commands/exit.md +32 -0
  325. package/plugin/commands/goals.md +20 -0
  326. package/plugin/commands/join.md +18 -0
  327. package/plugin/commands/notice.md +20 -0
  328. package/plugin/commands/propose.md +19 -0
  329. package/plugin/commands/round.md +17 -0
  330. package/plugin/commands/session.md +22 -0
  331. package/plugin/commands/settings.md +25 -0
  332. package/plugin/commands/setup.md +21 -0
  333. package/plugin/commands/team.md +23 -0
  334. package/plugin/domains/custom.md +27 -0
  335. package/plugin/domains/finance.md +31 -0
  336. package/plugin/domains/healthcare.md +34 -0
  337. package/plugin/domains/legal.md +31 -0
  338. package/plugin/domains/marketing.md +33 -0
  339. package/plugin/domains/operations.md +31 -0
  340. package/plugin/domains/research.md +32 -0
  341. package/plugin/domains/sales.md +30 -0
  342. package/plugin/domains/software.md +37 -0
  343. package/plugin/hooks/activity.sh +6 -0
  344. package/plugin/hooks/digging.sh +68 -0
  345. package/plugin/hooks/growth.sh +42 -0
  346. package/plugin/hooks/hooks.json +106 -0
  347. package/plugin/hooks/notice-on-pr.sh +35 -0
  348. package/plugin/hooks/notice-on-stop.sh +80 -0
  349. package/plugin/hooks/notice-pending.sh +58 -0
  350. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  351. package/plugin/hooks/tokens.sh +47 -0
  352. package/plugin/roles/examples/account-executive.md +25 -0
  353. package/plugin/roles/examples/ai-manager.md +34 -0
  354. package/plugin/roles/examples/associate.md +24 -0
  355. package/plugin/roles/examples/brand-lead.md +20 -0
  356. package/plugin/roles/examples/cfo.md +20 -0
  357. package/plugin/roles/examples/city-lead.md +25 -0
  358. package/plugin/roles/examples/clinical-director.md +26 -0
  359. package/plugin/roles/examples/clinical-ops.md +25 -0
  360. package/plugin/roles/examples/clinician.md +25 -0
  361. package/plugin/roles/examples/compliance.md +27 -0
  362. package/plugin/roles/examples/content.md +24 -0
  363. package/plugin/roles/examples/controller.md +27 -0
  364. package/plugin/roles/examples/cpto.md +41 -0
  365. package/plugin/roles/examples/customer-success.md +25 -0
  366. package/plugin/roles/examples/data-engineer.md +37 -0
  367. package/plugin/roles/examples/data.md +60 -0
  368. package/plugin/roles/examples/dev.md +37 -0
  369. package/plugin/roles/examples/devops.md +39 -0
  370. package/plugin/roles/examples/enablement.md +24 -0
  371. package/plugin/roles/examples/ethics.md +24 -0
  372. package/plugin/roles/examples/fin-analytics.md +27 -0
  373. package/plugin/roles/examples/health-compliance.md +25 -0
  374. package/plugin/roles/examples/health-data.md +26 -0
  375. package/plugin/roles/examples/knowledge.md +25 -0
  376. package/plugin/roles/examples/lifecycle.md +26 -0
  377. package/plugin/roles/examples/llm-engineer.md +34 -0
  378. package/plugin/roles/examples/managing-partner.md +20 -0
  379. package/plugin/roles/examples/methods.md +23 -0
  380. package/plugin/roles/examples/operations-lead.md +23 -0
  381. package/plugin/roles/examples/ops.md +24 -0
  382. package/plugin/roles/examples/patient-safety.md +26 -0
  383. package/plugin/roles/examples/performance.md +27 -0
  384. package/plugin/roles/examples/po.md +36 -0
  385. package/plugin/roles/examples/process-owner.md +23 -0
  386. package/plugin/roles/examples/product-design.md +39 -0
  387. package/plugin/roles/examples/program-manager.md +24 -0
  388. package/plugin/roles/examples/quality.md +25 -0
  389. package/plugin/roles/examples/research-director.md +24 -0
  390. package/plugin/roles/examples/research-ops.md +25 -0
  391. package/plugin/roles/examples/researcher.md +25 -0
  392. package/plugin/roles/examples/revenue-lead.md +25 -0
  393. package/plugin/roles/examples/revops.md +26 -0
  394. package/plugin/roles/examples/seo.md +27 -0
  395. package/plugin/roles/examples/specialist.md +24 -0
  396. package/plugin/scripts/admision.py +165 -0
  397. package/plugin/scripts/apaga.py +232 -0
  398. package/plugin/scripts/avatar.py +211 -0
  399. package/plugin/scripts/broker.py +531 -0
  400. package/plugin/scripts/cage.py +252 -0
  401. package/plugin/scripts/capabilities.py +197 -0
  402. package/plugin/scripts/card.py +312 -0
  403. package/plugin/scripts/cities.py +520 -0
  404. package/plugin/scripts/city-env.sh +90 -0
  405. package/plugin/scripts/city-runtime.sh +87 -0
  406. package/plugin/scripts/city-session.sh +526 -0
  407. package/plugin/scripts/city_env.py +67 -0
  408. package/plugin/scripts/crecimiento.py +140 -0
  409. package/plugin/scripts/deliberations.py +48 -0
  410. package/plugin/scripts/doctor.py +172 -0
  411. package/plugin/scripts/domains.py +201 -0
  412. package/plugin/scripts/evidencia.py +64 -0
  413. package/plugin/scripts/find-repos.sh +111 -0
  414. package/plugin/scripts/gh.py +159 -0
  415. package/plugin/scripts/hall_protocol.py +147 -0
  416. package/plugin/scripts/hook_activity.py +139 -0
  417. package/plugin/scripts/launch.py +94 -0
  418. package/plugin/scripts/logs.py +98 -0
  419. package/plugin/scripts/pairing.py +185 -0
  420. package/plugin/scripts/parcels.py +132 -0
  421. package/plugin/scripts/read-card.py +51 -0
  422. package/plugin/scripts/report.py +201 -0
  423. package/plugin/scripts/reset.py +180 -0
  424. package/plugin/scripts/roads.py +230 -0
  425. package/plugin/scripts/roles.py +274 -0
  426. package/plugin/scripts/runtime_log.py +69 -0
  427. package/plugin/scripts/runtime_processes.py +132 -0
  428. package/plugin/scripts/rutas.py +83 -0
  429. package/plugin/scripts/seat.py +1349 -0
  430. package/plugin/scripts/tokens.py +194 -0
  431. package/plugin/scripts/trust-repos.py +59 -0
  432. package/plugin/scripts/ui.py +280 -0
  433. package/plugin/scripts/units.py +80 -0
  434. package/plugin/scripts/workspace.py +364 -0
  435. package/plugin/skills/city/SKILL.md +191 -0
  436. package/templates/blank.md +39 -0
  437. package/templates/finance.md +44 -0
  438. package/templates/legal.md +45 -0
  439. package/templates/marketing.md +48 -0
  440. package/templates/product.md +45 -0
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Rebuild the city's history from the merge date of every pull request.
3
+
4
+ You do not have to wait for time to pass to have a past: a merged PR carries its
5
+ date, so a house's capital in March 2024 is however many PRs had landed by March
6
+ 2024. That is what the replay plays.
7
+
8
+ Run from a laptop with that person's own `gh`, so the token never leaves the
9
+ machine and never ends up inside a Worker.
10
+
11
+ ./city/scripts/history.py writes /tmp/history.sql
12
+ ./city/scripts/history.py --few only 8 repos, to try it
13
+ ./city/scripts/history.py --org acme the organisation to ask about
14
+ ./city/scripts/history.py --out h.sql somewhere else
15
+
16
+ Then load it:
17
+ npx wrangler@4 d1 execute city --remote --file /tmp/history.sql
18
+ """
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ from collections import defaultdict
24
+
25
+ RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
26
+
27
+
28
+ def org_por_defecto():
29
+ """The organisation, from the settings or from `gh`'s own idea of who you are."""
30
+ for v in (os.environ.get('AGENTS_CITY_ORG'), os.environ.get('GITHUB_ORG')):
31
+ if v:
32
+ return v
33
+ r = subprocess.run(['gh', 'api', 'user', '--jq', '.login'],
34
+ capture_output=True, text=True)
35
+ return r.stdout.strip() if r.returncode == 0 else ''
36
+
37
+
38
+ def parcelas_por_repo():
39
+ """repo -> [parcel ids], read from the same place the seeder reads."""
40
+ salida = subprocess.run([os.path.join(RAIZ, 'city', 'scripts', 'seed.py')],
41
+ capture_output=True, text=True).stdout
42
+ m = defaultdict(list)
43
+ for linea in salida.splitlines():
44
+ g = re.match(r"INSERT INTO parcela \(id,repo,ruta,unidad,nombre,dueno\) "
45
+ r"VALUES \('(.+?)','(.+?)',", linea)
46
+ if g:
47
+ m[g.group(2)].append(g.group(1))
48
+ return m
49
+
50
+
51
+ def dias_de(org, repo):
52
+ """How many PRs were merged each DAY in this repo.
53
+
54
+ Per day and not per month because a month is too coarse to watch anything
55
+ grow: the interesting film is the last ninety days.
56
+ """
57
+ try:
58
+ r = subprocess.run(
59
+ ['gh', 'api', f'repos/{org}/{repo}/pulls?state=closed&per_page=100',
60
+ '--paginate', '--jq', '.[] | select(.merged_at) | .merged_at'],
61
+ capture_output=True, text=True, timeout=180)
62
+ except (subprocess.TimeoutExpired, FileNotFoundError):
63
+ return {}
64
+ cuenta = defaultdict(int)
65
+ for linea in r.stdout.splitlines():
66
+ if len(linea) >= 10:
67
+ cuenta[linea[:10]] += 1
68
+ return cuenta
69
+
70
+
71
+ def main():
72
+ org = ''
73
+ salida = '/tmp/history.sql'
74
+ args = sys.argv[1:]
75
+ for i, a in enumerate(args):
76
+ if a == '--org' and i + 1 < len(args):
77
+ org = args[i + 1]
78
+ if a == '--out' and i + 1 < len(args):
79
+ salida = args[i + 1]
80
+ org = org or org_por_defecto()
81
+ if not org:
82
+ print('I need a GitHub organisation: --org <name>, or AGENTS_CITY_ORG.',
83
+ file=sys.stderr)
84
+ return 1
85
+
86
+ por_repo = parcelas_por_repo()
87
+ repos = sorted(por_repo)
88
+ if not repos:
89
+ print('No parcels. Is AGENTS_CITY_DATA pointing at your data repo?',
90
+ file=sys.stderr)
91
+ return 1
92
+ if '--few' in args or '--pocos' in args:
93
+ repos = repos[:8]
94
+
95
+ filas = []
96
+ for i, repo in enumerate(repos, 1):
97
+ dias = dias_de(org, repo)
98
+ # Every parcel of a repo inherits the repo's dates: counting per path
99
+ # needs one API call per PR. The map says so where it shows the number,
100
+ # rather than presenting a whole-repo figure as if it were the parcel's.
101
+ for parcela in por_repo[repo]:
102
+ for dia, n in dias.items():
103
+ filas.append((parcela, dia, n))
104
+ print(f' [{i}/{len(repos)}] {repo}: {sum(dias.values())} PRs '
105
+ f'across {len(dias)} days', file=sys.stderr, flush=True)
106
+
107
+ def q(s):
108
+ return "'" + str(s).replace("'", "''") + "'"
109
+ with open(salida, 'w') as f:
110
+ f.write('DELETE FROM hito;\n')
111
+ for parcela, dia, n in filas:
112
+ f.write('INSERT INTO hito (parcela_id,dia,n) VALUES ('
113
+ f'{q(parcela)},{q(dia)},{n}) '
114
+ 'ON CONFLICT(parcela_id,dia) DO UPDATE SET n=excluded.n;\n')
115
+ print(f'{len(filas)} rows -> {salida}', file=sys.stderr)
116
+ return 0
117
+
118
+
119
+ if __name__ == '__main__':
120
+ sys.exit(main())
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env python3
2
+ """Load your city into D1: units, parcels, people — and optionally a history.
3
+
4
+ Reads the data repo, which is the same thing the plugin reads: units.yml,
5
+ parcels.yml, roles/ and one card per person. Nothing about any particular
6
+ organisation lives in here.
7
+
8
+ ./city/scripts/seed.py print the SQL, change nothing
9
+ ./city/scripts/seed.py --local load it into the local D1
10
+ ./city/scripts/seed.py --remote load it into the deployed one
11
+ ./city/scripts/seed.py --data ../mine read a different data repo
12
+ ./city/scripts/seed.py --fake-history invent a plausible past (demo only)
13
+
14
+ Where the data comes from, in order: --data, $AGENTS_CITY_DATA, ~/agents-city-data.
15
+ """
16
+ import argparse
17
+ import shutil
18
+ import glob
19
+ import os
20
+ import random
21
+ import re
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ from datetime import date, timedelta
26
+
27
+ # The repo root, three levels up from city/scripts/seed.py. Worth being explicit:
28
+ # computing it wrong sends wrangler looking for city/city/worker.
29
+ RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
+ # The parcels.yml reader lives in plugin/scripts because that directory is what
31
+ # gets installed on everybody's machine. The plugin cannot import from city/, so
32
+ # this is the only direction that works — and there is one reader, not two.
33
+ sys.path.insert(0, os.path.join(RAIZ, 'plugin', 'scripts'))
34
+ import parcels # noqa: E402
35
+
36
+ import roles # noqa: E402
37
+ import units # noqa: E402
38
+ oficio = roles.oficio
39
+
40
+
41
+
42
+ def lanzador():
43
+ """How to run wrangler on this machine.
44
+
45
+ Not a detail: hardcoding one runner is what made the first from-scratch run
46
+ fail on a machine that had npm but not bun. npx ships with npm, so it is the
47
+ one almost everybody already has; bunx is faster when it is there.
48
+ """
49
+ for cand in (['bunx'], ['bun', 'x'], ['npx', '--yes']):
50
+ if shutil.which(cand[0]):
51
+ return cand
52
+ raise SystemExit(' I need npx (comes with Node) or bun. Install Node from nodejs.org.')
53
+
54
+ def q(s):
55
+ return "'" + str(s).replace("'", "''") + "'"
56
+
57
+
58
+
59
+
60
+ def unidades(datos):
61
+ """The districts, read by units.py like everywhere else. The column widths are
62
+ the map's own idea and stay here."""
63
+ fuera = units.lee(f'{datos}/units.yml')
64
+ for u in fuera:
65
+ u['cols'] = 16 if u['id'] == 'none' else 7 if u['id'] == 'lab' else 3
66
+ return fuera
67
+
68
+
69
+ def parcelas(datos):
70
+ """Declared parcels, and which repos are research that does not ship yet.
71
+
72
+ The format is read by plugin/scripts/parcels.py — the same reader the growth
73
+ reporter uses. It used to be read here by a second copy of the same walk and the
74
+ same regex, and the two drifted: this one handled the `lab:` section and warned
75
+ about lines it could not parse, the reporter's did neither and looked for the
76
+ folder under a key that does not exist. Two readers of one format is a bug
77
+ waiting for somebody to edit one of them.
78
+ """
79
+ declaradas, lab, raras = parcels.lee(f'{datos}/parcels.yml')
80
+ if raras:
81
+ print(parcels.aviso(raras), file=sys.stderr)
82
+ return ([(p['repo'], p['ruta'], p['unidad'], p['nombre']) for p in declaradas],
83
+ lab)
84
+
85
+
86
+ def objetivos(fichero):
87
+ """The goals written on one card.
88
+
89
+ Parsed from the card rather than from a second file, because the card is
90
+ where they are agreed and reviewed. A goal with no measure is still read and
91
+ still shown — saying "this one has no measure yet" is more useful than
92
+ pretending it does not exist.
93
+ """
94
+ try:
95
+ texto = open(fichero, encoding='utf-8').read()
96
+ except OSError:
97
+ return []
98
+ # Only above the format comment: the template below it is not somebody's goal.
99
+ texto = texto.split('<!-- Format', 1)[0]
100
+ fuera = []
101
+ for i, bloque in enumerate(re.split(r'^### O\d+ — ', texto, flags=re.M)[1:], 1):
102
+ lineas = bloque.strip().splitlines()
103
+ titulo = lineas[0].strip() if lineas else ''
104
+ if not titulo or titulo.startswith('<'):
105
+ continue
106
+ def campo(k, _b=bloque):
107
+ m = re.search(rf'^- \*\*{k}\*\*:\s*(.+)$', _b, re.M)
108
+ return (m.group(1).strip().strip('`') if m else '')
109
+ fuera.append({
110
+ 'n': i, 'titulo': titulo,
111
+ 'como': campo('How it is measured'), 'medida': campo('Measure'),
112
+ 'partida': campo('Baseline'), 'meta': campo('Target'),
113
+ 'cuando': campo('By when'), 'estado': campo('State') or 'not started',
114
+ })
115
+ return fuera
116
+
117
+
118
+ def gente(datos):
119
+ """One card per person. They live at the root of the data repo — that is what
120
+ the wizard writes and what the plugin reads — but a `team/` subfolder is
121
+ accepted too, because plenty of people will reach for one."""
122
+ fuera = []
123
+ for f in sorted(glob.glob(f'{datos}/*.md') + glob.glob(f'{datos}/team/*.md')):
124
+ t = open(f).read()
125
+ def g(k, d='', _t=t):
126
+ m = re.search(rf'^{k}:\s*(.+)$', _t, re.M)
127
+ return m.group(1).strip() if m else d
128
+
129
+ def lista(k, _t=t):
130
+ m = re.search(rf'^{k}:\s*\[(.*?)\]', _t, re.M)
131
+ return [x.strip() for x in (m.group(1) if m else '').split(',') if x.strip()]
132
+ u = g('user')
133
+ if u:
134
+ # The path travels with the person: the goals are read from the same
135
+ # card, and hunting for it again by name would be a second guess about
136
+ # where cards live.
137
+ fuera.append({'user': u, 'name': g('name', u), 'role': g('role', 'dev'),
138
+ 'agent': g('agent', f'{u}/dev'), 'repos': lista('repos'),
139
+ 'ficha': f})
140
+ return fuera
141
+
142
+
143
+ def dueno(repo, personas):
144
+ """Who answers for a repo. A dev wins over a cross-cutting role: the map asks
145
+ 'who knows this code', not 'who has an opinion about it'."""
146
+ for p in personas:
147
+ if p['role'] == 'dev' and repo in p['repos']:
148
+ return p['user']
149
+ for p in personas:
150
+ if repo in p['repos']:
151
+ return p['user']
152
+ return None
153
+
154
+
155
+ def historia_inventada(ids, semilla=1312):
156
+ """A plausible past, for demos and for trying the replay before you have one.
157
+
158
+ Invented numbers, but not an invented shape: growth is lumpy, each parcel
159
+ starts on a different day, most are quiet most of the time, and a few carry
160
+ the bulk. A flat random history looks obviously fake the moment you play it.
161
+ """
162
+ random.seed(semilla)
163
+ hoy = date.today()
164
+ inicio = hoy - timedelta(days=3 * 365)
165
+ filas = []
166
+ for pid in sorted(ids):
167
+ # A long tail and a couple of monsters, which is what a real portfolio
168
+ # looks like: most parcels land a handful of changes a month and two or
169
+ # three carry the company. A flat distribution made every house the same
170
+ # height, and a city where everything is the same height says nothing.
171
+ ritmo = random.choice([0.01, 0.02, 0.03, 0.05, 0.08, 0.12, 0.2, 0.45, 0.9])
172
+ d = inicio + timedelta(days=random.randint(0, 1000))
173
+ while d <= hoy:
174
+ if d.weekday() < 5 and random.random() < min(0.85, ritmo * 1.6):
175
+ filas.append((pid, d.isoformat(), max(1, int(random.gauss(ritmo * 3, ritmo * 2)))))
176
+ d += timedelta(days=1)
177
+ return filas
178
+
179
+
180
+ ETIQUETAS = ['ux', 'data', 'security', 'cost', 'product', 'llm']
181
+
182
+ # What a notice actually says, per property. Invented, but the shape is the point:
183
+ # a notice names the thing that moved, not "something changed".
184
+ TEXTOS = {
185
+ 'ux': ['the empty state now shows a spinner in {p}', 'the primary button moved in {p}'],
186
+ 'data': ['the event schema changed in {p}', 'a series was renamed in {p}'],
187
+ 'security': ['a new public endpoint in {p}', 'a dependency with a CVE in {p}'],
188
+ 'cost': ['the batch size doubled in {p}', 'a nightly job now runs hourly in {p}'],
189
+ 'product': ['a flag defaults to on in {p}', 'the onboarding order changed in {p}'],
190
+ 'llm': ['the system prompt changed in {p}', 'the model was swapped in {p}'],
191
+ }
192
+
193
+
194
+ def avisos_inventados(ids, personas, hitos, semilla=99):
195
+ """Notices with dates, so the replay has letters flying and not just floors.
196
+
197
+ Derived from the history rather than sprinkled at random: a notice happens on
198
+ a day when something actually landed, and it goes from the parcel that changed
199
+ to a person who is not its owner — which is the whole point of a notice.
200
+ """
201
+ if not hitos or not personas:
202
+ return []
203
+ random.seed(semilla)
204
+ dueno_de = {}
205
+ fuera, vistos = [], set()
206
+ for pid, dia, n in hitos:
207
+ # Roughly one notice per twelve landings: often enough to see, rare enough
208
+ # that a day with one means something.
209
+ if random.random() > min(0.35, n / 12):
210
+ continue
211
+ emisor = dueno_de.get(pid) or random.choice(personas)['user']
212
+ dueno_de[pid] = emisor
213
+ otros = [p for p in personas if p['user'] != emisor]
214
+ if not otros:
215
+ continue
216
+ receptor = random.choice(otros)
217
+ etq = random.choice(ETIQUETAS)
218
+ # One notice per (day, sender, recipient, parcel, property): the same
219
+ # person telling the same person the same thing twice on the same day is
220
+ # noise, and on screen it reads as a bug in the ticker.
221
+ clave = (dia, emisor, receptor['user'], pid, etq)
222
+ if clave in vistos:
223
+ continue
224
+ vistos.add(clave)
225
+ texto = random.choice(TEXTOS[etq]).format(p=pid)
226
+ fuera.append((dia, emisor, receptor['user'], etq, texto))
227
+ return fuera
228
+
229
+
230
+ MODELOS = ['claude-opus-5', 'claude-sonnet-5', 'claude-haiku-4-5']
231
+
232
+
233
+ def gasto_inventado(hitos, personas, semilla=404):
234
+ """Token spend for the demo, so the global counter is not an empty box.
235
+
236
+ Tied to the invented history rather than random: the days the city built are
237
+ the days it spent. Cache reads dominate the numbers, which is what a real
238
+ report looks like — a long session re-reads its context far more than it
239
+ writes anything new, and a counter that hides that misleads about the cost.
240
+ """
241
+ if not personas:
242
+ return []
243
+ random.seed(semilla)
244
+ desde = date.today() - timedelta(days=45)
245
+ por_dia = {}
246
+ for _, dia, n in hitos:
247
+ if dia >= desde.isoformat():
248
+ por_dia[dia] = por_dia.get(dia, 0) + n
249
+ filas = []
250
+ for dia in sorted(por_dia) or [date.today().isoformat()]:
251
+ trabajando = random.sample(personas, k=min(len(personas),
252
+ max(1, min(len(personas),
253
+ por_dia.get(dia, 1) + 1))))
254
+ for p in trabajando:
255
+ for modelo in random.sample(MODELOS, k=random.randint(1, 2)):
256
+ salida = random.randint(4_000, 60_000)
257
+ filas.append((dia, p['user'], modelo,
258
+ random.randint(2_000, 30_000), salida,
259
+ salida * random.randint(20, 90),
260
+ salida * random.randint(1, 6)))
261
+ return filas
262
+
263
+
264
+ def sql_personas(sql, personas, datos):
265
+ """People and their goals, straight off the cards."""
266
+ for p in personas:
267
+ for o in objetivos(p['ficha']):
268
+ sql.append('INSERT INTO objetivo '
269
+ '(usuario,n,titulo,como,medida,partida,meta,cuando,estado) '
270
+ f'VALUES ({q(p["user"])},{o["n"]},{q(o["titulo"])},{q(o["como"])},'
271
+ f'{q(o["medida"])},{q(o["partida"])},{q(o["meta"])},{q(o["cuando"])},'
272
+ f'{q(o["estado"])}) ON CONFLICT(usuario,n) DO UPDATE SET '
273
+ 'titulo=excluded.titulo, como=excluded.como, medida=excluded.medida, '
274
+ 'partida=excluded.partida, meta=excluded.meta, cuando=excluded.cuando, '
275
+ 'estado=excluded.estado;')
276
+ sql.append('INSERT INTO persona (usuario,nombre,rol,oficio,agente) VALUES ('
277
+ f'{q(p["user"])},{q(p["name"])},{q(p["role"])},'
278
+ f'{q(oficio(p["role"], datos))},{q(p["agent"])}) '
279
+ 'ON CONFLICT(usuario) DO UPDATE SET nombre=excluded.nombre, rol=excluded.rol, '
280
+ 'oficio=excluded.oficio, agente=excluded.agente;')
281
+
282
+
283
+ def sql_casas(sql, ids, acumulado, hitos):
284
+ """Houses start empty unless there is a history: the cron fills them from the
285
+ real repos, and an invented number in a real city is worse than a gap."""
286
+ hoy = date.today().isoformat()
287
+ if not hitos:
288
+ return
289
+ random.seed(7)
290
+ for pid in ids:
291
+ cap = acumulado.get(pid, 0)
292
+ abiertos = random.randint(0, 4) if cap else 0
293
+ sql.append('INSERT INTO casa (parcela_id,pisos,ladrillos,andamios,andamio_viejo,'
294
+ f'grieta,actividad30,actualizado) VALUES ({q(pid)},{cap},'
295
+ f'{random.randint(0, 9)},{abiertos},{random.randint(0, abiertos)},'
296
+ f'{1 if random.random() < 0.06 else 0},'
297
+ f'{random.randint(0, 40) if cap else 0},{q(hoy)}) '
298
+ 'ON CONFLICT(parcela_id) DO UPDATE SET pisos=excluded.pisos, '
299
+ 'ladrillos=excluded.ladrillos, andamios=excluded.andamios, '
300
+ 'andamio_viejo=excluded.andamio_viejo, grieta=excluded.grieta, '
301
+ 'actividad30=excluded.actividad30, actualizado=excluded.actualizado;')
302
+
303
+
304
+ def main():
305
+ ap = argparse.ArgumentParser()
306
+ ap.add_argument('--data', default=os.environ.get('AGENTS_CITY_DATA',
307
+ os.path.expanduser('~/agents-city-data')))
308
+ ap.add_argument('--local', action='store_true')
309
+ # Where the local D1 lives. bin/demo and bin/city point at different folders so
310
+ # the demo's live state never shows up on your own bus, and the seeder has to
311
+ # write into the same one the worker will read — otherwise it loads a city
312
+ # nobody serves.
313
+ ap.add_argument('--persist-to', default=os.environ.get('CITY_PERSIST', ''))
314
+ ap.add_argument('--remote', action='store_true')
315
+ ap.add_argument('--fake-history', action='store_true')
316
+ a = ap.parse_args()
317
+ datos = os.path.expanduser(a.data)
318
+
319
+ if not os.path.isdir(datos):
320
+ print(f'No data repo at {datos}.\nRun ./bin/setup.py first, or pass --data.',
321
+ file=sys.stderr)
322
+ return 1
323
+
324
+ unis, personas = unidades(datos), gente(datos)
325
+ declaradas, lab = parcelas(datos)
326
+ ya = {r for r, *_ in declaradas}
327
+ todos = sorted({r for p in personas for r in p['repos']} | ya | lab)
328
+
329
+ filas = list(declaradas)
330
+ for r in todos:
331
+ if r in ya:
332
+ continue
333
+ filas.append((r, '', 'lab' if r in lab else 'none', r))
334
+
335
+ sql, ids = [], []
336
+ for u in unis:
337
+ sql.append('INSERT INTO unidad (id,nombre,color,orden,nota,cols) VALUES ('
338
+ f'{q(u["id"])},{q(u["name"])},{q(u["color"])},{u["order"]},'
339
+ f'{q(u["note"]) if u["note"] else "NULL"},{u["cols"]}) '
340
+ 'ON CONFLICT(id) DO UPDATE SET nombre=excluded.nombre, color=excluded.color, '
341
+ 'orden=excluded.orden, nota=excluded.nota, cols=excluded.cols;')
342
+
343
+ sql_personas(sql, personas, datos)
344
+
345
+ for repo, ruta, unidad, nombre in filas:
346
+ # Shared, not repeated: the reporter pushes growth keyed on this id and
347
+ # the map stores rows keyed on it. If the two ever drift, growth lands on
348
+ # nothing and neither side notices.
349
+ pid = parcels.identidad(repo, ruta)
350
+ ids.append(pid)
351
+ d = dueno(repo, personas)
352
+ sql.append('INSERT INTO parcela (id,repo,ruta,unidad,nombre,dueno) VALUES ('
353
+ f'{q(pid)},{q(repo)},{q(ruta)},{q(unidad)},{q(nombre)},{q(d) if d else "NULL"}) '
354
+ 'ON CONFLICT(id) DO UPDATE SET repo=excluded.repo, ruta=excluded.ruta, '
355
+ 'unidad=excluded.unidad, nombre=excluded.nombre, dueno=excluded.dueno;')
356
+
357
+ hitos = historia_inventada(ids) if a.fake_history else []
358
+ acumulado = {}
359
+ for pid, dia, n in hitos:
360
+ acumulado[pid] = acumulado.get(pid, 0) + n
361
+ sql.append(f'INSERT INTO hito (parcela_id,dia,n) VALUES ({q(pid)},{q(dia)},{n}) '
362
+ 'ON CONFLICT(parcela_id,dia) DO UPDATE SET n=excluded.n;')
363
+
364
+ sql_casas(sql, ids, acumulado, hitos)
365
+
366
+ # Token spend. Stored per person because that is how the reports arrive and
367
+ # the only way to deduplicate them; shown as one global number and never as
368
+ # a ranking.
369
+ gastos = gasto_inventado(hitos, personas) if a.fake_history else []
370
+ for dia, usuario, modelo, ent, sal, cr, cw in gastos:
371
+ sql.append('INSERT INTO gasto (dia,usuario,modelo,entrada,salida,cache_r,cache_w) VALUES ('
372
+ f'{q(dia)},{q(usuario)},{q(modelo)},{ent},{sal},{cr},{cw}) '
373
+ 'ON CONFLICT(dia,usuario,modelo) DO UPDATE SET entrada=excluded.entrada, '
374
+ 'salida=excluded.salida, cache_r=excluded.cache_r, cache_w=excluded.cache_w;')
375
+
376
+ # Notices, with their day, so the replay can fly them.
377
+ #
378
+ # Cleared first when the history is invented: `evento` has an autoincrementing
379
+ # id and no natural key, so re-seeding a demo used to add a second copy of
380
+ # every notice — and a third, and a fourth. Everything else here upserts and
381
+ # is therefore safe to re-run; this is the one table that was not.
382
+ if a.fake_history:
383
+ sql.append("DELETE FROM evento WHERE tipo='notice';")
384
+ avisos = avisos_inventados(ids, personas, hitos) if a.fake_history else []
385
+ for dia, de, para, etq, texto in avisos:
386
+ sql.append('INSERT INTO evento (ts,tipo,origen,destino,etiqueta,texto) VALUES ('
387
+ f"{q(dia + 'T12:00:00Z')},'notice',{q(de)},{q(para)},{q(etq)},"
388
+ f'{q(texto)});')
389
+
390
+ # The data repo is the source of truth, so anything no longer in it goes.
391
+ #
392
+ # Without this, renaming a unit left its old district on the map for ever —
393
+ # an empty platform with a banner and nothing on it — and a parcel that was
394
+ # split differently left a house standing on the old plot. A seeder that only
395
+ # ever inserts cannot express "this is gone", and "this is gone" happens
396
+ # every time an org changes shape.
397
+ lista_u = ','.join(q(u['id']) for u in unis) or "''"
398
+ lista_p = ','.join(q(i) for i in ids) or "''"
399
+ sql.append(f'DELETE FROM unidad WHERE id NOT IN ({lista_u});')
400
+ sql.append(f'DELETE FROM casa WHERE parcela_id NOT IN ({lista_p});')
401
+ sql.append(f'DELETE FROM hito WHERE parcela_id NOT IN ({lista_p});')
402
+ sql.append(f'DELETE FROM parcela WHERE id NOT IN ({lista_p});')
403
+ # People too: someone who left the company should not keep a light on the map.
404
+ lista_g = ','.join(q(p['user']) for p in personas) or "''"
405
+ sql.append(f'DELETE FROM persona WHERE usuario NOT IN ({lista_g});')
406
+ # And their spend with them. Without this, seeding a brand new city over a
407
+ # database that once held another one shows the other one's token counter —
408
+ # which is how this was found: a fresh marketing city claiming 3.5M tokens
409
+ # spent today by thirteen people who were not in it.
410
+ sql.append(f'DELETE FROM gasto WHERE usuario NOT IN ({lista_g});')
411
+ sql.append(f'DELETE FROM objetivo WHERE usuario NOT IN ({lista_g});')
412
+
413
+ salida = '\n'.join(sql)
414
+ resumen = (f'{len(unis)} units, {len(personas)} people, {len(ids)} parcels'
415
+ + (f', {len(hitos)} history rows' if hitos else '')
416
+ + (f', {len(avisos)} notices' if avisos else '')
417
+ + (f', {len(gastos)} spend rows' if gastos else ''))
418
+ if a.local or a.remote:
419
+ modo = '--local' if a.local else '--remote'
420
+ # A unique file per run, not a fixed /tmp name: two cities seeding at
421
+ # once (the multi-domain demos do exactly this) used to overwrite each
422
+ # other's SQL mid-flight and load somebody else's city.
423
+ descriptor, ruta = tempfile.mkstemp(prefix='agents-city-seed-', suffix='.sql')
424
+ try:
425
+ with os.fdopen(descriptor, 'w') as archivo:
426
+ archivo.write(salida)
427
+ orden = [*lanzador(), 'wrangler@4', 'd1', 'execute', 'city', modo]
428
+ if a.local and a.persist_to:
429
+ orden += ['--persist-to', os.path.abspath(os.path.expanduser(a.persist_to))]
430
+ subprocess.run(orden + ['--file', ruta, '-y'],
431
+ cwd=f'{RAIZ}/city/worker', check=True)
432
+ finally:
433
+ os.unlink(ruta)
434
+ print(f'loaded: {resumen}')
435
+ else:
436
+ print(salida)
437
+ print(f'-- {resumen}', file=sys.stderr)
438
+ return 0
439
+
440
+
441
+ if __name__ == '__main__':
442
+ sys.exit(main())
@@ -0,0 +1,28 @@
1
+
2
+
3
+ City Kit Commercial (2.1)
4
+
5
+ Created/distributed by Kenney (www.kenney.nl)
6
+ Creation date: 21-07-2025 18:16
7
+
8
+ ------------------------------
9
+
10
+ License: (Creative Commons Zero, CC0)
11
+ http://creativecommons.org/publicdomain/zero/1.0/
12
+
13
+ You can use this content for personal, educational, and commercial purposes.
14
+
15
+ Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
16
+
17
+ ------------------------------
18
+
19
+ • Website : www.kenney.nl
20
+ • Donate : www.kenney.nl/donate
21
+
22
+ • Patreon : patreon.com/kenney
23
+
24
+ Follow on social media for updates:
25
+
26
+ • Twitter: twitter.com/KenneyNL
27
+ • BlueSky: kenney.bsky.social
28
+ • Instagram: instagram.com/kenney_nl
@@ -0,0 +1,3 @@
1
+ Edificios: Kenney "City Kit (Commercial)" 2.1 — https://kenney.nl/assets/city-kit-commercial
2
+ Licencia CC0 (dominio público, sin atribución obligatoria). License.txt va al lado.
3
+ Sólo se han traído 8 de los 41 modelos: pesan ~1 MB y sobran para la prueba.