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,283 @@
1
+ #!/usr/bin/env python3
2
+ """The parcels.yml reader, and the growth reporter that stands on it.
3
+
4
+ ./bin/test-parcels.py
5
+
6
+ Why these two together: the reporter's whole job is to turn that file into numbers,
7
+ and for as long as this project has existed it turned it into nothing. It read the
8
+ folder out of a key called `path`, the format has only ever written `ruta`, so every
9
+ parcel was skipped as folderless and the message printed told you to add a `path:`
10
+ that means nothing anywhere. No test caught it because there was no test, and
11
+ running it by hand *looked* like a city that had simply not grown yet.
12
+
13
+ There were also two readers of the one format — one in the seeder, one in the
14
+ reporter — walking it with a byte-identical regex and then disagreeing about the
15
+ `lab:` section, about unreadable lines, and about where a folder is. So half of what
16
+ is below pins the two down to the same answers.
17
+ """
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+
24
+ AQUI = os.path.dirname(os.path.abspath(__file__))
25
+ RAIZ = os.path.dirname(AQUI)
26
+ sys.path.insert(0, os.path.join(RAIZ, 'plugin', 'scripts'))
27
+ import parcels # noqa: E402
28
+
29
+ from testlib import comprueba, afirma, resumen # noqa: E402
30
+
31
+
32
+ def escribe(cuerpo):
33
+ d = tempfile.mkdtemp()
34
+ open(os.path.join(d, 'parcels.yml'), 'w').write(cuerpo)
35
+ return d
36
+
37
+
38
+ # ══ the format, read the way it is actually written ════════════════════════
39
+ def formato():
40
+ print(' the format')
41
+
42
+ d = escribe('''# a comment
43
+ repos:
44
+ main-site:
45
+ - ruta: "content/**/deposits/**" ; unidad: banking ; nombre: "main · deposits"
46
+ - ruta: "" ; unidad: none ; nombre: "main · platform"
47
+ api:
48
+ - ruta: "" ; unidad: mine ; nombre: "api"
49
+
50
+ lab:
51
+ - lab-procgen
52
+ - lab-shaders
53
+ ''')
54
+ ps, lab, raras = parcels.lee(f'{d}/parcels.yml')
55
+ comprueba('· every parcel line is read', len(ps), 3)
56
+ comprueba('· a split repo keeps both of its parcels',
57
+ [p['id'] for p in ps if p['repo'] == 'main-site'],
58
+ ['main-site:content/**/deposits/**', 'main-site'])
59
+ comprueba('· the unit comes off the line', ps[0]['unidad'], 'banking')
60
+ comprueba('· so does the display name', ps[0]['nombre'], 'main · deposits')
61
+ comprueba('· an empty ruta means the whole repo, and the id is just the repo',
62
+ (ps[1]['ruta'], ps[1]['id']), ('', 'main-site'))
63
+ comprueba('· the lab section is collected', lab, {'lab-procgen', 'lab-shaders'})
64
+ comprueba('· and nothing was unreadable', raras, [])
65
+ afirma('· lab entries are not also parcels',
66
+ 'lab-procgen' not in [p['repo'] for p in ps])
67
+ shutil.rmtree(d)
68
+
69
+ # The id has to be computed one way, because the reporter pushes on it and the
70
+ # map stores on it.
71
+ comprueba('· the id of a whole repo', parcels.identidad('api', ''), 'api')
72
+ comprueba('· the id of a slice', parcels.identidad('api', 'src/**'), 'api:src/**')
73
+
74
+ # Defaults, not errors: a half-written line still lands somewhere findable.
75
+ d = escribe('repos:\n solo:\n - ruta: ""\n')
76
+ ps, _, _ = parcels.lee(f'{d}/parcels.yml')
77
+ comprueba('· a line with no unit defaults to none', ps[0]['unidad'], 'none')
78
+ comprueba('· a line with no name is called after its repo', ps[0]['nombre'], 'solo')
79
+ shutil.rmtree(d)
80
+
81
+
82
+ # ══ files that are wrong in the ways real files are wrong ══════════════════
83
+ def formato_roto():
84
+ print(' files that are wrong')
85
+
86
+ comprueba('· a missing file reads as empty, not as a crash',
87
+ parcels.lee('/tmp/definitely-not-here/parcels.yml'), ([], set(), []))
88
+ comprueba('· a directory where a file should be, likewise',
89
+ parcels.lee(tempfile.mkdtemp()), ([], set(), []))
90
+
91
+ d = escribe('')
92
+ comprueba('· an empty file', parcels.lee(f'{d}/parcels.yml'), ([], set(), []))
93
+ shutil.rmtree(d)
94
+
95
+ d = escribe('repos:\n')
96
+ comprueba('· a header with nothing under it', parcels.lee(f'{d}/parcels.yml')[0], [])
97
+ shutil.rmtree(d)
98
+
99
+ # A line under a repo that is not a parcel must be *reported*, not swallowed:
100
+ # the alternative is a repo quietly landing in the wrong district.
101
+ d = escribe('repos:\n api:\n - path: "src" ; unidad: mine\n - nonsense\n')
102
+ ps, _, raras = parcels.lee(f'{d}/parcels.yml')
103
+ comprueba('· lines that are not parcels are not counted as parcels', ps, [])
104
+ comprueba('· they are handed back to be complained about', len(raras), 2)
105
+ afirma('· and the complaint names the right shape',
106
+ '- ruta:' in parcels.aviso(raras), parcels.aviso(raras)[:120])
107
+ comprueba('· with nothing wrong, there is nothing to say', parcels.aviso([]), '')
108
+ shutil.rmtree(d)
109
+
110
+ # `lab:` on one line, which is what every writer in this repo emits.
111
+ d = escribe('repos:\n api:\n - ruta: "" ; unidad: mine ; nombre: "api"\nlab: []\n')
112
+ ps, lab, raras = parcels.lee(f'{d}/parcels.yml')
113
+ comprueba('· an inline empty lab is empty, not a parse error', (len(ps), lab, raras),
114
+ (1, set(), []))
115
+ shutil.rmtree(d)
116
+
117
+ # Odd but legal names. A dot and a dash both appear in real repo names.
118
+ d = escribe('repos:\n my.repo-2:\n - ruta: "" ; unidad: u ; nombre: "x"\n')
119
+ comprueba('· dots and dashes in a repo name',
120
+ parcels.lee(f'{d}/parcels.yml')[0][0]['repo'], 'my.repo-2')
121
+ shutil.rmtree(d)
122
+
123
+ # Quotes are optional in the format, so both have to work.
124
+ d = escribe('repos:\n api:\n - ruta: src/** ; unidad: mine ; nombre: bare\n')
125
+ ps = parcels.lee(f'{d}/parcels.yml')[0]
126
+ comprueba('· unquoted values', (ps[0]['ruta'], ps[0]['nombre']), ('src/**', 'bare'))
127
+ shutil.rmtree(d)
128
+
129
+
130
+ # ══ the two readers must agree ═════════════════════════════════════════════
131
+ def un_solo_lector():
132
+ print(' one reader, two callers')
133
+
134
+ # The seeder and the reporter both go through parcels.lee now. The thing worth
135
+ # pinning is that they still produce the same parcels for the same file, because
136
+ # that is what drifted before.
137
+ import importlib.machinery as mach
138
+ import importlib.util as iu
139
+ s = iu.spec_from_loader('seed', mach.SourceFileLoader(
140
+ 'seed', os.path.join(RAIZ, 'city', 'scripts', 'seed.py')))
141
+ seed = iu.module_from_spec(s)
142
+ s.loader.exec_module(seed)
143
+ s2 = iu.spec_from_loader('rep', mach.SourceFileLoader(
144
+ 'rep', os.path.join(RAIZ, 'plugin', 'scripts', 'report.py')))
145
+ rep = iu.module_from_spec(s2)
146
+ s2.loader.exec_module(rep)
147
+
148
+ d = escribe('''repos:
149
+ api:
150
+ - ruta: "src/**" ; unidad: mine ; nombre: "api · src"
151
+ - ruta: "" ; unidad: none ; nombre: "api · rest"
152
+ lab: []
153
+ ''')
154
+ filas, lab = seed.parcelas(d)
155
+ delrep = rep.parcelas(d)
156
+ comprueba('· the seeder sees both parcels', len(filas), 2)
157
+ comprueba('· so does the reporter', len(delrep), 2)
158
+ comprueba('· and they agree on the ids, which is what growth is keyed on',
159
+ [seed.parcels.identidad(r, ru) for r, ru, _, _ in filas],
160
+ [p['id'] for p in delrep])
161
+ comprueba('· the seeder still returns its own tuple shape', filas[0],
162
+ ('api', 'src/**', 'mine', 'api · src'))
163
+ shutil.rmtree(d)
164
+
165
+ # And the demo, which is the only real file in the repo, must read the same as
166
+ # it did before the two readers became one: 34 parcels.
167
+ r = subprocess.run(['python3', os.path.join(RAIZ, 'city', 'scripts', 'seed.py'),
168
+ '--data', os.path.join(RAIZ, 'demo'), '--fake-history'],
169
+ capture_output=True, text=True)
170
+ comprueba('· the demo still seeds 34 parcels',
171
+ r.stdout.count('INSERT INTO parcela'), 34)
172
+
173
+
174
+ # ══ the reporter: where a folder is, and what it says when there is none ═══
175
+ def reportero():
176
+ print(' the growth reporter')
177
+
178
+ import importlib.machinery as mach
179
+ import importlib.util as iu
180
+ s = iu.spec_from_loader('rep', mach.SourceFileLoader(
181
+ 'rep', os.path.join(RAIZ, 'plugin', 'scripts', 'report.py')))
182
+ rep = iu.module_from_spec(s)
183
+ s.loader.exec_module(rep)
184
+
185
+ casa = tempfile.mkdtemp()
186
+ repo = os.path.join(casa, 'api')
187
+ os.makedirs(os.path.join(repo, 'src', 'checkout'))
188
+ # A real git repo with a remote, because find-repos.sh indexes by the remote and
189
+ # the subprocess below runs the real one. A directory called `api` is not a repo,
190
+ # and the first version of this test quietly proved only that.
191
+ for orden in (['git', 'init', '-q'],
192
+ ['git', 'remote', 'add', 'origin', 'https://example.com/x/api.git']):
193
+ subprocess.run(orden, cwd=repo, capture_output=True)
194
+
195
+ # Its own cache, so the test never rewrites the caller's repo index.
196
+ cache = tempfile.mkdtemp()
197
+ entorno = dict(os.environ, CITY_SEARCH_IN=casa, XDG_CACHE_HOME=cache,
198
+ CITY_SEARCH_DEPTH='3')
199
+
200
+ # In-process, the resolver is stubbed; the subprocess below uses the real one.
201
+ rep.donde_esta = lambda r, cache={}: repo if r == 'api' else ''
202
+
203
+ d = escribe('''repos:
204
+ api:
205
+ - ruta: "" ; unidad: u ; nombre: "whole"
206
+ - ruta: "src/checkout" ; unidad: u ; nombre: "a real subdir"
207
+ - ruta: "src/**" ; unidad: u ; nombre: "a glob"
208
+ - ruta: "src/nope" ; unidad: u ; nombre: "a subdir that is not there"
209
+ ghost:
210
+ - ruta: "" ; unidad: u ; nombre: "not cloned"
211
+ ''')
212
+ ps = {p['nombre']: p for p in rep.parcelas(d)}
213
+ comprueba('· an empty ruta runs in the repo root', ps['whole']['path'], repo)
214
+ comprueba('· a concrete subdirectory runs in that subdirectory',
215
+ ps['a real subdir']['path'], os.path.join(repo, 'src', 'checkout'))
216
+ comprueba('· a glob has no single directory, so it runs in the repo root',
217
+ ps['a glob']['path'], repo)
218
+ comprueba('· a subdirectory that is not there falls back to the repo root',
219
+ ps['a subdir that is not there']['path'], repo)
220
+ comprueba('· a repo that is not on this machine has no folder at all',
221
+ ps['not cloned']['path'], '')
222
+ afirma('· and the key is `path` for the caller but read from `ruta`',
223
+ ps['a glob']['ruta'] == 'src/**', repr(ps['a glob']))
224
+
225
+ # End to end: a real command, in the right folder, producing a real number.
226
+ open(os.path.join(d, 'city.yml'), 'w').write('grow_command: ls -1 | wc -l\n')
227
+ for n in ('a', 'b', 'c'):
228
+ open(os.path.join(repo, n), 'w').write('')
229
+ r = subprocess.run(['python3', os.path.join(RAIZ, 'plugin', 'scripts', 'report.py'),
230
+ '--data', d], capture_output=True, text=True, env=entorno)
231
+ afirma('· end to end, it reports a number for a parcel it can reach',
232
+ 'api' in r.stdout and 'Nothing sent' in r.stdout,
233
+ r.stdout[:400] + r.stderr[:200])
234
+ afirma('· the number comes from the parcel\'s own folder, not from the cwd',
235
+ any(l.split()[-1].isdigit() and int(l.split()[-1]) >= 3
236
+ for l in r.stdout.splitlines() if l.startswith(' api')),
237
+ r.stdout[:400])
238
+
239
+ # The unhappy message has to say what is actually wrong. It used to send people
240
+ # to add a `path:` key that does not exist in this format.
241
+ solo_fantasma = escribe('repos:\n ghost:\n - ruta: "" ; unidad: u ; nombre: "g"\n')
242
+ vacia = tempfile.mkdtemp()
243
+ r = subprocess.run(['python3', os.path.join(RAIZ, 'plugin', 'scripts', 'report.py'),
244
+ '--data', solo_fantasma], capture_output=True, text=True,
245
+ env=dict(entorno, CITY_SEARCH_IN=vacia,
246
+ XDG_CACHE_HOME=tempfile.mkdtemp()))
247
+ afirma('· an unreachable parcel is explained as not cloned',
248
+ 'not cloned on this machine' in r.stdout, r.stdout[:250])
249
+ afirma('· and nobody is told to add a `path:` key',
250
+ 'path:' not in r.stdout, r.stdout[:250])
251
+
252
+ vacio = escribe('repos:\n')
253
+ r = subprocess.run(['python3', os.path.join(RAIZ, 'plugin', 'scripts', 'report.py'),
254
+ '--data', vacio], capture_output=True, text=True)
255
+ afirma('· no parcels at all points at ./bin/seat',
256
+ './bin/seat' in r.stdout, r.stdout[:250])
257
+
258
+ # A command that fails is *unknown*, never zero: reporting zero would look like
259
+ # work being undone.
260
+ comprueba('· a failing command measures as unknown',
261
+ rep.cuenta('exit 3', casa), None)
262
+ comprueba('· a command printing nothing, likewise', rep.cuenta('true', casa), None)
263
+ comprueba('· no command at all, likewise', rep.cuenta('', casa), None)
264
+ comprueba('· the first number in the output is the answer',
265
+ rep.cuenta('echo " 42 files"', casa), 42)
266
+ comprueba('· thousands separators do not become a 1',
267
+ rep.cuenta('echo 1,234', casa), 1234)
268
+
269
+ for x in (casa, cache, vacia, d, solo_fantasma, vacio):
270
+ shutil.rmtree(x, ignore_errors=True)
271
+
272
+
273
+ def main():
274
+ print()
275
+ formato()
276
+ formato_roto()
277
+ un_solo_lector()
278
+ reportero()
279
+ return resumen('parcels + report')
280
+
281
+
282
+ if __name__ == '__main__':
283
+ sys.exit(main())
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ """Native runtime failures retain work and recover without terminal injection."""
3
+ import json
4
+ import os
5
+ import shutil
6
+ import socket
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 afirma, comprueba, detiene_proceso, resumen # noqa: E402
16
+
17
+ GATEWAY = os.path.join(RAIZ, 'plugin', 'channel', 'runtime-gateway.js')
18
+ CLIENT = os.path.join(RAIZ, 'plugin', 'channel', 'client.js')
19
+ FAKE = os.path.join(RAIZ, 'benchmarks', 'latency', 'fake-native-server.mjs')
20
+ PROVIDERS = ('codex', 'opencode', 'kimi')
21
+
22
+
23
+ def puerto():
24
+ with socket.socket() as sock:
25
+ sock.bind(('127.0.0.1', 0))
26
+ return sock.getsockname()[1]
27
+
28
+
29
+ def espera(condicion, segundos=10):
30
+ limite = time.monotonic() + segundos
31
+ while time.monotonic() < limite:
32
+ if condicion():
33
+ return True
34
+ time.sleep(.05)
35
+ return False
36
+
37
+
38
+ def lee_jsonl(path):
39
+ if not os.path.exists(path):
40
+ return []
41
+ with open(path, encoding='utf-8') as stream:
42
+ return [json.loads(line) for line in stream if line.strip()]
43
+
44
+
45
+ def lee(path):
46
+ if not os.path.exists(path):
47
+ return ''
48
+ with open(path, encoding='utf-8', errors='replace') as stream:
49
+ return stream.read()
50
+
51
+
52
+ def para(procesos):
53
+ for process in procesos:
54
+ if process.poll() is None:
55
+ process.terminate()
56
+ for process in procesos:
57
+ try:
58
+ process.wait(timeout=3)
59
+ except subprocess.TimeoutExpired:
60
+ process.kill()
61
+ process.wait(timeout=2)
62
+
63
+
64
+ def arranca_gateways(city, repos, actors, env, logs):
65
+ procesos, streams = [], []
66
+ for provider in PROVIDERS:
67
+ actor = actors[provider]
68
+ stream = open(logs[provider], 'a+', encoding='utf-8')
69
+ streams.append(stream)
70
+ procesos.append(subprocess.Popen(
71
+ ['node', GATEWAY, '--data', city, '--actor', actor,
72
+ '--cwd', os.path.join(repos, actor), '--command', provider,
73
+ '--auto', '1', '--interactive', '0'],
74
+ env=env, stdin=subprocess.DEVNULL,
75
+ stdout=stream, stderr=stream, text=True))
76
+ return procesos, streams
77
+
78
+
79
+ def main():
80
+ print('\n native runtime non-happy paths')
81
+ if not shutil.which('node'):
82
+ afirma('· node is available', False, 'node missing')
83
+ return resumen('runtime-failures')
84
+ base = tempfile.mkdtemp(prefix='agents-city-runtime-failures-')
85
+ app = os.path.join(base, 'app')
86
+ city = os.path.join(app, 'alice', 'home')
87
+ repos = os.path.join(base, 'repos')
88
+ os.makedirs(city)
89
+ actors = {provider: f'{provider}-agent' for provider in PROVIDERS}
90
+ for actor in actors.values():
91
+ os.makedirs(os.path.join(repos, actor))
92
+ open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
93
+ 'id: city_native_failures\nname: Home\nslug: home\nowner: alice\n')
94
+ open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
95
+ '{"version": 1, "roads": []}\n')
96
+ open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
97
+ f'---\nuser: alice\nagent: alice/ceo\nrepos: [{", ".join(actors.values())}]\n'
98
+ + ''.join(f'role.{actor}: blank\n' for actor in actors.values())
99
+ + ''.join(f'runs.{actor}: {provider}\n' for provider, actor in actors.items())
100
+ + '---\n')
101
+
102
+ ports = {provider: puerto() for provider in PROVIDERS}
103
+ captures = {provider: os.path.join(base, f'{provider}.jsonl')
104
+ for provider in PROVIDERS}
105
+ behaviors = {provider: os.path.join(base, f'{provider}.behavior')
106
+ for provider in PROVIDERS}
107
+ logs = {provider: os.path.join(base, f'{provider}.log')
108
+ for provider in PROVIDERS}
109
+ for path in behaviors.values():
110
+ open(path, 'w', encoding='utf-8').write('reject-prompt\n')
111
+ env = dict(
112
+ os.environ,
113
+ AGENTS_CITY_HOME=app,
114
+ AGENTS_CITY_DATA=city,
115
+ AGENTS_CITY_USER='alice',
116
+ CITY_RUNTIME_AUTO='1',
117
+ CITY_CODEX_APP_SERVER_URL=f'ws://127.0.0.1:{ports["codex"]}',
118
+ CITY_OPENCODE_SERVER_URL=f'http://127.0.0.1:{ports["opencode"]}',
119
+ CITY_KIMI_SERVER_URL=f'http://127.0.0.1:{ports["kimi"]}',
120
+ CITY_KIMI_SERVER_TOKEN='fake-secret',
121
+ )
122
+ for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
123
+ env.pop(key, None)
124
+
125
+ servers, gateways, streams = [], [], []
126
+ runtime_dir = os.path.join(app, '.runtime', 'bus', 'city-native-failures')
127
+ endpoint_path = os.path.join(runtime_dir, 'endpoint.json')
128
+ status_dir = os.path.join(runtime_dir, 'gateways')
129
+ outbox = os.path.join(runtime_dir, 'outbox')
130
+ metrics_path = os.path.join(runtime_dir, 'runtime-latency.jsonl')
131
+ try:
132
+ for provider in PROVIDERS:
133
+ servers.append(subprocess.Popen(
134
+ ['node', FAKE, provider, str(ports[provider]), captures[provider],
135
+ behaviors[provider]],
136
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True))
137
+ afirma('· all failure doubles start without external accounts',
138
+ espera(lambda: all(process.poll() is None for process in servers), .4))
139
+
140
+ no_token_env = dict(env)
141
+ no_token_env.pop('CITY_KIMI_SERVER_TOKEN')
142
+ no_token = subprocess.run(
143
+ ['node', GATEWAY, '--data', city, '--actor', actors['kimi'],
144
+ '--cwd', os.path.join(repos, actors['kimi']), '--command', 'kimi',
145
+ '--auto', '1', '--interactive', '0'],
146
+ env=no_token_env, capture_output=True, text=True, timeout=5)
147
+ afirma('· Kimi without a bearer token fails closed before joining the bus',
148
+ no_token.returncode != 0
149
+ and 'bearer token was not available' in no_token.stderr,
150
+ no_token.stderr[-400:])
151
+ afirma('· failed Kimi startup leaves no online status or stale PID',
152
+ not os.path.exists(os.path.join(status_dir, f'{actors["kimi"]}.json'))
153
+ and not os.path.exists(os.path.join(status_dir, f'{actors["kimi"]}.pid')))
154
+
155
+ unknown = subprocess.run(
156
+ ['node', GATEWAY, '--data', city, '--actor', actors['codex'],
157
+ '--cwd', os.path.join(repos, actors['codex']), '--command', 'mystery-cli',
158
+ '--auto', '1', '--interactive', '0'],
159
+ env=env, capture_output=True, text=True, timeout=5)
160
+ afirma('· an unknown CLI is rejected unless the user explicitly selects terminal:',
161
+ unknown.returncode != 0 and 'no native gateway exists' in unknown.stderr,
162
+ unknown.stderr[-400:])
163
+ afirma('· rejecting an unknown CLI never creates a terminal adapter',
164
+ not os.path.exists(os.path.join(runtime_dir, 'adapters')))
165
+
166
+ first, first_streams = arranca_gateways(city, repos, actors, env, logs)
167
+ gateways.extend(first)
168
+ streams.extend(first_streams)
169
+ afirma('· every native gateway joins before the simulated provider failure',
170
+ espera(lambda: all(os.path.exists(os.path.join(status_dir, f'{actor}.json'))
171
+ for actor in actors.values())),
172
+ '\n'.join(lee(logs[p])[-500:] for p in PROVIDERS))
173
+
174
+ opened = subprocess.run(
175
+ ['node', CLIENT, 'committee', 'open', '--input', '-'],
176
+ input=json.dumps({
177
+ 'question': 'Prove that a rejected provider request is never lost.',
178
+ 'desiredOutcome': 'Retain one assignment for safe recovery.',
179
+ 'definitionOfDone': ['each provider rejects the first native request'],
180
+ 'participants': list(actors.values()),
181
+ }), capture_output=True, text=True,
182
+ env=dict(env, CITY_BUS_ACTOR='seat'), timeout=12)
183
+ state = json.loads(opened.stdout) if opened.returncode == 0 else {}
184
+ afirma('· the chair can enqueue work while providers reject it',
185
+ bool(state.get('id')), opened.stderr.strip())
186
+ errores = {
187
+ 'codex': 'fake provider rejected the Codex prompt',
188
+ 'opencode': 'fake provider rejected the OpenCode prompt',
189
+ 'kimi': 'fake provider rejected the Kimi prompt',
190
+ }
191
+ afirma('· Codex, OpenCode and Kimi surface their native rejection',
192
+ espera(lambda: all(error in lee(logs[provider])
193
+ for provider, error in errores.items())),
194
+ '\n'.join(f'{p}: {lee(logs[p])[-320:]}' for p in PROVIDERS))
195
+ comprueba('· a rejected prompt records no false native acceptance',
196
+ lee_jsonl(metrics_path), [])
197
+ comprueba('· every rejected assignment remains once in its durable outbox',
198
+ {provider: len(os.listdir(os.path.join(outbox, actor)))
199
+ if os.path.isdir(os.path.join(outbox, actor)) else 0
200
+ for provider, actor in actors.items()},
201
+ {provider: 1 for provider in PROVIDERS})
202
+
203
+ para(first)
204
+ afirma('· failed gateway shutdown removes every stale identity file',
205
+ espera(lambda: all(not os.path.exists(os.path.join(status_dir, f'{actor}.pid'))
206
+ and not os.path.exists(os.path.join(status_dir, f'{actor}.json'))
207
+ for actor in actors.values())))
208
+ for path in behaviors.values():
209
+ open(path, 'w', encoding='utf-8').write('healthy\n')
210
+
211
+ second, second_streams = arranca_gateways(city, repos, actors, env, logs)
212
+ gateways.extend(second)
213
+ streams.extend(second_streams)
214
+ afirma('· recovered gateways accept the retained work through native APIs',
215
+ espera(lambda: all(len(lee_jsonl(captures[p])) == 1 for p in PROVIDERS), 12),
216
+ str({p: len(lee_jsonl(captures[p])) for p in PROVIDERS}))
217
+ afirma('· only a real native acceptance drains every durable outbox',
218
+ espera(lambda: all(not os.path.isdir(os.path.join(outbox, actor))
219
+ or not os.listdir(os.path.join(outbox, actor))
220
+ for actor in actors.values())))
221
+ time.sleep(.35)
222
+ comprueba('· recovery invokes each provider exactly once',
223
+ {p: len(lee_jsonl(captures[p])) for p in PROVIDERS},
224
+ {p: 1 for p in PROVIDERS})
225
+ metrics = lee_jsonl(metrics_path)
226
+ comprueba('· recovery records one acceptance per native transport',
227
+ {row.get('transport') for row in metrics},
228
+ {'codex-app-server-ws', 'opencode-http-sse', 'kimi-rest-ws'})
229
+ comprueba('· no failed attempt was counted in the latency ledger',
230
+ len(metrics), 3)
231
+ afirma('· all failure and recovery paths avoid clipboard and tmux injection',
232
+ not os.path.exists(os.path.join(runtime_dir, 'adapters'))
233
+ and not os.path.exists(os.path.join(runtime_dir, 'delivery-latency.jsonl')))
234
+ finally:
235
+ para(gateways)
236
+ para(servers)
237
+ for stream in streams:
238
+ stream.close()
239
+ hub_pid = 0
240
+ if os.path.exists(endpoint_path):
241
+ try:
242
+ hub_pid = int(json.load(open(endpoint_path, encoding='utf-8'))['pid'])
243
+ except (OSError, ValueError, KeyError, json.JSONDecodeError):
244
+ pass
245
+ afirma('· failure-suite cleanup leaves no orphan city hub',
246
+ detiene_proceso(hub_pid), str(hub_pid))
247
+ shutil.rmtree(base, ignore_errors=True)
248
+ return resumen('runtime-failures')
249
+
250
+
251
+ if __name__ == '__main__':
252
+ sys.exit(main())