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,544 @@
1
+ #!/usr/bin/env python3
2
+ """The agreements between doors — the class of bug the other suites cannot see.
3
+
4
+ ./bin/test-contracts.py
5
+
6
+ Every bug that survived 290 checks had the same shape: two pieces of code holding
7
+ the same fact, each correct alone, never confronted with each other. The session
8
+ asked for `agente` while every card said `agent`. The reporter read `path` where
9
+ every writer wrote `ruta`. The seat held eight of the wizard's twenty-one
10
+ suffixes. The seat wrote a city to `~/.agents-city` while the plugin looked in
11
+ `~/agents-city-data`. Module suites pass all of those, because each side is
12
+ internally consistent — the disagreement lives between them.
13
+
14
+ So this suite tests nothing about any module's inside. It only asserts
15
+ agreements: two resolvers land on the same folder, two languages resolve the
16
+ same key the same way, every writer's output satisfies the shared reader, every
17
+ fact with one owner has exactly one definition, and every path a command tells
18
+ an agent to run actually exists — the phantom `units.py --push` class.
19
+
20
+ When one of these fails, the fix is never "make the test pass": it is to decide
21
+ which side owns the fact and delete the other.
22
+ """
23
+ import glob
24
+ import importlib.machinery as mach
25
+ import importlib.util as iu
26
+ import json
27
+ import os
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ import sys
32
+ import tempfile
33
+
34
+ AQUI = os.path.dirname(os.path.abspath(__file__))
35
+ RAIZ = os.path.dirname(AQUI)
36
+ sys.path.insert(0, os.path.join(RAIZ, 'plugin', 'scripts'))
37
+ sys.path.insert(0, AQUI)
38
+ from testlib import comprueba, afirma, resumen # noqa: E402
39
+ import card
40
+ import parcels
41
+ import roles
42
+ import units # noqa: E402
43
+
44
+
45
+ def carga(nombre, ruta):
46
+ s = iu.spec_from_loader(nombre, mach.SourceFileLoader(nombre, ruta))
47
+ m = iu.module_from_spec(s)
48
+ s.loader.exec_module(m)
49
+ return m
50
+
51
+
52
+ # ══ the two resolvers of "where do the cards live" ══════════════════════════
53
+ def resolvedores():
54
+ print(' where the cards live: four resolvers, one answer')
55
+
56
+ # Run each side in a subprocess with a controlled HOME, because this is about
57
+ # what a fresh machine resolves — not what this one has lying around.
58
+ def py(codigo, casa, entorno=None):
59
+ env = {k: v for k, v in os.environ.items()
60
+ if k not in ('AGENTS_CITY_DATA', 'AGENTS_CITY_HOME',
61
+ 'AGENTS_CITY_USER', 'CITY_DIR')}
62
+ env['HOME'] = casa
63
+ env['AGENTS_CITY_USER'] = 'alice'
64
+ env.update(entorno or {})
65
+ r = subprocess.run([sys.executable, '-c', codigo],
66
+ capture_output=True, text=True, env=env)
67
+ return r.stdout.strip()
68
+
69
+ seatDatos = (f"import sys; sys.path.insert(0, {os.path.join(RAIZ, 'plugin', 'scripts')!r})\n"
70
+ "import importlib.machinery as m, importlib.util as u\n"
71
+ "s = u.spec_from_loader('seat', m.SourceFileLoader('seat', "
72
+ f"{os.path.join(RAIZ, 'plugin', 'scripts', 'seat.py')!r}))\n"
73
+ "mod = u.module_from_spec(s); s.loader.exec_module(mod)\n"
74
+ "print(mod.donde_viven_las_fichas())")
75
+ envDatos = (f"import sys; sys.path.insert(0, {os.path.join(RAIZ, 'plugin', 'scripts')!r})\n"
76
+ "import city_env; print(city_env.datos())")
77
+ shDatos = (f'. {os.path.join(RAIZ, "plugin", "scripts", "city-env.sh")}; '
78
+ 'printf "%s" "$AGENTS_CITY_DATA"')
79
+
80
+ def sh(casa, entorno=None):
81
+ env = {k: v for k, v in os.environ.items()
82
+ if k not in ('AGENTS_CITY_DATA', 'AGENTS_CITY_HOME',
83
+ 'AGENTS_CITY_USER', 'CITY_DIR')}
84
+ env['HOME'] = casa
85
+ env['AGENTS_CITY_USER'] = 'alice'
86
+ env.update(entorno or {})
87
+ r = subprocess.run(['/bin/bash', '-c', shDatos],
88
+ capture_output=True, text=True, env=env)
89
+ return r.stdout.strip()
90
+
91
+ # A machine with nothing: everyone lands on the seat's own folder.
92
+ casa = tempfile.mkdtemp()
93
+ espera = os.path.realpath(os.path.join(casa, '.agents-city', 'alice', 'home'))
94
+ comprueba('· blank machine — the seat', py(seatDatos, casa), espera)
95
+ comprueba('· blank machine — the plugin (python)', py(envDatos, casa), espera)
96
+ comprueba('· blank machine — the hooks (shell)', sh(casa), espera)
97
+
98
+ # A legacy team-looking folder is no longer magical: personal city selection
99
+ # remains user/home unless explicitly requested.
100
+ os.makedirs(os.path.join(casa, 'agents-city-data'))
101
+ comprueba('· unrelated team folder — the seat', py(seatDatos, casa), espera)
102
+ comprueba('· unrelated team folder — the plugin', py(envDatos, casa), espera)
103
+ comprueba('· unrelated team folder — the hooks', sh(casa), espera)
104
+
105
+ # An explicit setting beats everything, everywhere. It has to point at a real
106
+ # directory for the seat (which validates), so make one.
107
+ puesto = os.path.realpath(os.path.join(casa, 'elegido'))
108
+ os.makedirs(puesto)
109
+ e = {'AGENTS_CITY_DATA': puesto}
110
+ comprueba('· explicit setting — the seat', py(seatDatos, casa, e), puesto)
111
+ comprueba('· explicit setting — the plugin', py(envDatos, casa, e), puesto)
112
+ comprueba('· explicit setting — the hooks', sh(casa, e), puesto)
113
+ shutil.rmtree(casa)
114
+
115
+
116
+ # ══ every writer satisfies the one reader ═══════════════════════════════════
117
+ def escritores():
118
+ print(' every writer against the shared reader')
119
+ d = tempfile.mkdtemp()
120
+
121
+ # units.yml has one writer module and two callers that used to inline it.
122
+ import setup as W
123
+ W.escribe({'destino': d, 'unidades': [{'id': 'x', 'name': 'X', 'color': 'aabbcc'}],
124
+ 'roles': ['cpto'], 'repos': [], 'gente': [], 'org': '', 'rutas': {},
125
+ 'kind': 'product', 'grow_cmd': ''})
126
+ ids = {u['id'] for u in units.lee(os.path.join(d, 'units.yml'))}
127
+ comprueba("· the wizard's units.yml reads back, specials included",
128
+ ids, {'x', 'lab', 'none'})
129
+
130
+ seat = carga('seat', os.path.join(RAIZ, 'plugin', 'scripts', 'seat.py'))
131
+ d2 = tempfile.mkdtemp()
132
+ seat.escribe_suelo(d2, ['api'])
133
+ comprueba("· the seat's units.yml reads back the same way",
134
+ {u['id'] for u in units.lee(os.path.join(d2, 'units.yml'))},
135
+ {'mine', 'lab', 'none'})
136
+ ps, lab, raras = parcels.lee(os.path.join(d2, 'parcels.yml'))
137
+ comprueba("· and the seat's parcels.yml satisfies the shared reader",
138
+ ([p['id'] for p in ps], lab, raras), (['api'], set(), []))
139
+
140
+ # The wizard's parcels.yml too.
141
+ ps, _, raras = parcels.lee(os.path.join(d, 'parcels.yml'))
142
+ comprueba("· the wizard's parcels.yml too", raras, [])
143
+ shutil.rmtree(d)
144
+ shutil.rmtree(d2)
145
+
146
+
147
+ # ══ facts with one owner have one definition ═════════════════════════════════
148
+ def una_definicion():
149
+ print(' one fact, one definition')
150
+
151
+ fuentes = []
152
+ for base in ('bin', 'plugin/scripts', 'city/scripts', 'demo'):
153
+ for f in glob.glob(os.path.join(RAIZ, base, '*')):
154
+ if not os.path.isfile(f):
155
+ continue
156
+ es_py = f.endswith('.py') or open(f, 'rb').read(24).startswith(
157
+ b'#!/usr/bin/env python')
158
+ if es_py:
159
+ fuentes.append(f)
160
+
161
+ def definiciones(patron, dueno):
162
+ fuera = []
163
+ for f in fuentes:
164
+ if os.path.basename(f).startswith('test'):
165
+ continue
166
+ for i, l in enumerate(open(f, encoding='utf-8').readlines(), 1):
167
+ if re.search(patron, l) and not l.strip().startswith('#'):
168
+ rel = os.path.relpath(f, RAIZ)
169
+ if rel != dueno:
170
+ fuera.append(f'{rel}:{i}')
171
+ return fuera
172
+
173
+ for patron, dueno, que in (
174
+ (r"ARQUITECTOS\s*=\s*\{", 'plugin/scripts/roles.py', 'who sets the goals'),
175
+ (r"SUFIJOS\s*=\s*\{|AGENTE\s*=\s*\{'cpto'", 'plugin/scripts/roles.py',
176
+ 'the agent suffix per role'),
177
+ (r"f'\{repo\}:\{ruta\}'", 'plugin/scripts/parcels.py', "a parcel's id"),
178
+ (r"def usuario_de_correo", 'plugin/scripts/gh.py', 'email → username'),
179
+ (r"def bloque_objetivo", 'plugin/scripts/card.py', "a goal's card lines"),
180
+ ):
181
+ extras = definiciones(patron, dueno)
182
+ afirma(f'· {que} is defined only in {os.path.basename(dueno)}',
183
+ not extras, 'also in: ' + ', '.join(extras))
184
+
185
+
186
+ # ══ what the commands tell an agent to run must exist ════════════════════════
187
+ def rutas_reales():
188
+ print(' every path a command names exists')
189
+ docs = (glob.glob(os.path.join(RAIZ, 'plugin', 'commands', '*.md'))
190
+ + glob.glob(os.path.join(RAIZ, 'plugin', 'skills', '*', 'SKILL.md')))
191
+ patron = re.compile(r'(?:[a-z]+/)*(?:scripts|bin)/[a-zA-Z0-9._-]+\.(?:py|sh)')
192
+ for doc in docs:
193
+ for ref in set(patron.findall(open(doc, encoding='utf-8').read())):
194
+ base = os.path.basename(ref)
195
+ hay = (glob.glob(os.path.join(RAIZ, '**', base), recursive=False)
196
+ or glob.glob(os.path.join(RAIZ, '*', 'scripts', base))
197
+ or glob.glob(os.path.join(RAIZ, 'bin', base))
198
+ or glob.glob(os.path.join(RAIZ, 'plugin', 'scripts', base)))
199
+ afirma(f'· {os.path.relpath(doc, RAIZ)} → {ref}',
200
+ bool(hay), 'names a file that does not exist — the phantom '
201
+ '`units.py --push` class')
202
+
203
+ # The hooks configuration too: a hook pointing nowhere fails on every turn.
204
+ hooks = json.load(open(os.path.join(RAIZ, 'plugin', 'hooks', 'hooks.json')))
205
+ texto = json.dumps(hooks)
206
+ for m in set(re.findall(r'[\w${}/.-]*/((?:[\w-]+)\.(?:sh|py))', texto)):
207
+ afirma(f'· hooks.json → {m}',
208
+ os.path.isfile(os.path.join(RAIZ, 'plugin', 'hooks', m))
209
+ or os.path.isfile(os.path.join(RAIZ, 'plugin', 'scripts', m)), '')
210
+
211
+
212
+ # ══ the conscience stays inside the city ═════════════════════════════════════
213
+ def conciencia_acotada():
214
+ """Installing the plugin must not enrol every Claude session on the machine.
215
+
216
+ Happy: inside a city runtime (CITY_BUS_ACTOR set) the guard lets the hook
217
+ run; CITY_HOOKS=everywhere restores the machine-wide behaviour explicitly.
218
+ Non-happy: a plain session — no identity, no opt-in — gets `{}` and silence
219
+ from every hook, and no file appears anywhere.
220
+ """
221
+ print(' the conscience stays inside the city')
222
+ guardia = os.path.join(RAIZ, 'plugin', 'hooks', 'solo-en-ciudad.sh')
223
+
224
+ def corre(entorno, script=None, marca='; echo despues'):
225
+ casa = tempfile.mkdtemp()
226
+ env = {'HOME': casa, 'PATH': os.environ.get('PATH', ''),
227
+ 'CLAUDE_PLUGIN_ROOT': os.path.join(RAIZ, 'plugin')}
228
+ env.update(entorno)
229
+ orden = f'. {guardia}{marca}' if script is None else None
230
+ r = subprocess.run(['/bin/bash', '-c', orden] if orden else ['/bin/bash', script],
231
+ input='{}', capture_output=True, text=True, env=env)
232
+ residuos = os.listdir(casa)
233
+ shutil.rmtree(casa, ignore_errors=True)
234
+ return r, residuos
235
+
236
+ r, residuos = corre({})
237
+ afirma('· non-happy: outside a city the guard answers {} and stops',
238
+ r.returncode == 0 and r.stdout.strip() == '{}' and not residuos, r.stdout)
239
+ r, _ = corre({'CITY_BUS_ACTOR': 'nova'})
240
+ afirma('· happy: a city runtime passes the guard',
241
+ r.returncode == 0 and 'despues' in r.stdout, r.stdout)
242
+ r, _ = corre({'CITY_HOOKS': 'everywhere'})
243
+ afirma('· happy: CITY_HOOKS=everywhere is the explicit machine-wide opt-in',
244
+ r.returncode == 0 and 'despues' in r.stdout, r.stdout)
245
+ # The opt-in also reads $CITY_DIR/.env, where the transport settings live.
246
+ casa = tempfile.mkdtemp()
247
+ canal = os.path.join(casa, 'canal')
248
+ os.makedirs(canal)
249
+ open(os.path.join(canal, '.env'), 'w').write('CITY_HOOKS=everywhere\n')
250
+ r = subprocess.run(['/bin/bash', '-c', f'. {guardia}; echo despues'],
251
+ input='{}', capture_output=True, text=True,
252
+ env={'HOME': casa, 'PATH': os.environ.get('PATH', ''),
253
+ 'CITY_DIR': canal})
254
+ shutil.rmtree(casa, ignore_errors=True)
255
+ afirma('· happy: the opt-in can live in $CITY_DIR/.env',
256
+ r.returncode == 0 and 'despues' in r.stdout, r.stdout)
257
+
258
+ # Every hook wires the guard, and outside a city every hook is silent.
259
+ for nombre in sorted(glob.glob(os.path.join(RAIZ, 'plugin', 'hooks', '*.sh'))):
260
+ base = os.path.basename(nombre)
261
+ if base == 'solo-en-ciudad.sh':
262
+ continue
263
+ afirma(f'· {base} sources the guard',
264
+ 'solo-en-ciudad.sh' in open(nombre, encoding='utf-8').read(), '')
265
+ r, residuos = corre({}, script=nombre)
266
+ afirma(f'· non-happy: {base} is mute outside a city',
267
+ r.returncode == 0 and r.stdout.strip() == '{}' and not residuos,
268
+ f'{r.stdout!r} {residuos}')
269
+
270
+
271
+ # ══ the demo is a city this code would write ═════════════════════════════════
272
+ def demo_coherente():
273
+ print(' the demo agrees with the code')
274
+ for f in sorted(glob.glob(os.path.join(RAIZ, 'demo', '*.md'))):
275
+ c = card.lee(f)
276
+ if not c.get('user'):
277
+ continue
278
+ comprueba(f"· {c['user']}'s agent is what roles.py would mint",
279
+ c['agent'], f"{c['user']}/{roles.sufijo(c['role'])}")
280
+ # And every role a demo card claims has a role file for a round to read.
281
+ for f in sorted(glob.glob(os.path.join(RAIZ, 'demo', '*.md'))):
282
+ c = card.lee(f)
283
+ if c.get('user'):
284
+ afirma(f"· role file exists for {c['role']}",
285
+ os.path.isfile(os.path.join(RAIZ, 'plugin', 'roles', 'examples',
286
+ f"{c['role']}.md")), '')
287
+
288
+
289
+ # ══ several cities, one machine ══════════════════════════════════════════════
290
+ def varias_ciudades():
291
+ print(' several cities on one machine')
292
+ import cities
293
+
294
+ casa = tempfile.mkdtemp()
295
+ # Two cities, one of them named, plus the solo default.
296
+ rankia = os.path.join(casa, 'rankia-data')
297
+ os.makedirs(rankia)
298
+ open(os.path.join(rankia, 'city.yml'), 'w').write('kind: product\nname: Rankia\n')
299
+ open(os.path.join(rankia, 'units.yml'), 'w').write('units:\n')
300
+ cliente = os.path.join(casa, 'acme')
301
+ os.makedirs(cliente)
302
+ open(os.path.join(cliente, 'units.yml'), 'w').write('units:\n')
303
+
304
+ comprueba('· even the first city carries owner and city in its session',
305
+ cities.sesion('jl', os.path.expanduser('~/.agents-city')), 'jl-agents-city')
306
+ comprueba('· a named city suffixes it, so two never share one session',
307
+ cities.sesion('jl', rankia), 'jl-rankia')
308
+ afirma('· two folders sharing a basename get different state slugs',
309
+ cities.slug(rankia) != cities.slug(cliente))
310
+
311
+ # The shell delegates to the same code — pin the delegation, not a copy.
312
+ cli = subprocess.run([sys.executable,
313
+ os.path.join(RAIZ, 'plugin', 'scripts', 'cities.py'),
314
+ 'sesion', 'jl', rankia],
315
+ capture_output=True, text=True).stdout.strip()
316
+ comprueba('· the tmux script (via the CLI) agrees with the module',
317
+ cli, cities.sesion('jl', rankia))
318
+ afirma('· and city-session.sh actually delegates instead of copying the rule',
319
+ 'cities.py" sesion' in open(os.path.join(
320
+ RAIZ, 'plugin', 'scripts', 'city-session.sh')).read())
321
+
322
+ comprueba('· a name resolves to its folder',
323
+ cities.resuelve(rankia), os.path.realpath(rankia))
324
+ comprueba('· and an unknown name resolves to nothing, not a guess',
325
+ cities.resuelve('no-such-city-xyz'), '')
326
+ afirma('· a random folder is not a city and cannot be registered',
327
+ not cities.es_ciudad(tempfile.mkdtemp()))
328
+ shutil.rmtree(casa)
329
+
330
+
331
+ def ventanas_gemelas():
332
+ """Window, engine key and bus actor share one canonical repo slug."""
333
+ print(' the window slug has one owner')
334
+
335
+ sesion = open(os.path.join(RAIZ, 'plugin/scripts/city-session.sh'),
336
+ encoding='utf-8').read()
337
+ afirma('· city-session delegates instead of copying the slug rule',
338
+ 'python3 "$LEER" --window "$r"' in sesion)
339
+ lector = os.path.join(RAIZ, 'plugin', 'scripts', 'read-card.py')
340
+ for nombre in ('MiApp@feature/X', 'Data_Pipeline', 'API', 'a/b_c@d',
341
+ 'web.app', 'two words', 'plain'):
342
+ concha = subprocess.run(
343
+ [sys.executable, lector, '--window', nombre],
344
+ capture_output=True, text=True).stdout.strip()
345
+ comprueba(f'· {nombre!r} has the same slug through both doors',
346
+ card.ventana(nombre), concha)
347
+
348
+
349
+ def canal_compilado():
350
+ """Every vendor-neutral bus entry point ships as a committed bundle."""
351
+ print(' the channel artifacts match their sources')
352
+
353
+ ts = open(os.path.join(RAIZ, 'plugin/channel/bus.ts'), encoding='utf-8').read()
354
+ js = open(os.path.join(RAIZ, 'plugin/channel/bus.js'), encoding='utf-8').read()
355
+ tools = re.findall(r"name:\s*'(bus_[a-z_]+)'", ts)
356
+
357
+ afirma('· bus.ts declares at least the send/roster/inbox trio',
358
+ {'bus_send', 'bus_roster', 'bus_inbox'} <= set(tools),
359
+ f'found only: {sorted(set(tools))}')
360
+ faltan = sorted({t for t in tools if t not in js})
361
+ afirma('· every tool in bus.ts exists in the committed bus.js',
362
+ not faltan, 'stale artifact, missing: ' + ', '.join(faltan)
363
+ + ' — rebuild: cd plugin/channel && npm run build')
364
+ marcadores = {
365
+ 'local-hub.js': ('agents-city-bus/2', 'committee.open'),
366
+ 'client.js': ('committee.open', 'road.send'),
367
+ 'adapter.js': ('Agents City authenticated local bus', 'tmux'),
368
+ }
369
+ for nombre, esperados in marcadores.items():
370
+ ruta = os.path.join(RAIZ, 'plugin', 'channel', nombre)
371
+ contenido = open(ruta, encoding='utf-8').read() if os.path.isfile(ruta) else ''
372
+ afirma(f'· {nombre} is built and carries its contract',
373
+ all(valor in contenido for valor in esperados),
374
+ f'missing marker in {nombre}; run npm run build')
375
+
376
+
377
+ def plugin_canal():
378
+ """Claude must discover the same MCP server named by the Channel."""
379
+ print(' the installed Claude Channel contract')
380
+ manifest_path = os.path.join(RAIZ, 'plugin', '.claude-plugin', 'plugin.json')
381
+ package_path = os.path.join(RAIZ, 'package.json')
382
+ marketplace_path = os.path.join(RAIZ, '.claude-plugin', 'marketplace.json')
383
+ mcp_path = os.path.join(RAIZ, 'plugin', '.mcp.json')
384
+ manifest = json.load(open(manifest_path, encoding='utf-8'))
385
+ package = json.load(open(package_path, encoding='utf-8'))
386
+ marketplace = json.load(open(marketplace_path, encoding='utf-8'))
387
+ marketplace_plugin = next(
388
+ (plugin for plugin in marketplace.get('plugins', [])
389
+ if plugin.get('name') == manifest.get('name')),
390
+ {},
391
+ )
392
+ afirma('· package, plugin and marketplace publish one release version',
393
+ package.get('version') == manifest.get('version')
394
+ == marketplace_plugin.get('version'),
395
+ f"package={package.get('version')!r}, plugin={manifest.get('version')!r}, "
396
+ f"marketplace={marketplace_plugin.get('version')!r}")
397
+ afirma('· the plugin ships its MCP registry at the plugin root',
398
+ os.path.isfile(mcp_path),
399
+ 'Claude can load skills while reporting MCP servers (0) without plugin/.mcp.json')
400
+ mcp = json.load(open(mcp_path, encoding='utf-8')) if os.path.isfile(mcp_path) else {}
401
+ servers = mcp.get('mcpServers', {})
402
+ channels = manifest.get('channels', [])
403
+ afirma('· the Channel names a server Claude can actually discover',
404
+ len(channels) == 1 and channels[0].get('server') in servers,
405
+ f'channels={channels!r}, servers={sorted(servers)!r}')
406
+ city_bus = servers.get('city-bus', {})
407
+ comprueba('· the discovered server executes the production launcher',
408
+ city_bus.get('command'), '${CLAUDE_PLUGIN_ROOT}/channel/run.sh')
409
+ afirma('· plugin options still reach the MCP subprocess',
410
+ city_bus.get('env', {}).get('AGENTS_CITY_DATA_DEFAULT')
411
+ == '${user_config.data_repo}'
412
+ and city_bus.get('env', {}).get('CITY_BUS_TOKEN')
413
+ == '${user_config.bus_token}',
414
+ str(city_bus.get('env', {})))
415
+ afirma('· there is only one MCP registry to keep in sync',
416
+ 'mcpServers' not in manifest,
417
+ 'remove the duplicate inline registry from plugin.json')
418
+
419
+
420
+ def puerta_npm():
421
+ """Every npm command remains visually distinct in the public help."""
422
+ print(' the npm front door')
423
+ salida = subprocess.run(
424
+ ['node', os.path.join(RAIZ, 'bin', 'agents-city.js'), '--help'],
425
+ capture_output=True, text=True).stdout
426
+ afirma('· the longest command does not run into its description',
427
+ 'committee chair-mediated' in salida, salida[:240])
428
+
429
+
430
+ def documentacion_publica():
431
+ """Both public READMEs remain complete operational manuals.
432
+
433
+ Help output and implementation tests own the detailed behaviour. This check
434
+ protects the other direction: adding a public command, slash command, domain,
435
+ runtime, or release without documenting it in both supported languages.
436
+ """
437
+ print(' bilingual public documentation')
438
+ package = json.load(open(os.path.join(RAIZ, 'package.json'), encoding='utf-8'))
439
+ public_commands = [
440
+ 'hall', 'seat', 'cities', 'road', 'bus', 'committee', 'benchmark',
441
+ 'reset', 'skills', 'city', 'demo', 'setup', 'report', 'tokens', 'exit',
442
+ 'test',
443
+ ]
444
+ public_contract = [
445
+ '--help', '--version', '--no-browser', '--out', '--tui', '--repos',
446
+ '--agent-roles', '--agents', '--goal', '--engines', '--domain', '--role',
447
+ '--no-yolo', '--no-sync', '--only', '--model', '--effort',
448
+ 'cities list', 'cities current', 'cities create', 'cities use',
449
+ 'road list', 'road connect', 'road disconnect', 'road invite',
450
+ 'bus roster', 'bus inbox', 'bus send',
451
+ 'committee list', 'committee history', 'committee show',
452
+ 'committee status', 'committee open', 'committee respond',
453
+ 'committee synthesize', 'committee floor-request',
454
+ 'committee floor-grant', 'committee floor-deny', 'committee reply',
455
+ 'committee decide', 'committee verify', 'committee replan',
456
+ 'committee close', 'committee cancel', 'committee schema', '--input',
457
+ 'benchmark stress', 'benchmark live', 'benchmark committee', '--runtime',
458
+ '--command', '--timeout', '--json', '--keep', '--no-save', '--dry-run',
459
+ '--push', '--quiet', '--days', '--all',
460
+ 'claude-stream-json', 'stream-json', 'managed-settings.json',
461
+ ]
462
+ slash_commands = [
463
+ f'/city:{os.path.basename(path)[:-3]}'
464
+ for path in glob.glob(os.path.join(RAIZ, 'plugin', 'commands', '*.md'))
465
+ ]
466
+ domain_ids = []
467
+ for path in glob.glob(os.path.join(RAIZ, 'plugin', 'domains', '*.md')):
468
+ match = re.search(r'^id:\s*(\S+)', open(path, encoding='utf-8').read(), re.M)
469
+ if match:
470
+ domain_ids.append(match.group(1))
471
+
472
+ for filename, language_heading, cookbook_prefix in (
473
+ ('README.es.md', '## Referencia completa de comandos', '### Caso '),
474
+ ('README.md', '## Complete command reference', '### Case '),
475
+ ):
476
+ path = os.path.join(RAIZ, filename)
477
+ content = open(path, encoding='utf-8').read()
478
+ afirma(f'· {filename} is a detailed manual, not a short landing page',
479
+ content.count('\n') > 1_500 and language_heading in content,
480
+ f'only {content.count(chr(10)) + 1} lines')
481
+ missing_commands = [
482
+ command for command in public_commands
483
+ if f'agents-city {command}' not in content
484
+ ]
485
+ afirma(f'· {filename} documents every npm command',
486
+ not missing_commands, 'missing: ' + ', '.join(missing_commands))
487
+ missing_contract = [token for token in public_contract if token not in content]
488
+ afirma(f'· {filename} documents every public option and subcommand',
489
+ not missing_contract, 'missing: ' + ', '.join(missing_contract))
490
+ missing_slash = [command for command in slash_commands if command not in content]
491
+ afirma(f'· {filename} documents every Claude slash command',
492
+ not missing_slash, 'missing: ' + ', '.join(missing_slash))
493
+ missing_domains = [domain for domain in domain_ids if f'`{domain}`' not in content]
494
+ afirma(f'· {filename} documents every built-in domain',
495
+ not missing_domains, 'missing: ' + ', '.join(missing_domains))
496
+ afirma(f'· {filename} covers all five runtime modes',
497
+ all(name in content for name in
498
+ ('Claude', 'Codex', 'OpenCode', 'Kimi', 'terminal:')))
499
+ afirma(f'· {filename} carries eighteen reproducible use cases',
500
+ content.count(cookbook_prefix) == 18,
501
+ f'found {content.count(cookbook_prefix)}')
502
+ afirma(f'· {filename} tracks the package release used by its tarball examples',
503
+ package['version'] in content,
504
+ f"README has no {package['version']}")
505
+ afirma(f'· {filename} links to the other language',
506
+ '[Español](README.es.md)' in content and '[English](README.md)' in content)
507
+ afirma(f'· {filename} shows the real city identity key',
508
+ 'cityId' not in content and 'schema: agents-city/city@1' not in content,
509
+ 'documents a field city.yml does not write')
510
+
511
+
512
+ def ci_bootstrap():
513
+ """A clean Linux checkout installs the WebSocket runtime before E2E tests."""
514
+ print(' clean-checkout CI bootstrap')
515
+ workflow = open(
516
+ os.path.join(RAIZ, '.github', 'workflows', 'test.yml'), encoding='utf-8'
517
+ ).read()
518
+ install = workflow.find('npm --prefix plugin/channel ci --silent')
519
+ suites = workflow.find('run: ./bin/test')
520
+ afirma('· CI installs local channel dependencies before integration suites',
521
+ 0 <= install < suites,
522
+ 'the clean checkout would launch WebSocket doubles without ws installed')
523
+
524
+
525
+ def main():
526
+ print()
527
+ resolvedores()
528
+ escritores()
529
+ una_definicion()
530
+ rutas_reales()
531
+ conciencia_acotada()
532
+ demo_coherente()
533
+ varias_ciudades()
534
+ ventanas_gemelas()
535
+ canal_compilado()
536
+ plugin_canal()
537
+ puerta_npm()
538
+ documentacion_publica()
539
+ ci_bootstrap()
540
+ return resumen('contracts')
541
+
542
+
543
+ if __name__ == '__main__':
544
+ sys.exit(main())