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,64 @@
1
+ #!/usr/bin/env python3
2
+ """A 40-agent Claude/Codex city survives fan-out and hub replacement."""
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ AQUI = os.path.dirname(os.path.abspath(__file__))
9
+ RAIZ = os.path.dirname(AQUI)
10
+ sys.path.insert(0, AQUI)
11
+ from testlib import afirma, comprueba, resumen # noqa: E402
12
+
13
+
14
+ def main():
15
+ print('\n 40-agent mixed Claude/Codex stress')
16
+ result = subprocess.run(
17
+ [sys.executable, os.path.join(RAIZ, 'benchmarks', 'stress', 'run.py'),
18
+ '--agents', '40', '--rounds', '2', '--json'],
19
+ capture_output=True, text=True, timeout=90)
20
+ try:
21
+ report = json.loads(result.stdout)
22
+ except json.JSONDecodeError:
23
+ report = {}
24
+ afirma('· the offline 40-agent stress run passes',
25
+ result.returncode == 0 and report.get('status') == 'pass',
26
+ result.stderr.strip() or result.stdout[-1000:])
27
+ comprueba('· the city mixes 20 Claude and 20 Codex actors',
28
+ report.get('mix'),
29
+ {'claude-stream-json': 20, 'codex-app-server-ws': 20})
30
+ comprueba('· both rounds reach all 40 native runtimes',
31
+ [row.get('accepted') for row in report.get('rounds', [])], [40, 40])
32
+ comprueba('· each round keeps exactly one shared WebSocket hub',
33
+ [row.get('hubProcesses') for row in report.get('rounds', [])], [1, 1])
34
+ afirma('· local native acceptance stays inside the stress budget',
35
+ len(report.get('rounds', [])) == 2
36
+ and all(row.get('p95NativeAcceptMs', 99999) < 5_000
37
+ and row.get('maxNativeAcceptMs', 99999) < 10_000
38
+ for row in report['rounds']),
39
+ str(report.get('rounds')))
40
+ comprueba('· 80 acceptances are split across the native transports',
41
+ report.get('transportAcceptances'),
42
+ {'claude-stream-json': 40, 'codex-app-server-ws': 40})
43
+ afirma('· the browser WebSocket represents all 40 actors and survives hub replacement',
44
+ report.get('liveFeed', {}).get('transport')
45
+ == 'authenticated-read-only-websocket'
46
+ and report.get('liveFeed', {}).get('participantsPerRound') == [40, 40]
47
+ and report.get('liveFeed', {}).get('openEvents') == 2
48
+ and report.get('liveFeed', {}).get('survivedHubRestart') is True,
49
+ str(report.get('liveFeed')))
50
+ comprueba('· the live browser feed duplicates no committee event',
51
+ report.get('liveFeed', {}).get('duplicates'), 0)
52
+ comprueba('· stress recovery produces no duplicate delivery',
53
+ report.get('duplicateDeliveries'), 0)
54
+ afirma('· stress never uses terminal injection or leaves queued work',
55
+ report.get('terminalInjection') is False
56
+ and report.get('remainingOutboxes') == 0)
57
+ comprueba('· stress cleanup leaves no runtime or hub process',
58
+ report.get('cleanup'),
59
+ {'remainingHubProcesses': [], 'remainingRuntimeProcesses': 0})
60
+ return resumen('stress')
61
+
62
+
63
+ if __name__ == '__main__':
64
+ sys.exit(main())
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """Run the terminal widgets through a fake window, and check what they return.
3
+
4
+ Why this exists, precisely: the scripted harness in ui.py sets `TTY = False`, so
5
+ every widget answers from AGENTS_CITY_ANSWERS and returns *before* entering its
6
+ curses loop. That covers the wizard's logic and leaves the four widgets never
7
+ executed — and one of them shipped broken for exactly that reason. `elige` did
8
+ `marcadas |= {...}` inside its nested `run()`, which made `marcadas` local to it,
9
+ so the very first frame died on `UnboundLocalError` and the roles screen was
10
+ unreachable in any real terminal.
11
+
12
+ So: no pty, no terminal, no hands. Replace `curses.wrapper` with something that
13
+ calls `run()` against a window that renders nothing and returns a scripted list
14
+ of keystrokes. Every branch of the key handling runs for real.
15
+
16
+ ./bin/test-widgets.py
17
+ """
18
+ import curses
19
+ import os
20
+ import sys
21
+
22
+ AQUI = os.path.dirname(os.path.abspath(__file__))
23
+ sys.path.insert(0, os.path.join(os.path.dirname(AQUI), 'plugin', 'scripts'))
24
+ import ui # noqa: E402
25
+
26
+ FALLOS = []
27
+ TECLAS = []
28
+ COMPROBADAS = 0
29
+
30
+
31
+ class Ventana:
32
+ """Wide enough that nothing wraps, and every drawing call is a no-op. What we
33
+ are testing is what the widget decides, not what it paints."""
34
+ def getmaxyx(self):
35
+ return (40, 110)
36
+
37
+ def getch(self):
38
+ if not TECLAS:
39
+ raise AssertionError('the widget asked for a key after the script ran out '
40
+ '— it is not returning when it should')
41
+ return TECLAS.pop(0)
42
+
43
+ def __getattr__(self, _):
44
+ return lambda *a, **k: None
45
+
46
+
47
+ def preparado():
48
+ """curses, stubbed down to the parts the widgets touch."""
49
+ ui.TTY = True
50
+ curses.wrapper = lambda fn, *a, **k: fn(Ventana(), *a, **k)
51
+ curses.curs_set = lambda *_: None
52
+ curses.color_pair = lambda _: 0
53
+ ui._pairs = lambda: None
54
+
55
+
56
+ def comprueba(nombre, teclas, llama, espera):
57
+ global TECLAS, COMPROBADAS
58
+ TECLAS = list(teclas)
59
+ COMPROBADAS += 1
60
+ try:
61
+ salida = llama()
62
+ except Exception as e: # noqa: BLE001
63
+ FALLOS.append(f'{nombre}: raised {type(e).__name__}: {e}')
64
+ return
65
+ if salida != espera:
66
+ FALLOS.append(f'{nombre}: got {salida!r}, expected {espera!r}')
67
+ return
68
+ if TECLAS:
69
+ FALLOS.append(f'{nombre}: returned with {len(TECLAS)} keystrokes unread')
70
+
71
+
72
+ ESPACIO, ENTRA, ESC = ord(' '), 13, 27
73
+ ABAJO, ARRIBA = curses.KEY_DOWN, curses.KEY_UP
74
+ OPS = [('a', 'Alpha', 'first'), ('b', 'Beta', 'second'), ('c', 'Gamma', 'third')]
75
+
76
+
77
+ def main():
78
+ preparado()
79
+
80
+ # ── elige: the multi-select, and the one that was broken ────────────────
81
+ comprueba('elige · enter with the preselection',
82
+ [ENTRA], lambda: ui.elige('R', OPS, marcadas=['a']), ['a'])
83
+ comprueba('elige · a takes all', # was UnboundLocalError
84
+ [ord('a'), ENTRA], lambda: ui.elige('R', OPS), ['a', 'b', 'c'])
85
+ comprueba('elige · n drops all', # was UnboundLocalError
86
+ [ord('a'), ord('n'), ENTRA], lambda: ui.elige('R', OPS), [])
87
+ comprueba('elige · space toggles on',
88
+ [ESPACIO, ENTRA], lambda: ui.elige('R', OPS), ['a'])
89
+ comprueba('elige · space toggles off again',
90
+ [ESPACIO, ENTRA], lambda: ui.elige('R', OPS, marcadas=['a']), [])
91
+ comprueba('elige · moves down before picking',
92
+ [ABAJO, ESPACIO, ENTRA], lambda: ui.elige('R', OPS), ['b'])
93
+ comprueba('elige · will not stop below the minimum',
94
+ [ENTRA, ESPACIO, ENTRA], lambda: ui.elige('R', OPS, minimo=1), ['a'])
95
+ comprueba('elige · quitting returns nothing',
96
+ [ESC], lambda: ui.elige('R', OPS), None)
97
+ comprueba('elige · filter narrows what a and space reach',
98
+ [ord('/'), ord('B'), ENTRA, ord('a'), ENTRA],
99
+ lambda: ui.elige('R', OPS), ['b'])
100
+ comprueba('elige · n only drops what the filter shows',
101
+ [ord('a'), ord('/'), ord('B'), ENTRA, ord('n'), ENTRA],
102
+ lambda: ui.elige('R', OPS), ['a', 'c'])
103
+ comprueba('elige · an empty list still returns',
104
+ [ENTRA], lambda: ui.elige('R', []), [])
105
+
106
+ # ── una: the single choice ─────────────────────────────────────────────
107
+ comprueba('una · enter takes the first',
108
+ [ENTRA], lambda: ui.una('K', OPS), 'a')
109
+ comprueba('una · down then enter',
110
+ [ABAJO, ENTRA], lambda: ui.una('K', OPS), 'b')
111
+ comprueba('una · up from the top wraps to the bottom',
112
+ [ARRIBA, ENTRA], lambda: ui.una('K', OPS), 'c')
113
+ comprueba('una · down past the bottom wraps to the top',
114
+ [ABAJO, ABAJO, ABAJO, ENTRA], lambda: ui.una('K', OPS), 'a')
115
+ comprueba('una · an empty list returns nothing instead of dividing by zero',
116
+ [], lambda: ui.una('K', []), None)
117
+ comprueba('una · quitting returns nothing',
118
+ [ESC], lambda: ui.una('K', OPS), None)
119
+
120
+ # ── pantalla: the page of text ─────────────────────────────────────────
121
+ comprueba('pantalla · enter continues',
122
+ [ENTRA], lambda: ui.pantalla('T', 'body\n\nsecond paragraph'), True)
123
+ comprueba('pantalla · q stops',
124
+ [ord('q')], lambda: ui.pantalla('T', 'body'), False)
125
+
126
+ # `pide` reads with input(), not curses, so it has nothing to stub. Its wrapping
127
+ # helper is pure and worth one check: paragraphs survive as blank lines.
128
+ lineas = ui.texto('one two\n\nthree four', 40)
129
+ if '' not in lineas:
130
+ FALLOS.append('texto: paragraph break was lost')
131
+
132
+ if FALLOS:
133
+ print(f'\n {len(FALLOS)} widget failure(s)\n')
134
+ for f in FALLOS:
135
+ print(f' ✗ {f}')
136
+ print()
137
+ return 1
138
+ print(f'\n widgets ok — {COMPROBADAS} checks across elige, una, pantalla and texto\n')
139
+ return 0
140
+
141
+
142
+ if __name__ == '__main__':
143
+ sys.exit(main())
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """Agent-first workspaces: legacy repos still work, new agents mount folders.
3
+
4
+ Under a throwaway data dir. The load-bearing cases: a bare `repos:` card
5
+ normalises to one agent per repo (unchanged behaviour), a new `agents:` card
6
+ drives workspaces with mounts, mounts are validated symlinks that resolve to
7
+ their real target for the cage, and re-mounting a name repoints atomically.
8
+ """
9
+
10
+ import os
11
+ import shutil
12
+ import sys
13
+ import tempfile
14
+
15
+ AQUI = os.path.dirname(os.path.abspath(__file__))
16
+ RAIZ = os.path.dirname(AQUI)
17
+ sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
18
+ sys.path.insert(0, AQUI)
19
+
20
+ import workspace as ws # noqa: E402
21
+ from testlib import afirma, comprueba, resumen # noqa: E402
22
+
23
+ LEGACY = """---
24
+ user: ada
25
+ agent: ada/seat
26
+ repos: [nova, store-service]
27
+ role.nova: po
28
+ role.store-service: security
29
+ runs.store-service: codex
30
+ ---
31
+ """
32
+
33
+ NUEVO = """---
34
+ user: ada
35
+ agent: ada/seat
36
+ agents: [writer, chair]
37
+ kind.writer: knowledge
38
+ role.writer: blank
39
+ mounts.writer: [~/docs/handbook]
40
+ kind.chair: coordinator
41
+ runs.chair: claude
42
+ ---
43
+ """
44
+
45
+
46
+ def legacy_subsume():
47
+ base = tempfile.mkdtemp(prefix="agents-city-ws-")
48
+ try:
49
+ ags = ws.agentes(LEGACY, base)
50
+ comprueba("a bare repos card yields one agent per repo", len(ags), 2)
51
+ a = {x.slug: x for x in ags}
52
+ afirma("each legacy agent is marked legacy", all(x.legacy for x in ags))
53
+ comprueba("its kind defaults to code", a["nova"].clase, "code")
54
+ comprueba("its role carries over", a["nova"].rol, "po")
55
+ comprueba("its runtime carries over", a["store-service"].runtime, "codex")
56
+ comprueba("a legacy agent's single mount is its repo name",
57
+ a["nova"].mounts, ["nova"])
58
+ # With a legacy resolver, the workspace becomes the resolved repo path.
59
+ ags2 = ws.agentes(LEGACY, base, resolver_legacy=lambda r: f"/repos/{r}")
60
+ comprueba("the resolver sets the workspace to the repo path",
61
+ {x.slug: x.workspace for x in ags2}["nova"], "/repos/nova")
62
+ finally:
63
+ shutil.rmtree(base, ignore_errors=True)
64
+
65
+
66
+ def nuevo_modelo():
67
+ base = tempfile.mkdtemp(prefix="agents-city-ws-")
68
+ try:
69
+ ags = {x.slug: x for x in ws.agentes(NUEVO, base)}
70
+ comprueba("an agents card yields the declared agents", len(ags), 2)
71
+ afirma("new agents are not legacy", not any(x.legacy for x in ags.values()))
72
+ comprueba("a knowledge agent keeps its kind", ags["writer"].clase, "knowledge")
73
+ comprueba("a coordinator keeps its kind", ags["chair"].clase, "coordinator")
74
+ comprueba("its workspace is under the city's agents dir",
75
+ ags["writer"].workspace, ws.workspace_de(base, "writer"))
76
+ comprueba("declared mounts are read from the card",
77
+ ags["writer"].mounts, ["~/docs/handbook"])
78
+ afirma("como_dict is JSON-shaped",
79
+ set(ags["writer"].como_dict()) >= {"name", "kind", "workspace", "mounts"})
80
+ finally:
81
+ shutil.rmtree(base, ignore_errors=True)
82
+
83
+
84
+ def montajes():
85
+ base = tempfile.mkdtemp(prefix="agents-city-ws-")
86
+ try:
87
+ real = os.path.join(base, "real-docs")
88
+ os.makedirs(real)
89
+ with open(os.path.join(real, "note.md"), "w") as f:
90
+ f.write("x")
91
+ enlace = ws.monta(base, "writer", real)
92
+ afirma("mounting creates a symlink in the workspace", os.path.islink(enlace))
93
+ comprueba("the workspace mounts dir now lists it",
94
+ [e for e, _ in ws.mounts_en_disco(base, "writer")],
95
+ ["real-docs"])
96
+ comprueba("the mount resolves to its real target",
97
+ ws.mounts_en_disco(base, "writer")[0][1], os.path.realpath(real))
98
+
99
+ # mount_targets follows the symlink to the real path — what the cage seals.
100
+ agente = ws.agentes(NUEVO, base)[0] # 'writer'
101
+ targets = ws.mount_targets(agente, base)
102
+ afirma("mount_targets returns the resolved real path, not the link",
103
+ os.path.realpath(real) in targets)
104
+
105
+ # Re-mounting the same name repoints atomically, does not error.
106
+ otro = os.path.join(base, "other-docs")
107
+ os.makedirs(otro)
108
+ ws.monta(base, "writer", otro, nombre="real-docs")
109
+ comprueba("re-mounting a name repoints it",
110
+ ws.mounts_en_disco(base, "writer")[0][1], os.path.realpath(otro))
111
+
112
+ afirma("mounting a nonexistent path is refused",
113
+ _rechaza(lambda: ws.monta(base, "writer", os.path.join(base, "ghost"))))
114
+ afirma("unmount removes the link", ws.desmonta(base, "writer", "real-docs"))
115
+ afirma("unmount of nothing returns False", not ws.desmonta(base, "writer", "nope"))
116
+ finally:
117
+ shutil.rmtree(base, ignore_errors=True)
118
+
119
+
120
+ def colisiones_de_basename():
121
+ # Two sources sharing a basename must NOT collapse into one mount.
122
+ base = tempfile.mkdtemp(prefix="agents-city-ws-")
123
+ try:
124
+ a = os.path.join(base, "work", "api")
125
+ b = os.path.join(base, "other", "api")
126
+ os.makedirs(a)
127
+ os.makedirs(b)
128
+ ws.monta(base, "dev", a)
129
+ ws.monta(base, "dev", b)
130
+ destinos = sorted(t for _, t in ws.mounts_en_disco(base, "dev"))
131
+ comprueba("both same-basename mounts survive (no silent overwrite)",
132
+ len(destinos), 2)
133
+ afirma("both real targets are present",
134
+ os.path.realpath(a) in destinos and os.path.realpath(b) in destinos)
135
+ # An uppercase/underscore name is slugged the same way at mount and unmount.
136
+ docs = os.path.join(base, "My_Docs")
137
+ os.makedirs(docs)
138
+ ws.monta(base, "dev", docs, nombre="My_Docs")
139
+ afirma("a mount is removable by the name it was created with",
140
+ ws.desmonta(base, "dev", "My_Docs"))
141
+ # A colon in the resolved path is refused (it would break the pipeline).
142
+ raro = os.path.join(base, "a:b")
143
+ os.makedirs(raro)
144
+ afirma("a colon-containing path is refused", _rechaza(lambda: ws.monta(base, "dev", raro)))
145
+ finally:
146
+ shutil.rmtree(base, ignore_errors=True)
147
+
148
+
149
+ def slugs_colisionados_rechazados():
150
+ base = tempfile.mkdtemp(prefix="agents-city-ws-")
151
+ try:
152
+ chocan = "---\nuser: x\nagents: [store-service, store_service]\n---\n"
153
+ afirma("two agents that slug to one identity are refused",
154
+ _rechaza(lambda: ws.agentes(chocan, base)))
155
+ finally:
156
+ shutil.rmtree(base, ignore_errors=True)
157
+
158
+
159
+ def sync_solo_materializados():
160
+ base = tempfile.mkdtemp(prefix="agents-city-ws-")
161
+ try:
162
+ real = os.path.join(base, "real")
163
+ os.makedirs(real)
164
+ card_txt = ("---\nuser: x\nagents: [w]\nkind.w: knowledge\n"
165
+ f"mounts.w: [{real}, {os.path.join(base, 'ghost')}]\n---\n")
166
+ agente = ws.agentes(card_txt, base)[0]
167
+ targets = ws.sincroniza(agente, base)
168
+ comprueba("sync returns only what materialised, not the missing source",
169
+ targets, [os.path.realpath(real)])
170
+ finally:
171
+ shutil.rmtree(base, ignore_errors=True)
172
+
173
+
174
+ def _rechaza(fn):
175
+ try:
176
+ fn()
177
+ return False
178
+ except (OSError, ValueError):
179
+ return True
180
+
181
+
182
+ legacy_subsume()
183
+ nuevo_modelo()
184
+ montajes()
185
+ colisiones_de_basename()
186
+ slugs_colisionados_rechazados()
187
+ sync_solo_materializados()
188
+ sys.exit(resumen("workspace"))
package/bin/testlib.py ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """The two assertions and the summary every suite here uses.
3
+
4
+ Four suites carried an identical copy of `comprueba`, `afirma`, the two counters and
5
+ the closing summary. The suites exist to hunt down copies that drift; their own
6
+ helpers should not be the next example.
7
+
8
+ Each suite runs as its own process, so module state is per-suite by construction.
9
+ """
10
+ import os
11
+ import signal
12
+ import subprocess
13
+ import time
14
+
15
+ FALLOS, HECHAS = [], 0
16
+
17
+
18
+ def comprueba(nombre, real, esperado):
19
+ """Equality, with both sides printed when it is not."""
20
+ global HECHAS
21
+ HECHAS += 1
22
+ if real != esperado:
23
+ FALLOS.append(f'{nombre}\n got {real!r}\n expected {esperado!r}')
24
+
25
+
26
+ def afirma(nombre, condicion, detalle=''):
27
+ """A plain condition, with whatever context the caller thought to keep."""
28
+ global HECHAS
29
+ HECHAS += 1
30
+ if not condicion:
31
+ FALLOS.append(f'{nombre}{chr(10) + " " + detalle if detalle else ""}')
32
+
33
+
34
+ def detiene_proceso(pid, segundos=2):
35
+ """Stop one exact test-owned PID and prove it did not become an orphan."""
36
+ if not pid:
37
+ return True
38
+ try:
39
+ os.kill(int(pid), signal.SIGTERM)
40
+ except ProcessLookupError:
41
+ return True
42
+ limite = time.monotonic() + segundos
43
+ while time.monotonic() < limite:
44
+ try:
45
+ os.kill(int(pid), 0)
46
+ except ProcessLookupError:
47
+ return True
48
+ time.sleep(.05)
49
+ try:
50
+ os.kill(int(pid), signal.SIGKILL)
51
+ except ProcessLookupError:
52
+ return True
53
+ time.sleep(.1)
54
+ try:
55
+ os.kill(int(pid), 0)
56
+ return False
57
+ except ProcessLookupError:
58
+ return True
59
+
60
+
61
+ def hubs_de_ciudad(datos):
62
+ """Return only local-hub PIDs whose exact --data argument is test-owned."""
63
+ target = os.path.realpath(datos)
64
+ result = subprocess.run(
65
+ ['ps', '-axo', 'pid=,command='], capture_output=True, text=True)
66
+ pids = []
67
+ marker = 'local-hub.js --data '
68
+ for line in result.stdout.splitlines():
69
+ if marker not in line:
70
+ continue
71
+ raw_pid, command = line.strip().split(None, 1)
72
+ supplied = command.split(marker, 1)[1].strip()
73
+ if os.path.realpath(supplied) == target:
74
+ pids.append(int(raw_pid))
75
+ return sorted(set(pids))
76
+
77
+
78
+ def detiene_hubs_de_ciudad(datos):
79
+ """Stop every exact test-owned hub, including a hidden startup-race loser."""
80
+ return all(detiene_proceso(pid) for pid in hubs_de_ciudad(datos)) \
81
+ and not hubs_de_ciudad(datos)
82
+
83
+
84
+ def resumen(etiqueta):
85
+ """Print the verdict and return the exit code. `N checks` is the exact phrase
86
+ bin/test greps for, so it lives in one place."""
87
+ print()
88
+ if FALLOS:
89
+ print(f' {len(FALLOS)} of {HECHAS} failed\n')
90
+ for f in FALLOS:
91
+ print(f' ✗ {f}')
92
+ print()
93
+ return 1
94
+ print(f' {etiqueta} ok — {HECHAS} checks\n')
95
+ return 0
package/bin/tokens.py ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python3
2
+ """Report this machine's token spend to the city.
3
+
4
+ A one-line wrapper. The reporter itself ships inside the plugin, because that is
5
+ what gets installed on everybody's machine — keeping a second copy here would
6
+ mean two implementations of the same arithmetic, and one of them would rot.
7
+
8
+ ./bin/tokens.py --url https://your-city.workers.dev --push
9
+ """
10
+ import os
11
+ import sys
12
+ os.execv(sys.executable, [sys.executable,
13
+ os.path.join(os.path.dirname(os.path.abspath(__file__)),
14
+ '..', 'plugin', 'scripts', 'tokens.py'), *sys.argv[1:]])
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # Deploy the hub. Creates the token KV the first time and writes its id into
3
+ # wrangler.toml.
4
+ #
5
+ # ./scripts/deploy.sh
6
+ #
7
+ # You have to be logged into the right Cloudflare account:
8
+ # npx --yes wrangler@4 login (interactive, opens the browser)
9
+ # o exportar CLOUDFLARE_API_TOKEN con un token que tenga
10
+ # Workers Scripts:Edit · Workers KV Storage:Edit · Account Settings:Read
11
+
12
+ set -euo pipefail
13
+
14
+ cd "$(dirname "$0")/../worker"
15
+
16
+ # A malformed CF_API_TOKEN in the environment wins over everything else in
17
+ # wrangler, including a perfectly good OAuth session — so it is taken out of the
18
+ # way for this one invocation.
19
+ unset CF_API_TOKEN
20
+
21
+ # The runner: bun if it is here, npx otherwise — npx ships with npm, so it is
22
+ # the one almost everybody already has.
23
+ if command -v bunx >/dev/null 2>&1; then CORRE=(bunx)
24
+ elif command -v bun >/dev/null 2>&1; then CORRE=(bun x)
25
+ else CORRE=(npx --yes); fi
26
+ W() { "${CORRE[@]}" wrangler@4 "$@"; }
27
+
28
+ echo "→ account:"
29
+ W whoami 2>&1 | grep -E "logged in|Account Name|│" | head -5 || true
30
+ echo
31
+
32
+ if grep -qE 'id = "(PENDIENTE_DE_CREAR|<YOUR_KV_NAMESPACE_ID>)"' wrangler.toml; then
33
+ echo "→ creating the token KV..."
34
+ OUT="$(W kv namespace create TOKENS 2>&1)"
35
+ echo "$OUT" | tail -5
36
+ ID="$(printf '%s' "$OUT" | grep -oE '"?id"?[ =:]+"?[a-f0-9]{32}' | grep -oE '[a-f0-9]{32}' | head -1)"
37
+ if [ -z "$ID" ]; then
38
+ echo "I could not find the KV id in wrangler's output." >&2
39
+ echo "Put it into worker/wrangler.toml by hand and run this again." >&2
40
+ exit 1
41
+ fi
42
+ sed -i.bak -E "s/id = \"(PENDIENTE_DE_CREAR|<YOUR_KV_NAMESPACE_ID>)\"/id = \"$ID\"/" wrangler.toml && rm -f wrangler.toml.bak
43
+ echo " KV created: $ID"
44
+ echo
45
+ fi
46
+
47
+ echo "→ deploying..."
48
+ W deploy
49
+
50
+ cat <<'EOF'
51
+
52
+ Done. Now one token per owner prefix (it can serve several cities):
53
+
54
+ ./scripts/mint-token.sh <user>
55
+
56
+ And check the hub answers:
57
+
58
+ curl https://city-bus.<your-subdomain>.workers.dev/health
59
+
60
+ EOF
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Mint a token for one city owner prefix and register it in KV.
3
+ #
4
+ # ./scripts/mint-token.sh alice
5
+ # ./scripts/mint-token.sh alice --local (against wrangler dev)
6
+ # ./scripts/mint-token.sh alice --to bob,carol (may only write to those)
7
+ #
8
+ # It prints the token ONCE: KV keeps only its SHA-256, so a lost token is a
9
+ # reissued token. Hand it over privately — never through the repo.
10
+
11
+ set -euo pipefail
12
+
13
+ USER_NAME="${1:-}"
14
+ [ -z "$USER_NAME" ] && { echo "usage: $0 <user> [--local] [--to a,b]" >&2; exit 1; }
15
+ shift
16
+
17
+ if ! [[ "$USER_NAME" =~ ^[a-z0-9][a-z0-9_-]{0,31}$ ]]; then
18
+ echo "invalid user: lowercase, digits, dashes; no slashes" >&2; exit 1
19
+ fi
20
+
21
+ REMOTE="--remote"
22
+ CAN_SEND_TO=""
23
+ while [ $# -gt 0 ]; do
24
+ case "$1" in
25
+ --local) REMOTE="--local"; shift ;;
26
+ --to) CAN_SEND_TO="${2:-}"; shift 2 ;;
27
+ *) echo "unknown option: $1" >&2; exit 1 ;;
28
+ esac
29
+ done
30
+
31
+ # The runner: npx ships with npm, so it is the one almost everybody has.
32
+ if command -v bunx >/dev/null 2>&1; then CORRE=(bunx)
33
+ elif command -v bun >/dev/null 2>&1; then CORRE=(bun x)
34
+ elif command -v npx >/dev/null 2>&1; then CORRE=(npx --yes)
35
+ else echo "I need npx (comes with Node) or bun." >&2; exit 1; fi
36
+
37
+ cd "$(dirname "$0")/../worker"
38
+
39
+ TOKEN="rb_$(openssl rand -hex 24)"
40
+ HASH="$(printf '%s' "$TOKEN" | shasum -a 256 | cut -d' ' -f1)"
41
+
42
+ if [ -n "$CAN_SEND_TO" ]; then
43
+ LIST="$(printf '%s' "$CAN_SEND_TO" | awk -F, '{for(i=1;i<=NF;i++) printf "%s\"%s\"", (i>1?",":""), $i}')"
44
+ VALUE="{\"user\":\"$USER_NAME\",\"can_send_to\":[$LIST]}"
45
+ else
46
+ VALUE="{\"user\":\"$USER_NAME\"}"
47
+ fi
48
+
49
+ "${CORRE[@]}" wrangler@4 kv key put $REMOTE --binding TOKENS "tok:$HASH" "$VALUE" >/dev/null
50
+
51
+ cat <<EOF
52
+
53
+ Token for "$USER_NAME" (shown this once only):
54
+
55
+ $TOKEN
56
+
57
+ They should keep it in the plugin's sensitive configuration or in
58
+ ~/.claude/channels/city-bus/.env :
59
+
60
+ CITY_BUS_URL=https://city-bus.<your-subdomain>.workers.dev
61
+ CITY_BUS_TOKEN=$TOKEN
62
+
63
+ Each seat derives its own $USER_NAME/<city> address at runtime.
64
+
65
+ To revoke it:
66
+ npx --yes wrangler@4 kv key delete $REMOTE --binding TOKENS "tok:$HASH"
67
+
68
+ EOF