agents-city 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2226 -0
  5. package/README.md +2196 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +119 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/doctor +3 -0
  28. package/bin/exit +5 -0
  29. package/bin/hall +73 -0
  30. package/bin/hall.html +498 -0
  31. package/bin/logs +3 -0
  32. package/bin/navegador.mjs +243 -0
  33. package/bin/report.py +14 -0
  34. package/bin/reset +3 -0
  35. package/bin/road +3 -0
  36. package/bin/seat +10 -0
  37. package/bin/serve.py +1579 -0
  38. package/bin/setup.py +753 -0
  39. package/bin/shortcut +3 -0
  40. package/bin/skills +3 -0
  41. package/bin/test +133 -0
  42. package/bin/test-actualiza.py +130 -0
  43. package/bin/test-adapter.py +183 -0
  44. package/bin/test-admision.py +99 -0
  45. package/bin/test-atajos.py +301 -0
  46. package/bin/test-avatar.py +58 -0
  47. package/bin/test-benchmark.py +40 -0
  48. package/bin/test-broker.py +334 -0
  49. package/bin/test-cage.py +374 -0
  50. package/bin/test-card.py +402 -0
  51. package/bin/test-channel.py +379 -0
  52. package/bin/test-cities.py +453 -0
  53. package/bin/test-claude-runtime.py +252 -0
  54. package/bin/test-committee.py +340 -0
  55. package/bin/test-contracts.py +551 -0
  56. package/bin/test-crecimiento.py +164 -0
  57. package/bin/test-demo.py +308 -0
  58. package/bin/test-doctor.py +139 -0
  59. package/bin/test-domains.py +151 -0
  60. package/bin/test-evidencia.py +50 -0
  61. package/bin/test-exit.py +128 -0
  62. package/bin/test-hall-protocol.py +93 -0
  63. package/bin/test-launch.py +169 -0
  64. package/bin/test-live-feed.py +508 -0
  65. package/bin/test-navegador.py +140 -0
  66. package/bin/test-pairing.py +99 -0
  67. package/bin/test-parcels.py +283 -0
  68. package/bin/test-runtime-failures.py +252 -0
  69. package/bin/test-runtime-ui.py +488 -0
  70. package/bin/test-runtime.py +268 -0
  71. package/bin/test-rutas.py +80 -0
  72. package/bin/test-seat.py +1598 -0
  73. package/bin/test-security.py +150 -0
  74. package/bin/test-serve.py +1251 -0
  75. package/bin/test-stress.py +64 -0
  76. package/bin/test-widgets.py +143 -0
  77. package/bin/test-workspace.py +251 -0
  78. package/bin/testlib.py +118 -0
  79. package/bin/tokens.py +14 -0
  80. package/bin/update +3 -0
  81. package/bus/scripts/deploy.sh +60 -0
  82. package/bus/scripts/mint-token.sh +68 -0
  83. package/bus/scripts/setup-dev.sh +79 -0
  84. package/bus/scripts/test-channel.ts +8 -0
  85. package/bus/scripts/test-hub.ts +266 -0
  86. package/bus/scripts/test-local.sh +40 -0
  87. package/bus/scripts/test-queue.ts +132 -0
  88. package/bus/worker/package-lock.json +1971 -0
  89. package/bus/worker/package.json +17 -0
  90. package/bus/worker/src/index.ts +485 -0
  91. package/bus/worker/tsconfig.json +13 -0
  92. package/bus/worker/wrangler.toml +32 -0
  93. package/city/oven/README.md +59 -0
  94. package/city/oven/collect.py +43 -0
  95. package/city/oven/oven.html +4 -0
  96. package/city/scripts/history.py +120 -0
  97. package/city/scripts/seed.py +442 -0
  98. package/city/web/assets/modelos/License.txt +28 -0
  99. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  100. package/city/web/assets/modelos/building-a.glb +0 -0
  101. package/city/web/assets/modelos/building-b.glb +0 -0
  102. package/city/web/assets/modelos/building-c.glb +0 -0
  103. package/city/web/assets/modelos/building-d.glb +0 -0
  104. package/city/web/assets/modelos/building-e.glb +0 -0
  105. package/city/web/assets/modelos/building-f.glb +0 -0
  106. package/city/web/assets/modelos/building-g.glb +0 -0
  107. package/city/web/assets/modelos/building-h.glb +0 -0
  108. package/city/web/assets/sprites/building-a.png +0 -0
  109. package/city/web/assets/sprites/building-b.png +0 -0
  110. package/city/web/assets/sprites/building-c.png +0 -0
  111. package/city/web/assets/sprites/building-d.png +0 -0
  112. package/city/web/assets/sprites/building-e.png +0 -0
  113. package/city/web/assets/sprites/building-f.png +0 -0
  114. package/city/web/assets/sprites/building-g.png +0 -0
  115. package/city/web/assets/sprites/building-h.png +0 -0
  116. package/city/web/assets/sprites/building-i.png +0 -0
  117. package/city/web/assets/sprites/building-j.png +0 -0
  118. package/city/web/assets/sprites/building-k.png +0 -0
  119. package/city/web/assets/sprites/building-l.png +0 -0
  120. package/city/web/assets/sprites/building-m.png +0 -0
  121. package/city/web/assets/sprites/building-n.png +0 -0
  122. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  123. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  124. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  125. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  126. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  127. package/city/web/assets/sprites/catalogo.json +47 -0
  128. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  129. package/city/web/assets/sprites/detail-awning.png +0 -0
  130. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  131. package/city/web/assets/sprites/detail-overhang.png +0 -0
  132. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  133. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  143. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  144. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  145. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  146. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  147. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  148. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  149. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  150. package/city/web/assets/sprites/medidas.json +248 -0
  151. package/city/web/dist/city.js +939 -0
  152. package/city/web/dist/index.html +294 -0
  153. package/city/web/dist/modelos/License.txt +28 -0
  154. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  155. package/city/web/dist/modelos/building-a.glb +0 -0
  156. package/city/web/dist/modelos/building-b.glb +0 -0
  157. package/city/web/dist/modelos/building-c.glb +0 -0
  158. package/city/web/dist/modelos/building-d.glb +0 -0
  159. package/city/web/dist/modelos/building-e.glb +0 -0
  160. package/city/web/dist/modelos/building-f.glb +0 -0
  161. package/city/web/dist/modelos/building-g.glb +0 -0
  162. package/city/web/dist/modelos/building-h.glb +0 -0
  163. package/city/web/dist/sprites/building-a.png +0 -0
  164. package/city/web/dist/sprites/building-b.png +0 -0
  165. package/city/web/dist/sprites/building-c.png +0 -0
  166. package/city/web/dist/sprites/building-d.png +0 -0
  167. package/city/web/dist/sprites/building-e.png +0 -0
  168. package/city/web/dist/sprites/building-f.png +0 -0
  169. package/city/web/dist/sprites/building-g.png +0 -0
  170. package/city/web/dist/sprites/building-h.png +0 -0
  171. package/city/web/dist/sprites/building-i.png +0 -0
  172. package/city/web/dist/sprites/building-j.png +0 -0
  173. package/city/web/dist/sprites/building-k.png +0 -0
  174. package/city/web/dist/sprites/building-l.png +0 -0
  175. package/city/web/dist/sprites/building-m.png +0 -0
  176. package/city/web/dist/sprites/building-n.png +0 -0
  177. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  178. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  179. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  180. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  181. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  182. package/city/web/dist/sprites/catalogo.json +47 -0
  183. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  184. package/city/web/dist/sprites/detail-awning.png +0 -0
  185. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  186. package/city/web/dist/sprites/detail-overhang.png +0 -0
  187. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  188. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  198. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  199. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  200. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  201. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  202. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  203. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  204. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  205. package/city/web/dist/sprites/medidas.json +248 -0
  206. package/city/web/dist-hall/hall.js +1363 -0
  207. package/city/web/index.html +294 -0
  208. package/city/web/package.json +18 -0
  209. package/city/web/sella.py +21 -0
  210. package/city/web/src/activity-actors.ts +47 -0
  211. package/city/web/src/activity.ts +172 -0
  212. package/city/web/src/ayuntamiento.ts +580 -0
  213. package/city/web/src/draw.ts +640 -0
  214. package/city/web/src/game-speech.ts +79 -0
  215. package/city/web/src/hall.ts +1959 -0
  216. package/city/web/src/main.ts +2252 -0
  217. package/city/web/src/oven.ts +124 -0
  218. package/city/web/src/people.ts +244 -0
  219. package/city/web/src/presencia.ts +193 -0
  220. package/city/web/src/puertas.ts +180 -0
  221. package/city/web/src/repo-roles.ts +59 -0
  222. package/city/worker/package.json +16 -0
  223. package/city/worker/schema.sql +117 -0
  224. package/city/worker/src/index.ts +702 -0
  225. package/city/worker/src/square.ts +218 -0
  226. package/city/worker/wrangler.toml +58 -0
  227. package/demo/ada.md +50 -0
  228. package/demo/bruno.md +62 -0
  229. package/demo/camila.md +56 -0
  230. package/demo/city/ada.md +40 -0
  231. package/demo/city/city.yml +8 -0
  232. package/demo/city/parcels.yml +15 -0
  233. package/demo/city/roads.json +4 -0
  234. package/demo/city/units.yml +10 -0
  235. package/demo/clinica/city.yml +8 -0
  236. package/demo/clinica/parcels.yml +15 -0
  237. package/demo/clinica/roads.json +4 -0
  238. package/demo/clinica/units.yml +9 -0
  239. package/demo/clinica/vera.md +46 -0
  240. package/demo/dante.md +54 -0
  241. package/demo/despacho/city.yml +8 -0
  242. package/demo/despacho/marta.md +46 -0
  243. package/demo/despacho/parcels.yml +15 -0
  244. package/demo/despacho/roads.json +4 -0
  245. package/demo/despacho/units.yml +9 -0
  246. package/demo/elsa.md +50 -0
  247. package/demo/farid.md +52 -0
  248. package/demo/greta.md +50 -0
  249. package/demo/hugo.md +49 -0
  250. package/demo/iris.md +49 -0
  251. package/demo/jonas.md +51 -0
  252. package/demo/kira.md +51 -0
  253. package/demo/luca.md +49 -0
  254. package/demo/parcels.yml +92 -0
  255. package/demo/seed.py +20 -0
  256. package/demo/show.py +161 -0
  257. package/demo/stories.py +824 -0
  258. package/demo/units.yml +33 -0
  259. package/docs/agents-first.md +106 -0
  260. package/docs/glossary.md +55 -0
  261. package/docs/map-live-layers.md +73 -0
  262. package/docs/security.md +265 -0
  263. package/docs/self-host.md +151 -0
  264. package/docs/testing.md +205 -0
  265. package/package.json +73 -0
  266. package/plugin/.claude-plugin/plugin.json +62 -0
  267. package/plugin/.mcp.json +15 -0
  268. package/plugin/channel/activity-cli.ts +16 -0
  269. package/plugin/channel/adapter-prompts.ts +113 -0
  270. package/plugin/channel/adapter.js +4292 -0
  271. package/plugin/channel/adapter.ts +133 -0
  272. package/plugin/channel/bus.js +20188 -0
  273. package/plugin/channel/bus.ts +131 -0
  274. package/plugin/channel/city-config.ts +125 -0
  275. package/plugin/channel/claude-channel.ts +58 -0
  276. package/plugin/channel/cli-args.ts +75 -0
  277. package/plugin/channel/client.js +4200 -0
  278. package/plugin/channel/client.ts +38 -0
  279. package/plugin/channel/committee/activity.ts +256 -0
  280. package/plugin/channel/committee/collection.ts +154 -0
  281. package/plugin/channel/committee/decision.ts +175 -0
  282. package/plugin/channel/committee/floor.ts +135 -0
  283. package/plugin/channel/committee/guards.ts +30 -0
  284. package/plugin/channel/committee/history.ts +38 -0
  285. package/plugin/channel/committee/render.ts +102 -0
  286. package/plugin/channel/committee/service.ts +72 -0
  287. package/plugin/channel/committee/storage.ts +92 -0
  288. package/plugin/channel/committee/types.ts +163 -0
  289. package/plugin/channel/committee/view.ts +63 -0
  290. package/plugin/channel/committee-cli.ts +109 -0
  291. package/plugin/channel/delivery-metrics.ts +61 -0
  292. package/plugin/channel/delivery-queue.ts +118 -0
  293. package/plugin/channel/hub/activity-controller.ts +72 -0
  294. package/plugin/channel/hub/activity-feed.ts +169 -0
  295. package/plugin/channel/hub/committee-controller.ts +60 -0
  296. package/plugin/channel/hub/connections.ts +48 -0
  297. package/plugin/channel/hub/diagnostics.ts +67 -0
  298. package/plugin/channel/hub/envelope-validity.ts +109 -0
  299. package/plugin/channel/hub/envelopes.ts +66 -0
  300. package/plugin/channel/hub/lifecycle.ts +91 -0
  301. package/plugin/channel/hub/local-roads.ts +89 -0
  302. package/plugin/channel/hub/remote-roads.ts +165 -0
  303. package/plugin/channel/hub/road-controller.ts +99 -0
  304. package/plugin/channel/hub-client.ts +149 -0
  305. package/plugin/channel/local-hub.js +5995 -0
  306. package/plugin/channel/local-hub.ts +349 -0
  307. package/plugin/channel/map-reporter.ts +48 -0
  308. package/plugin/channel/package-lock.json +1731 -0
  309. package/plugin/channel/package.json +19 -0
  310. package/plugin/channel/protocol.ts +98 -0
  311. package/plugin/channel/road-cli.ts +18 -0
  312. package/plugin/channel/run.sh +15 -0
  313. package/plugin/channel/runtime/claude.ts +458 -0
  314. package/plugin/channel/runtime/codex-config.ts +77 -0
  315. package/plugin/channel/runtime/codex.ts +669 -0
  316. package/plugin/channel/runtime/command.ts +62 -0
  317. package/plugin/channel/runtime/factory.ts +15 -0
  318. package/plugin/channel/runtime/json-rpc.ts +154 -0
  319. package/plugin/channel/runtime/kimi.ts +277 -0
  320. package/plugin/channel/runtime/opencode.ts +248 -0
  321. package/plugin/channel/runtime/process.ts +84 -0
  322. package/plugin/channel/runtime/types.ts +45 -0
  323. package/plugin/channel/runtime-files.ts +73 -0
  324. package/plugin/channel/runtime-gateway.js +6396 -0
  325. package/plugin/channel/runtime-gateway.ts +358 -0
  326. package/plugin/channel/runtime-metrics.ts +52 -0
  327. package/plugin/channel/runtime-subscription.ts +180 -0
  328. package/plugin/channel/terminal-delivery.ts +131 -0
  329. package/plugin/channel/untrusted.ts +60 -0
  330. package/plugin/commands/committee.md +31 -0
  331. package/plugin/commands/exit.md +32 -0
  332. package/plugin/commands/goals.md +20 -0
  333. package/plugin/commands/join.md +18 -0
  334. package/plugin/commands/notice.md +20 -0
  335. package/plugin/commands/propose.md +19 -0
  336. package/plugin/commands/round.md +17 -0
  337. package/plugin/commands/session.md +22 -0
  338. package/plugin/commands/settings.md +25 -0
  339. package/plugin/commands/setup.md +21 -0
  340. package/plugin/commands/team.md +23 -0
  341. package/plugin/domains/custom.md +27 -0
  342. package/plugin/domains/finance.md +31 -0
  343. package/plugin/domains/healthcare.md +34 -0
  344. package/plugin/domains/legal.md +31 -0
  345. package/plugin/domains/marketing.md +33 -0
  346. package/plugin/domains/operations.md +31 -0
  347. package/plugin/domains/research.md +32 -0
  348. package/plugin/domains/sales.md +30 -0
  349. package/plugin/domains/software.md +37 -0
  350. package/plugin/hooks/activity.sh +6 -0
  351. package/plugin/hooks/digging.sh +68 -0
  352. package/plugin/hooks/growth.sh +42 -0
  353. package/plugin/hooks/hooks.json +106 -0
  354. package/plugin/hooks/notice-on-pr.sh +35 -0
  355. package/plugin/hooks/notice-on-stop.sh +80 -0
  356. package/plugin/hooks/notice-pending.sh +58 -0
  357. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  358. package/plugin/hooks/tokens.sh +47 -0
  359. package/plugin/roles/examples/account-executive.md +25 -0
  360. package/plugin/roles/examples/ai-manager.md +34 -0
  361. package/plugin/roles/examples/associate.md +24 -0
  362. package/plugin/roles/examples/brand-lead.md +20 -0
  363. package/plugin/roles/examples/cfo.md +20 -0
  364. package/plugin/roles/examples/city-lead.md +25 -0
  365. package/plugin/roles/examples/clinical-director.md +26 -0
  366. package/plugin/roles/examples/clinical-ops.md +25 -0
  367. package/plugin/roles/examples/clinician.md +25 -0
  368. package/plugin/roles/examples/compliance.md +27 -0
  369. package/plugin/roles/examples/content.md +24 -0
  370. package/plugin/roles/examples/controller.md +27 -0
  371. package/plugin/roles/examples/cpto.md +41 -0
  372. package/plugin/roles/examples/customer-success.md +25 -0
  373. package/plugin/roles/examples/data-engineer.md +37 -0
  374. package/plugin/roles/examples/data.md +60 -0
  375. package/plugin/roles/examples/dev.md +37 -0
  376. package/plugin/roles/examples/devops.md +39 -0
  377. package/plugin/roles/examples/enablement.md +24 -0
  378. package/plugin/roles/examples/ethics.md +24 -0
  379. package/plugin/roles/examples/fin-analytics.md +27 -0
  380. package/plugin/roles/examples/health-compliance.md +25 -0
  381. package/plugin/roles/examples/health-data.md +26 -0
  382. package/plugin/roles/examples/knowledge.md +25 -0
  383. package/plugin/roles/examples/lifecycle.md +26 -0
  384. package/plugin/roles/examples/llm-engineer.md +34 -0
  385. package/plugin/roles/examples/managing-partner.md +20 -0
  386. package/plugin/roles/examples/methods.md +23 -0
  387. package/plugin/roles/examples/operations-lead.md +23 -0
  388. package/plugin/roles/examples/ops.md +24 -0
  389. package/plugin/roles/examples/patient-safety.md +26 -0
  390. package/plugin/roles/examples/performance.md +27 -0
  391. package/plugin/roles/examples/po.md +36 -0
  392. package/plugin/roles/examples/process-owner.md +23 -0
  393. package/plugin/roles/examples/product-design.md +39 -0
  394. package/plugin/roles/examples/program-manager.md +24 -0
  395. package/plugin/roles/examples/quality.md +25 -0
  396. package/plugin/roles/examples/research-director.md +24 -0
  397. package/plugin/roles/examples/research-ops.md +25 -0
  398. package/plugin/roles/examples/researcher.md +25 -0
  399. package/plugin/roles/examples/revenue-lead.md +25 -0
  400. package/plugin/roles/examples/revops.md +26 -0
  401. package/plugin/roles/examples/seo.md +27 -0
  402. package/plugin/roles/examples/specialist.md +24 -0
  403. package/plugin/scripts/actualiza.py +198 -0
  404. package/plugin/scripts/admision.py +165 -0
  405. package/plugin/scripts/apaga.py +232 -0
  406. package/plugin/scripts/atajos.py +506 -0
  407. package/plugin/scripts/avatar.py +211 -0
  408. package/plugin/scripts/broker.py +531 -0
  409. package/plugin/scripts/cage.py +492 -0
  410. package/plugin/scripts/capabilities.py +210 -0
  411. package/plugin/scripts/card.py +322 -0
  412. package/plugin/scripts/cities.py +520 -0
  413. package/plugin/scripts/city-env.sh +90 -0
  414. package/plugin/scripts/city-runtime.sh +87 -0
  415. package/plugin/scripts/city-session.sh +552 -0
  416. package/plugin/scripts/city_env.py +67 -0
  417. package/plugin/scripts/crecimiento.py +140 -0
  418. package/plugin/scripts/deliberations.py +48 -0
  419. package/plugin/scripts/doctor.py +294 -0
  420. package/plugin/scripts/domains.py +201 -0
  421. package/plugin/scripts/evidencia.py +64 -0
  422. package/plugin/scripts/find-repos.sh +111 -0
  423. package/plugin/scripts/gh.py +159 -0
  424. package/plugin/scripts/hall_protocol.py +147 -0
  425. package/plugin/scripts/hook_activity.py +139 -0
  426. package/plugin/scripts/launch.py +94 -0
  427. package/plugin/scripts/logs.py +98 -0
  428. package/plugin/scripts/pairing.py +185 -0
  429. package/plugin/scripts/parcels.py +132 -0
  430. package/plugin/scripts/read-card.py +55 -0
  431. package/plugin/scripts/report.py +201 -0
  432. package/plugin/scripts/reset.py +180 -0
  433. package/plugin/scripts/roads.py +230 -0
  434. package/plugin/scripts/roles.py +274 -0
  435. package/plugin/scripts/runtime_log.py +69 -0
  436. package/plugin/scripts/runtime_processes.py +132 -0
  437. package/plugin/scripts/rutas.py +83 -0
  438. package/plugin/scripts/seat.py +1696 -0
  439. package/plugin/scripts/tokens.py +194 -0
  440. package/plugin/scripts/trust-repos.py +59 -0
  441. package/plugin/scripts/ui.py +280 -0
  442. package/plugin/scripts/units.py +80 -0
  443. package/plugin/scripts/workspace.py +561 -0
  444. package/plugin/skills/city/SKILL.md +191 -0
  445. package/templates/blank.md +39 -0
  446. package/templates/finance.md +44 -0
  447. package/templates/legal.md +45 -0
  448. package/templates/marketing.md +48 -0
  449. package/templates/product.md +45 -0
package/bin/shortcut ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ # Put a city on your desktop: its icon, its name, double-click to enter.
3
+ exec python3 "$(cd "$(dirname "$0")/../plugin/scripts" && pwd)/atajos.py" "$@"
package/bin/skills ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ # Recognise repo-local skills without installing or changing them.
3
+ exec python3 "$(cd "$(dirname "$0")/../plugin/scripts" && pwd)/capabilities.py" "$@"
package/bin/test ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bash
2
+ # Every test in the repo, in one command.
3
+ #
4
+ # ./bin/test all of it
5
+ # ./bin/test card seat just these
6
+ #
7
+ # Twelve suites and two compatibility walks. Nothing here needs a network, an
8
+ # account, a terminal or a deployed anything: the widgets run against a fake curses
9
+ # window, the reporter against a temporary git repo, and the wizard against a file
10
+ # of answers. The one browser suite drives headless Chrome when the machine has
11
+ # one and says so when it does not — a page whose buttons are wired to nothing
12
+ # passes every other check in here, and one shipped.
13
+ set -uo pipefail
14
+ cd "$(dirname "$0")/.." || exit 1
15
+
16
+ # Arm the commit-time gate while we are here. Git refuses to ship hooks inside a
17
+ # repo (on purpose), so the first test run in a clone points this clone at ours.
18
+ # Installed copies are not git repos and skip this silently.
19
+ if [ -d .git ] && [ -x .githooks/pre-commit ] \
20
+ && [ -z "$(git config core.hooksPath 2>/dev/null)" ]; then
21
+ git config core.hooksPath .githooks \
22
+ && printf ' pre-commit gate armed (core.hooksPath .githooks)\n'
23
+ fi
24
+
25
+ SUITES=(widgets card parcels domains serve seat cities channel committee live-feed runtime claude-runtime runtime-ui runtime-failures stress adapter benchmark demo contracts exit rutas cage broker evidencia admision pairing hall-protocol doctor security workspace avatar crecimiento atajos navegador actualiza launch)
26
+ [ $# -gt 0 ] && SUITES=("$@")
27
+
28
+ fallos=0
29
+ total=0
30
+
31
+ printf '\n'
32
+ for s in "${SUITES[@]}"; do
33
+ guion="bin/test-$s.py"
34
+ if [ ! -x "$guion" ]; then
35
+ printf ' %-10s \033[31mno such suite\033[0m (%s)\n' "$s" "$guion"
36
+ fallos=$((fallos + 1)); continue
37
+ fi
38
+ salida="$(./"$guion" 2>&1)"
39
+ if [ $? -eq 0 ]; then
40
+ n="$(printf '%s' "$salida" | grep -oE '[0-9]+ checks' | tail -1)"
41
+ printf ' %-10s \033[32mok\033[0m %s\n' "$s" "${n:-}"
42
+ total=$((total + $(printf '%s' "${n:-0}" | grep -oE '^[0-9]+' || echo 0)))
43
+ else
44
+ printf ' %-10s \033[31mFAILED\033[0m\n' "$s"
45
+ printf '%s\n' "$salida" | sed 's/^/ /'
46
+ fallos=$((fallos + 1))
47
+ fi
48
+ done
49
+
50
+ if [ $# -eq 0 ]; then
51
+ if ./bin/setup.py --help 2>&1 | grep -q 'one personal Agents City' \
52
+ && ! ./bin/setup.py --help 2>&1 | grep -q 'legacy-team'; then
53
+ printf ' %-10s \033[32mok\033[0m public setup exposes only personal cities\n' "setup"
54
+ else
55
+ printf ' %-10s \033[31mFAILED\033[0m\n' "setup"
56
+ fallos=$((fallos + 1))
57
+ fi
58
+
59
+ # The map fixture remains a compatibility input for the visual seeder.
60
+ if [ "$(./city/scripts/seed.py --data demo --fake-history 2>/dev/null \
61
+ | grep -c 'INSERT INTO parcela')" = 34 ]; then
62
+ printf ' %-10s \033[32mok\033[0m the demo still seeds 34 parcels\n' "seeder"
63
+ else
64
+ printf ' %-10s \033[31mFAILED\033[0m\n' "seeder"
65
+ fallos=$((fallos + 1))
66
+ fi
67
+ fi
68
+
69
+ # The npm package: every path the shim dispatches to must actually ship. This is
70
+ # the check that catches "added a module, forgot the files whitelist" — the shim
71
+ # would break only on the installed machine, where nobody is looking.
72
+ if [ $# -eq 0 ] && command -v npm >/dev/null 2>&1; then
73
+ faltan=$(npm pack --dry-run --json 2>/dev/null | python3 -c '
74
+ import json, sys
75
+ salida = json.load(sys.stdin)
76
+ dentro = {f["path"] for f in salida[0]["files"]}
77
+ necesarios = ["bin/agents-city.js", "bin/hall", "bin/seat", "bin/city", "bin/demo",
78
+ "bin/setup.py", "bin/serve.py", "bin/hall.html",
79
+ "bin/ui.py" if "bin/ui.py" in dentro else "plugin/scripts/ui.py",
80
+ "plugin/scripts/seat.py", "plugin/scripts/card.py",
81
+ "plugin/scripts/parcels.py", "plugin/scripts/units.py",
82
+ "plugin/scripts/roles.py", "plugin/scripts/gh.py",
83
+ "plugin/scripts/domains.py", "plugin/domains/software.md",
84
+ "plugin/roles/examples/data-engineer.md",
85
+ "plugin/scripts/cities.py", "plugin/scripts/roads.py",
86
+ "plugin/scripts/cage.py", "plugin/scripts/broker.py",
87
+ "plugin/scripts/rutas.py", "plugin/scripts/evidencia.py",
88
+ "plugin/scripts/admision.py", "plugin/scripts/pairing.py",
89
+ "plugin/scripts/hall_protocol.py", "plugin/scripts/doctor.py",
90
+ "plugin/scripts/workspace.py", "plugin/scripts/avatar.py",
91
+ "plugin/scripts/crecimiento.py",
92
+ "plugin/scripts/reset.py", "plugin/scripts/capabilities.py",
93
+ "plugin/scripts/deliberations.py",
94
+ "plugin/scripts/runtime_processes.py",
95
+ "plugin/scripts/runtime_log.py", "plugin/scripts/logs.py",
96
+ "plugin/scripts/launch.py", "plugin/scripts/hook_activity.py",
97
+ "plugin/scripts/city-session.sh", "plugin/scripts/city-runtime.sh",
98
+ "plugin/scripts/find-repos.sh",
99
+ "plugin/channel/bus.js", "plugin/channel/local-hub.js",
100
+ "plugin/channel/client.js", "plugin/channel/adapter.js",
101
+ "plugin/channel/runtime-gateway.js",
102
+ "plugin/.mcp.json",
103
+ "bin/cities", "bin/road", "bin/bus", "bin/committee", "bin/agents",
104
+ "bin/reset", "bin/skills", "bin/benchmark", "bin/logs",
105
+ "benchmarks/committee/run.py",
106
+ "benchmarks/latency/live.py", "benchmarks/latency/README.md",
107
+ "benchmarks/latency/fake-native-server.mjs",
108
+ "benchmarks/latency/fake-claude-cli.mjs",
109
+ "benchmarks/stress/run.py", "benchmarks/stress/README.md",
110
+ "city/web/src/repo-roles.ts",
111
+ "demo/show.py", "demo/city/city.yml", "demo/city/ada.md",
112
+ "city/scripts/seed.py", "city/worker/schema.sql", "demo/parcels.yml"]
113
+ problemas = [f"missing: {n}" for n in necesarios if n not in dentro]
114
+ basura = ("/node_modules/", "/__pycache__/", "/.wrangler/")
115
+ for ruta in sorted(dentro):
116
+ if any(x in f"/{ruta}/" for x in basura) or ruta.endswith((".pyc", "/.DS_Store")):
117
+ problemas.append(f"development artifact shipped: {ruta}")
118
+ print("\n".join(problemas))')
119
+ if [ -z "$faltan" ]; then
120
+ printf ' %-10s \033[32mok\033[0m every path the shim needs ships in the tarball\n' "package"
121
+ else
122
+ printf ' %-10s \033[31mFAILED\033[0m — invalid npm package contents:\n' "package"
123
+ printf '%s\n' "$faltan" | sed 's/^/ /'
124
+ fallos=$((fallos + 1))
125
+ fi
126
+ fi
127
+
128
+ printf '\n'
129
+ if [ "$fallos" -gt 0 ]; then
130
+ printf ' \033[31m%d suite(s) failed\033[0m\n\n' "$fallos"
131
+ exit 1
132
+ fi
133
+ printf ' \033[32mall green\033[0m — %d checks\n\n' "$total"
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ """Knowing there is a new version, and getting it.
3
+
4
+ ./bin/test-actualiza.py
5
+
6
+ No network: the registry call is replaced, because a suite that depends on
7
+ npm being up tests npm. What is tested here is everything around it — the
8
+ version comparison (where a prerelease must sort BELOW its release, the bug
9
+ every hand-rolled semver has), the day-long cache, the opt-out, and the refusal
10
+ to `npm install -g` over somebody's git checkout.
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import sys
17
+ import tempfile
18
+ import time
19
+
20
+ AQUI = os.path.dirname(os.path.abspath(__file__))
21
+ RAIZ = os.path.dirname(AQUI)
22
+ sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
23
+ sys.path.insert(0, AQUI)
24
+
25
+ import actualiza # noqa: E402
26
+ from testlib import afirma, comprueba, resumen # noqa: E402
27
+
28
+
29
+ def comparacion():
30
+ print(" which version is newer")
31
+ for nueva, vieja in (
32
+ ("0.2.0", "0.1.0"),
33
+ ("1.0.0", "0.9.9"),
34
+ ("0.1.10", "0.1.9"),
35
+ # The one every hand-rolled comparison gets wrong: a release outranks
36
+ # its own prereleases, so 1.0.0 must beat 1.0.0-beta.22.
37
+ ("1.0.0", "1.0.0-beta.22"),
38
+ ("0.3.0-beta.22", "0.3.0-beta.21"),
39
+ ):
40
+ afirma(f"· {nueva} is newer than {vieja}", actualiza.es_mas_nueva(nueva, vieja),
41
+ f"{actualiza._partes(nueva)} vs {actualiza._partes(vieja)}")
42
+ for igual_o_vieja, actual in (("0.1.0", "0.1.0"), ("0.1.0", "0.2.0"),
43
+ ("1.0.0-beta.1", "1.0.0")):
44
+ afirma(f"· {igual_o_vieja} is NOT newer than {actual}",
45
+ not actualiza.es_mas_nueva(igual_o_vieja, actual))
46
+ afirma("· and nothing is newer than nothing", not actualiza.es_mas_nueva("", "0.1.0")
47
+ and not actualiza.es_mas_nueva("0.2.0", ""))
48
+
49
+
50
+ def cache_y_opt_out():
51
+ print(" the check: cached, quiet, and refusable")
52
+ casa = tempfile.mkdtemp()
53
+ previo = {k: os.environ.get(k) for k in ("AGENTS_CITY_HOME", "CITY_UPDATE_CHECK")}
54
+ os.environ["AGENTS_CITY_HOME"] = casa
55
+ os.environ.pop("CITY_UPDATE_CHECK", None)
56
+ llamadas = []
57
+
58
+ def registro_falso():
59
+ llamadas.append(1)
60
+ return "9.9.9"
61
+
62
+ real = actualiza.consulta_registro
63
+ actualiza.consulta_registro = lambda *a, **k: registro_falso()
64
+ try:
65
+ instalada, ultima, hay = actualiza.comprueba()
66
+ afirma("· a first check asks the registry and reports the newer version",
67
+ ultima == "9.9.9" and hay and len(llamadas) == 1, f"{instalada} {ultima} {hay}")
68
+ actualiza.comprueba()
69
+ actualiza.comprueba()
70
+ comprueba("· and the next ones read the day's answer instead of asking again",
71
+ len(llamadas), 1)
72
+ _, _, hay2 = actualiza.comprueba(forzar=True)
73
+ afirma("· --check forces a fresh ask", len(llamadas) == 2 and hay2)
74
+
75
+ # The cache is what makes a plane or a firewall silent rather than noisy.
76
+ actualiza.consulta_registro = lambda *a, **k: ""
77
+ _, ultima3, _ = actualiza.comprueba(forzar=True)
78
+ comprueba("· an unreachable registry falls back to what it knew", ultima3, "9.9.9")
79
+ guardado = json.load(open(os.path.join(casa, ".runtime", "version.json")))
80
+ afirma("· which is remembered under the runtime dir, with its timestamp",
81
+ guardado["latest"] == "9.9.9" and time.time() - guardado["when"] < 60,
82
+ str(guardado))
83
+
84
+ os.environ["CITY_UPDATE_CHECK"] = "0"
85
+ instalada, ultima4, hay4 = actualiza.comprueba(forzar=True)
86
+ afirma("· and CITY_UPDATE_CHECK=0 stops it entirely, saying nothing",
87
+ ultima4 == "" and hay4 is False and instalada, f"{instalada} {ultima4}")
88
+ comprueba("· so the passive notice stays empty too", actualiza.aviso(), "")
89
+ finally:
90
+ actualiza.consulta_registro = real
91
+ for k, v in previo.items():
92
+ if v is None:
93
+ os.environ.pop(k, None)
94
+ else:
95
+ os.environ[k] = v
96
+ shutil.rmtree(casa, ignore_errors=True)
97
+
98
+
99
+ def no_toca_un_checkout():
100
+ print(" what update refuses to do")
101
+ # This repo IS a git checkout, so the refusal is testable against itself:
102
+ # `npm install -g` over somebody's working copy is not an update.
103
+ comprueba("· a git checkout is recognised as one", actualiza.como_se_instalo(), "clone")
104
+ import io
105
+ from contextlib import redirect_stdout
106
+
107
+ salida = io.StringIO()
108
+ with redirect_stdout(salida):
109
+ codigo = actualiza.actualiza()
110
+ afirma("· and update refuses it, telling you the command that fits",
111
+ codigo == 1 and "git pull" in salida.getvalue(), salida.getvalue())
112
+
113
+
114
+ def la_puerta():
115
+ print(" the command in the front door")
116
+ front = open(os.path.join(RAIZ, "bin", "agents-city.js"), encoding="utf-8").read()
117
+ for orden in ("update", "doctor"):
118
+ afirma(f"· `agents-city {orden}` is listed and points at one implementation",
119
+ f"{orden}: {{ que: ['bin/{orden}']" in front, "")
120
+ for guion in ("update", "doctor"):
121
+ texto = open(os.path.join(RAIZ, "bin", guion), encoding="utf-8").read()
122
+ afirma(f"· bin/{guion} holds no logic of its own",
123
+ ".py" in texto and "def " not in texto, texto)
124
+
125
+
126
+ comparacion()
127
+ cache_y_opt_out()
128
+ no_toca_un_checkout()
129
+ la_puerta()
130
+ sys.exit(resumen("actualiza"))
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """Explicit unknown-CLI fallback remains safe, slow and visibly separate."""
3
+ import json
4
+ import datetime
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import time
11
+
12
+ AQUI = os.path.dirname(os.path.abspath(__file__))
13
+ RAIZ = os.path.dirname(AQUI)
14
+ sys.path.insert(0, AQUI)
15
+ from testlib import ( # noqa: E402
16
+ afirma, comprueba, detiene_hubs_de_ciudad, hubs_de_ciudad, resumen,
17
+ )
18
+
19
+ ADAPTER = os.path.join(RAIZ, 'plugin', 'channel', 'adapter.js')
20
+ CLIENT = os.path.join(RAIZ, 'plugin', 'channel', 'client.js')
21
+ FAKE_TUI = os.path.join(RAIZ, 'benchmarks', 'latency', 'fake-tui.py')
22
+ RUNTIMES = {
23
+ 'legacy-one': 'custom-tui',
24
+ 'legacy-two': 'custom-tui',
25
+ 'legacy-three': 'custom-tui',
26
+ 'legacy-four': 'custom-tui',
27
+ }
28
+
29
+
30
+ def espera(condicion, segundos=8):
31
+ limite = time.monotonic() + segundos
32
+ while time.monotonic() < limite:
33
+ if condicion():
34
+ return True
35
+ time.sleep(.1)
36
+ return False
37
+
38
+
39
+ def main():
40
+ print('\n explicit terminal fallback for unknown CLIs')
41
+ if not shutil.which('tmux'):
42
+ afirma('· tmux is installed', False, 'tmux missing')
43
+ return resumen('adapter')
44
+ base = tempfile.mkdtemp(prefix='agents-city-adapter-')
45
+ app = os.path.join(base, 'app')
46
+ city = os.path.join(app, 'alice', 'home')
47
+ os.makedirs(city)
48
+ repos = list(RUNTIMES)
49
+ open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
50
+ 'id: city_adapter\nname: Home\nslug: home\nowner: alice\n')
51
+ open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
52
+ '{"version": 1, "roads": []}\n')
53
+ runs = ''.join(f'runs.{actor}: {runtime}\n'
54
+ for actor, runtime in RUNTIMES.items())
55
+ roles = ''.join(f'role.{actor}: {"seo" if actor == "legacy-two" else "blank"}\n'
56
+ for actor in RUNTIMES)
57
+ open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
58
+ f'---\nuser: alice\nagent: alice/ceo\nrepos: [{", ".join(repos)}]\n'
59
+ f'{roles}{runs}---\n')
60
+ env = dict(os.environ, AGENTS_CITY_HOME=app, AGENTS_CITY_DATA=city,
61
+ AGENTS_CITY_USER='alice')
62
+ for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
63
+ env.pop(key, None)
64
+ session = f'ac-adapter-{os.getpid()}'
65
+ adapters = []
66
+ endpoint = os.path.join(app, '.runtime', 'bus', 'city-adapter', 'endpoint.json')
67
+ hub_pid = 0
68
+ try:
69
+ for actor in repos:
70
+ capture = os.path.join(base, f'{actor}.txt')
71
+ subprocess.run(
72
+ ['tmux', 'new-session' if actor == repos[0] else 'new-window',
73
+ '-d' if actor == repos[0] else '-d',
74
+ '-s' if actor == repos[0] else '-t',
75
+ session if actor == repos[0] else session,
76
+ '-n', actor, sys.executable, FAKE_TUI, capture,
77
+ '--settle-ms', '120'],
78
+ check=True, capture_output=True, text=True)
79
+ for actor, runtime in RUNTIMES.items():
80
+ process = subprocess.Popen(
81
+ ['node', ADAPTER, '--data', city, '--actor', actor,
82
+ '--target', f'{session}:{actor}', '--runtime', runtime],
83
+ env=dict(env, CITY_BUS_ACTOR=actor, CITY_AGENT_RUNTIME=runtime,
84
+ CITY_TERMINAL_READY_DELAY_MS='40',
85
+ CITY_TERMINAL_SUBMIT_DELAY_MS='180'),
86
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
87
+ adapters.append(process)
88
+ afirma('· all four explicit fallback adapters authenticate',
89
+ espera(lambda: all(p.poll() is None for p in adapters), .5))
90
+
91
+ payload = {
92
+ 'question': 'Which runtime received this?',
93
+ 'desiredOutcome': 'The same authenticated brief everywhere',
94
+ 'definitionOfDone': ['every capture has the protocol prompt'],
95
+ 'participants': repos,
96
+ }
97
+ opened = subprocess.run(
98
+ ['node', CLIENT, 'committee', 'open', '--input', '-'],
99
+ input=json.dumps(payload), capture_output=True, text=True,
100
+ env=dict(env, CITY_BUS_ACTOR='seat'), timeout=12)
101
+ state = json.loads(opened.stdout) if opened.returncode == 0 else {}
102
+ thread = state.get('id', '')
103
+ afirma('· the chair emits one typed assignment per selected runtime',
104
+ bool(thread), opened.stderr.strip())
105
+
106
+ for actor, runtime in RUNTIMES.items():
107
+ capture = os.path.join(base, f'{actor}.txt')
108
+ delivered = espera(
109
+ lambda p=capture: os.path.exists(p) and bool(
110
+ open(p, encoding='utf-8', errors='ignore').read().strip()))
111
+ records = [json.loads(line) for line in open(
112
+ capture, encoding='utf-8', errors='ignore') if line.strip()
113
+ ] if os.path.exists(capture) else []
114
+ body = records[-1]['body'] if records else ''
115
+ afirma(f'· {runtime} receives the same chaired assignment',
116
+ delivered and thread in body
117
+ and 'agents-city committee respond' in body
118
+ and 'Do not contact another repo agent directly' in body,
119
+ body[-160:])
120
+ esperado = (
121
+ 'operating role is seo'
122
+ if actor == 'legacy-two'
123
+ else 'operating role is blank'
124
+ )
125
+ afirma(
126
+ f'· {runtime} receives its own operating role',
127
+ esperado in body,
128
+ body[-300:],
129
+ )
130
+
131
+ outbox = os.path.join(app, '.runtime', 'bus', 'city-adapter', 'outbox')
132
+ drained = espera(
133
+ lambda: all(not os.path.isdir(os.path.join(outbox, actor))
134
+ or not os.listdir(os.path.join(outbox, actor))
135
+ for actor in repos))
136
+ afirma('· delivery acknowledgements drain every durable actor outbox', drained)
137
+ metrics_path = os.path.join(
138
+ app, '.runtime', 'bus', 'city-adapter', 'delivery-latency.jsonl')
139
+ metrics = [json.loads(line) for line in open(metrics_path, encoding='utf-8')
140
+ if line.strip()] if os.path.exists(metrics_path) else []
141
+ actors = {row.get('actor') for row in metrics}
142
+ afirma('· every accepted prompt has an auditable latency trace',
143
+ actors == set(repos), str(actors))
144
+ afirma('· Enter is delayed until each multiline paste has settled',
145
+ metrics and all(row.get('pasteToSubmitMs', 0) >= 150
146
+ for row in metrics), str(metrics))
147
+ submitted = [row['submittedAt'] for row in metrics]
148
+ instants = [datetime.datetime.fromisoformat(
149
+ value.replace('Z', '+00:00')).timestamp() for value in submitted]
150
+ spread = max(instants) - min(instants) if instants else 99
151
+ afirma('· four terminal fallbacks submit in parallel, not one after another',
152
+ spread < 1.0, f'{spread:.3f}s')
153
+ data = json.load(open(endpoint, encoding='utf-8'))
154
+ hub_pid = int(data.get('pid', 0))
155
+ comprueba('· all runtimes used one city WebSocket endpoint',
156
+ data.get('protocol'), 'agents-city-bus/2')
157
+ comprueba('· concurrent adapter startup creates exactly one city hub',
158
+ len(hubs_de_ciudad(city)), 1)
159
+ finally:
160
+ for process in adapters:
161
+ if process.poll() is None:
162
+ process.terminate()
163
+ for process in adapters:
164
+ try:
165
+ process.wait(timeout=2)
166
+ except subprocess.TimeoutExpired:
167
+ process.kill()
168
+ subprocess.run(['tmux', 'kill-session', '-t', session],
169
+ capture_output=True)
170
+ if os.path.exists(endpoint):
171
+ try:
172
+ data = json.load(open(endpoint, encoding='utf-8'))
173
+ hub_pid = int(data['pid'])
174
+ except (OSError, ValueError, KeyError, json.JSONDecodeError):
175
+ pass
176
+ limpio = detiene_hubs_de_ciudad(city)
177
+ afirma('· fallback cleanup leaves no orphan city hub', limpio, str(hub_pid))
178
+ shutil.rmtree(base, ignore_errors=True)
179
+ return resumen('adapter')
180
+
181
+
182
+ if __name__ == '__main__':
183
+ sys.exit(main())
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """Admission as an ordered gate graph: the decisive gate, the reason, redaction.
3
+
4
+ Pure and transport-free. The cases that matter: the first blocker is the
5
+ decisive gate; a road that matches but whose sender does not is an explicit
6
+ block, not a fallthrough; an unknown sender under pairing yields a pairing
7
+ requirement rather than a silent drop; and diagnostics never carry a raw
8
+ allowlist entry.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+
14
+ AQUI = os.path.dirname(os.path.abspath(__file__))
15
+ RAIZ = os.path.dirname(AQUI)
16
+ sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
17
+ sys.path.insert(0, AQUI)
18
+
19
+ import admision as ad # noqa: E402
20
+ from testlib import afirma, comprueba, resumen # noqa: E402
21
+
22
+ FELIZ = dict(road_existe=True, road_incidente=False, direccion_coincide=True,
23
+ emparejado=False, allowlist=["alice/home"], payload_valido=True,
24
+ texto_no_vacio=True)
25
+
26
+
27
+ def dispatch():
28
+ d = ad.decide_entrada(**FELIZ)
29
+ afirma("a fully valid inbound dispatches", d.permitido())
30
+ comprueba("its reason is road_allowed", d.razon, "road_allowed")
31
+ afirma("the graph records every phase it walked", len(d.grafo) >= 3)
32
+ afirma("a decision exposes no evidence field to mistake for authorization",
33
+ not hasattr(d, "evidencia") and "evidence" not in d.como_dict())
34
+
35
+
36
+ def bloqueos():
37
+ d = ad.decide_entrada(**{**FELIZ, "road_existe": False})
38
+ comprueba("no road -> skip", d.admision, ad.Admision.OMITE)
39
+ comprueba("decisive gate is the road gate", d.puerta_decisiva, "road")
40
+ comprueba("reason is road_missing", d.razon, "road_missing")
41
+
42
+ d = ad.decide_entrada(**{**FELIZ, "road_incidente": True})
43
+ comprueba("a road under incident drops", d.admision, ad.Admision.DESCARTA)
44
+ afirma("a dropped decision never reports itself as permitted", not d.permitido())
45
+
46
+ d = ad.decide_entrada(**{**FELIZ, "dirigido_aqui": False})
47
+ comprueba("a message not addressed here is skipped", d.admision, ad.Admision.OMITE)
48
+ comprueba("and address is the decisive gate", d.puerta_decisiva, "address")
49
+
50
+ d = ad.decide_entrada(**{**FELIZ, "payload_valido": False})
51
+ comprueba("a malformed payload drops", d.admision, ad.Admision.DESCARTA)
52
+ d = ad.decide_entrada(**{**FELIZ, "texto_no_vacio": False})
53
+ comprueba("empty text drops", d.razon, "payload_empty")
54
+
55
+
56
+ def emparejamiento():
57
+ base = {**FELIZ, "direccion_coincide": False}
58
+ d = ad.decide_entrada(**{**base, "emparejado": False})
59
+ comprueba("an unknown sender under pairing asks to pair, not drops",
60
+ d.admision, ad.Admision.EMPAREJA)
61
+ comprueba("reason is sender_not_paired", d.razon, "sender_not_paired")
62
+ d = ad.decide_entrada(**{**base, "emparejado": True})
63
+ afirma("a paired-but-not-in-config sender dispatches", d.permitido())
64
+ afirma("and the pairing fact is recorded as a gate in the graph",
65
+ any(g.razon == "sender_paired" for g in d.grafo))
66
+
67
+
68
+ def orden_y_redaccion():
69
+ # Both the road and the payload are bad; the earlier (road) gate must win.
70
+ d = ad.decide_entrada(**{**FELIZ, "road_existe": False, "payload_valido": False})
71
+ comprueba("the FIRST blocker is decisive, not the last", d.puerta_decisiva, "road")
72
+
73
+ d = ad.decide_entrada(**{**FELIZ, "allowlist": ["alice/home", "bob/research"]})
74
+ diag = next(g.diagnostico for g in d.grafo if g.diagnostico)
75
+ comprueba("the diagnostic reveals the count", diag["n"], 2)
76
+ afirma("but never a raw allowlist entry",
77
+ "alice/home" not in str(diag) and "bob/research" not in str(diag))
78
+ afirma("opaque ids are stable and prefixed",
79
+ all(i.startswith("x") for i in diag["ids"]))
80
+ comprueba("the same entry hashes to the same opaque id",
81
+ ad.id_opaco("alice/home"), ad.id_opaco("alice/home"))
82
+
83
+
84
+ def contrato_razones():
85
+ try:
86
+ ad.Puerta("x", "route", False, "not_a_real_reason")
87
+ afirma("a reason outside the closed set is rejected", False)
88
+ except ValueError:
89
+ afirma("a reason outside the closed set is rejected", True)
90
+ afirma("como_dict is JSON-shaped for the wire",
91
+ set(ad.decide_entrada(**FELIZ).como_dict()) >= {"admission", "reasonCode", "gates"})
92
+
93
+
94
+ dispatch()
95
+ bloqueos()
96
+ emparejamiento()
97
+ orden_y_redaccion()
98
+ contrato_razones()
99
+ sys.exit(resumen("admision"))