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,488 @@
1
+ #!/usr/bin/env python3
2
+ """Codex opens its official remote TUI and the bus safely joins its thread."""
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_hubs_de_ciudad, 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
+
21
+
22
+ def puerto():
23
+ with socket.socket() as sock:
24
+ sock.bind(('127.0.0.1', 0))
25
+ return sock.getsockname()[1]
26
+
27
+
28
+ def espera(condicion, segundos=10):
29
+ limite = time.monotonic() + segundos
30
+ while time.monotonic() < limite:
31
+ if condicion():
32
+ return True
33
+ time.sleep(.05)
34
+ return False
35
+
36
+
37
+ def lee(path):
38
+ if not os.path.exists(path):
39
+ return ''
40
+ with open(path, encoding='utf-8', errors='replace') as stream:
41
+ return stream.read()
42
+
43
+
44
+ def lineas_json(path):
45
+ if not os.path.exists(path):
46
+ return []
47
+ with open(path, encoding='utf-8') as stream:
48
+ return [json.loads(line) for line in stream if line.strip()]
49
+
50
+
51
+ def solicitudes(path):
52
+ return [row for row in lineas_json(path) if row.get('request')]
53
+
54
+
55
+ def aprobaciones(path):
56
+ return [row for row in lineas_json(path) if row.get('approval')]
57
+
58
+
59
+ def main():
60
+ print('\n Codex native TUI over its city WebSocket thread')
61
+ if not shutil.which('node'):
62
+ afirma('· node is available', False, 'node missing')
63
+ return resumen('runtime-ui')
64
+
65
+ base = tempfile.mkdtemp(prefix='agents-city-runtime-ui-')
66
+ app = os.path.join(base, 'app')
67
+ city = os.path.join(app, 'alice', 'home')
68
+ repo = os.path.join(base, 'repos', 'codex-agent')
69
+ fake_bin = os.path.join(base, 'bin')
70
+ for path in (city, repo, fake_bin):
71
+ os.makedirs(path)
72
+ open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
73
+ 'id: city_native_ui\nname: Home\nslug: home\nowner: alice\n')
74
+ open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
75
+ '{"version": 1, "roads": []}\n')
76
+ open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
77
+ '---\nuser: alice\nagent: alice/ceo\nrepos: [codex-agent]\n'
78
+ 'role.codex-agent: engineer\nruns.codex-agent: codex\n---\n')
79
+
80
+ tui_args = os.path.join(base, 'tui-args.txt')
81
+ tui_pid = os.path.join(base, 'tui.pid')
82
+ tui_cwd = os.path.join(base, 'tui-cwd.txt')
83
+ fake_codex = os.path.join(fake_bin, 'codex')
84
+ open(fake_codex, 'w', encoding='utf-8').write(
85
+ f'#!{sys.executable}\n'
86
+ 'import os, signal, sys, time\n'
87
+ 'open(os.environ["CITY_TEST_TUI_ARGS"], "w").write("\\n".join(sys.argv[1:]) + "\\n")\n'
88
+ 'open(os.environ["CITY_TEST_TUI_PID"], "w").write(str(os.getpid()) + "\\n")\n'
89
+ 'open(os.environ["CITY_TEST_TUI_CWD"], "w").write(os.getcwd() + "\\n")\n'
90
+ 'print("FAKE_CODEX_TUI_ATTACHED", flush=True)\n'
91
+ 'def stop(_signal, _frame): raise SystemExit(0)\n'
92
+ 'signal.signal(signal.SIGINT, stop)\n'
93
+ 'signal.signal(signal.SIGTERM, stop)\n'
94
+ 'signal.signal(signal.SIGHUP, stop)\n'
95
+ 'while True: time.sleep(1)\n')
96
+ os.chmod(fake_codex, 0o755)
97
+
98
+ port = puerto()
99
+ url = f'ws://127.0.0.1:{port}'
100
+ capture = os.path.join(base, 'codex.jsonl')
101
+ behavior = os.path.join(base, 'behavior.txt')
102
+ open(behavior, 'w', encoding='utf-8').write('healthy\n')
103
+ log_path = os.path.join(base, 'gateway.log')
104
+ runtime_dir = os.path.join(app, '.runtime', 'bus', 'city-native-ui')
105
+ status_path = os.path.join(runtime_dir, 'gateways', 'codex-agent.json')
106
+ activity_path = os.path.join(runtime_dir, 'activity.jsonl')
107
+ diagnostic_path = os.path.join(runtime_dir, 'diagnostics.jsonl')
108
+ env = dict(
109
+ os.environ,
110
+ AGENTS_CITY_HOME=app,
111
+ AGENTS_CITY_DATA=city,
112
+ AGENTS_CITY_USER='alice',
113
+ CITY_CODEX_APP_SERVER_URL=url,
114
+ CITY_TEST_TUI_ARGS=tui_args,
115
+ CITY_TEST_TUI_PID=tui_pid,
116
+ CITY_TEST_TUI_CWD=tui_cwd,
117
+ FAKE_CODEX_TUI_CWD=repo,
118
+ # Compatibility case for an explicitly outer-caged gateway. The normal
119
+ # launcher no longer wraps Codex because MCP workers also sandbox, but
120
+ # the connector must still avoid nesting its turn sandbox if embedded.
121
+ CITY_OUTER_CAGE='1',
122
+ )
123
+ for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
124
+ env.pop(key, None)
125
+
126
+ server = None
127
+ gateway = None
128
+ stream = None
129
+ try:
130
+ server = subprocess.Popen(
131
+ ['node', FAKE, 'codex', str(port), capture, behavior],
132
+ env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
133
+ ready = server.stdout.readline() if server.stdout else ''
134
+ afirma('· deterministic Codex app-server starts', '"ready":true' in ready, ready)
135
+
136
+ stream = open(log_path, 'w+', encoding='utf-8')
137
+ gateway = subprocess.Popen(
138
+ ['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
139
+ '--cwd', repo, '--command', fake_codex, '--auto', '1', '--interactive', '1'],
140
+ env=env, stdin=subprocess.DEVNULL, stdout=stream, stderr=stream, text=True)
141
+ afirma('· interactive gateway authenticates and opens a child TUI',
142
+ espera(lambda: os.path.exists(status_path)
143
+ and os.path.exists(tui_args) and os.path.exists(tui_pid)),
144
+ lee(log_path))
145
+
146
+ arguments = lee(tui_args).splitlines()
147
+ comprueba('· it uses the official top-level remote TUI command',
148
+ arguments, ['--remote', url])
149
+ comprueba('· the TUI inherits the repo working directory',
150
+ os.path.realpath(lee(tui_cwd).strip()), os.path.realpath(repo))
151
+ log = lee(log_path)
152
+ afirma('· Codex UI replaces the custom city prompt',
153
+ 'FAKE_CODEX_TUI_ATTACHED' in log and 'city> ' not in log, log)
154
+ afirma('· an empty TUI thread is adopted before its rollout exists',
155
+ 'adopted over WebSocket; awaiting its first rollout' in log, log)
156
+ afirma('· happy: a normal TUI question and answer reach City live without a committee',
157
+ espera(lambda: any(
158
+ row.get('kind') == 'conversation.agent'
159
+ and row.get('summary') == 'This city coordinates its repo agents.'
160
+ for row in lineas_json(activity_path)))
161
+ and any(row.get('kind') == 'conversation.user'
162
+ and row.get('summary') == 'What is this city for?'
163
+ for row in lineas_json(activity_path)),
164
+ lee(activity_path))
165
+ diagnostics = lineas_json(diagnostic_path)
166
+ afirma('· the empty TUI adoption and later subscription are durable diagnostics',
167
+ any(row.get('event') == 'codex.thread.adopted' for row in diagnostics)
168
+ and any(row.get('event') == 'codex.thread.joined'
169
+ and row.get('replayedItems') == 2 for row in diagnostics),
170
+ lee(diagnostic_path))
171
+ afirma('· non-happy: private TUI reasoning is never mirrored',
172
+ 'PRIVATE_TUI_REASONING' not in lee(activity_path), lee(activity_path))
173
+
174
+ open(behavior, 'w', encoding='utf-8').write('permission-request\n')
175
+ opened = subprocess.run(
176
+ ['node', CLIENT, 'committee', 'open', '--input', '-'],
177
+ input=json.dumps({
178
+ 'question': 'Prove the attached Codex thread still receives bus work.',
179
+ 'desiredOutcome': 'One native WebSocket acceptance.',
180
+ 'definitionOfDone': ['the provider accepts exactly once'],
181
+ 'participants': ['codex-agent'],
182
+ }), capture_output=True, text=True,
183
+ env=dict(env, CITY_BUS_ACTOR='seat'), timeout=12)
184
+ state = json.loads(opened.stdout) if opened.returncode == 0 else {}
185
+ afirma('· a real city committee assignment reaches the attached thread',
186
+ bool(state.get('id')) and espera(
187
+ lambda: len(solicitudes(capture)) == 1
188
+ and len(aprobaciones(capture)) == 1),
189
+ opened.stderr or lee(log_path))
190
+ delivered = solicitudes(capture)
191
+ approved = aprobaciones(capture)
192
+ afirma('· the materialized first TUI turn completes the real subscription',
193
+ 'Codex TUI thread thread_tui joined over WebSocket' in lee(log_path),
194
+ lee(log_path))
195
+ afirma('· the gateway joins and delivers to the TUI-created thread',
196
+ len(delivered) == 1
197
+ and delivered[0].get('request', {}).get('params', {}).get('threadId')
198
+ == 'thread_tui',
199
+ lee(log_path))
200
+ first_params = delivered[0].get('request', {}).get('params', {})
201
+ afirma('· an explicitly outer-caged gateway avoids a second turn sandbox',
202
+ first_params.get('sandboxPolicy') == {'type': 'dangerFullAccess'}
203
+ and first_params.get('approvalPolicy') == 'on-request',
204
+ json.dumps(first_params))
205
+ first_approval = approved[0].get('approval', {}).get('result', {})
206
+ afirma('· auto mode approves the exact provider permissions for this turn',
207
+ first_approval.get('scope') == 'turn'
208
+ and first_approval.get('permissions', {}).get('network', {}).get('enabled') is True
209
+ and repo in first_approval.get('permissions', {}).get(
210
+ 'fileSystem', {}).get('write', []),
211
+ json.dumps(first_approval))
212
+ time.sleep(.1)
213
+ log = lee(log_path)
214
+ afirma('· the gateway does not print duplicate model deltas over the TUI',
215
+ 'fake codex response' not in log, log)
216
+ afirma('· happy: Codex completed user and agent items reach City live',
217
+ espera(lambda: any(
218
+ row.get('kind') == 'conversation.agent'
219
+ and row.get('summary') == 'fake codex response'
220
+ and row.get('thread') == state.get('id')
221
+ for row in lineas_json(activity_path)))
222
+ and all('[Agents City authenticated local bus]' not in row.get('summary', '')
223
+ and '<channel source="plugin:city:city-bus"'
224
+ not in row.get('summary', '')
225
+ for row in lineas_json(activity_path)),
226
+ lee(activity_path))
227
+ afirma('· non-happy: Codex private reasoning never enters activity storage',
228
+ 'PRIVATE_FAKE_CODEX_REASONING' not in lee(activity_path),
229
+ lee(activity_path))
230
+ status = json.loads(lee(status_path)) if os.path.exists(status_path) else {}
231
+ afirma('· delivery remains native and never injects the terminal',
232
+ status.get('transport') == 'codex-app-server-ws'
233
+ and status.get('terminalInjection') is False,
234
+ str(status))
235
+
236
+ # A second assignment proves thread/resume subscribed the gateway to
237
+ # completion events. Without that subscription the first turn would
238
+ # remain permanently "active" and this request would never be sent.
239
+ opened_again = subprocess.run(
240
+ ['node', CLIENT, 'committee', 'open', '--input', '-'],
241
+ input=json.dumps({
242
+ 'question': 'Prove the resumed Codex thread accepts a second turn.',
243
+ 'desiredOutcome': 'A second ordered native acceptance.',
244
+ 'definitionOfDone': ['the second provider turn starts'],
245
+ 'participants': ['codex-agent'],
246
+ }), capture_output=True, text=True,
247
+ env=dict(env, CITY_BUS_ACTOR='seat'), timeout=12)
248
+ second_state = json.loads(opened_again.stdout) if opened_again.returncode == 0 else {}
249
+ afirma('· completion events unlock a second ordered bus assignment',
250
+ bool(second_state.get('id'))
251
+ and espera(lambda: len(solicitudes(capture)) == 2
252
+ and len(aprobaciones(capture)) == 2),
253
+ opened_again.stderr or lee(log_path))
254
+ comprueba('· both assignments stay on the one TUI-created thread',
255
+ [row['request']['params']['threadId'] for row in solicitudes(capture)],
256
+ ['thread_tui', 'thread_tui'])
257
+
258
+ child_pid = int(lee(tui_pid).strip() or 0)
259
+ afirma('· closing the Codex TUI closes its gateway',
260
+ detiene_proceso(child_pid)
261
+ and espera(lambda: gateway.poll() is not None and not os.path.exists(status_path)),
262
+ lee(log_path))
263
+
264
+ # Non-happy path: without auto approval the connector must answer the
265
+ # provider request explicitly with no granted permissions. It still
266
+ # uses the native protocol and stays alive; it does not turn `never`
267
+ # into a mysterious tool failure.
268
+ stream.close()
269
+ stream = None
270
+ non_auto_log = os.path.join(base, 'gateway-non-auto.log')
271
+ stream = open(non_auto_log, 'w+', encoding='utf-8')
272
+ non_auto_env = dict(env)
273
+ non_auto_env.pop('CITY_OUTER_CAGE', None)
274
+ gateway = subprocess.Popen(
275
+ ['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
276
+ '--cwd', repo, '--command', fake_codex, '--auto', '0', '--interactive', '0'],
277
+ env=non_auto_env, stdin=subprocess.DEVNULL,
278
+ stdout=stream, stderr=stream, text=True)
279
+ afirma('· non-happy: a non-auto native gateway still starts cleanly',
280
+ espera(lambda: os.path.exists(status_path)), lee(non_auto_log))
281
+ non_auto_opened = subprocess.run(
282
+ ['node', CLIENT, 'committee', 'open', '--input', '-'],
283
+ input=json.dumps({
284
+ 'question': 'Prove non-auto permissions remain denied.',
285
+ 'desiredOutcome': 'One explicit denial without a crash.',
286
+ 'definitionOfDone': ['the provider receives an empty grant'],
287
+ 'participants': ['codex-agent'],
288
+ }), capture_output=True, text=True,
289
+ env=dict(non_auto_env, CITY_BUS_ACTOR='seat'), timeout=12)
290
+ afirma('· non-happy: provider approval is answered instead of hanging',
291
+ non_auto_opened.returncode == 0
292
+ and espera(lambda: len(solicitudes(capture)) == 3
293
+ and len(aprobaciones(capture)) == 3),
294
+ non_auto_opened.stderr or lee(non_auto_log))
295
+ non_auto_params = solicitudes(capture)[-1]['request']['params']
296
+ non_auto_approval = aprobaciones(capture)[-1]['approval'].get('result', {})
297
+ afirma('· non-happy: no outer cage keeps Codex workspace confinement',
298
+ non_auto_params.get('sandboxPolicy', {}).get('type') == 'workspaceWrite'
299
+ and non_auto_params.get('sandboxPolicy', {}).get('networkAccess') is False,
300
+ json.dumps(non_auto_params))
301
+ afirma('· non-happy: disabled auto approval grants no requested capability',
302
+ non_auto_approval == {'permissions': {}, 'scope': 'turn'},
303
+ json.dumps(non_auto_approval))
304
+ gateway.terminate()
305
+ afirma('· non-happy: the non-auto gateway cleans up normally',
306
+ espera(lambda: gateway.poll() is not None and not os.path.exists(status_path)),
307
+ lee(non_auto_log))
308
+
309
+ # Non-happy path: a TUI process can stay alive without ever creating a
310
+ # usable thread. The gateway must time out and must not adopt a stale
311
+ # or unrelated thread from the app-server.
312
+ stream.close()
313
+ stream = None
314
+ open(behavior, 'w', encoding='utf-8').write('missing-tui-thread\n')
315
+ missing_log = os.path.join(base, 'gateway-missing-thread.log')
316
+ stream = open(missing_log, 'w+', encoding='utf-8')
317
+ missing_env = dict(env, CITY_CODEX_TUI_READY_TIMEOUT_MS='350')
318
+ gateway = subprocess.Popen(
319
+ ['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
320
+ '--cwd', repo, '--command', fake_codex,
321
+ '--auto', '1', '--interactive', '1'],
322
+ env=missing_env, stdin=subprocess.DEVNULL,
323
+ stdout=stream, stderr=stream, text=True)
324
+ afirma('· non-happy: a TUI without a new thread fails visibly',
325
+ espera(lambda: gateway.poll() is not None)
326
+ and gateway.returncode != 0
327
+ and 'did not create a new thread' in lee(missing_log),
328
+ lee(missing_log))
329
+ afirma('· non-happy: a missing thread leaves no online gateway status',
330
+ not os.path.exists(status_path), lee(missing_log))
331
+ afirma('· non-happy: a missing Codex thread leaves durable diagnostics',
332
+ 'gateway.start.failed' in lee(diagnostic_path)
333
+ and 'did not create a new thread' in lee(diagnostic_path),
334
+ lee(diagnostic_path))
335
+
336
+ # Non-happy path: a supported runtime can still fail in its own UI.
337
+ # That must be visible and terminal, never a silent downgrade to the
338
+ # gateway readline prompt or to terminal injection.
339
+ stream.close()
340
+ stream = None
341
+ failing_dir = os.path.join(base, 'failing')
342
+ os.makedirs(failing_dir)
343
+ failing_codex = os.path.join(failing_dir, 'codex')
344
+ open(behavior, 'w', encoding='utf-8').write('healthy\n')
345
+ open(failing_codex, 'w', encoding='utf-8').write(
346
+ '#!/bin/sh\necho "fake Codex TUI startup failure" >&2\nexit 42\n')
347
+ os.chmod(failing_codex, 0o755)
348
+ failure_log = os.path.join(base, 'gateway-failure.log')
349
+ stream = open(failure_log, 'w+', encoding='utf-8')
350
+ gateway = subprocess.Popen(
351
+ ['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
352
+ '--cwd', repo, '--command', failing_codex,
353
+ '--auto', '1', '--interactive', '1'],
354
+ env=env, stdin=subprocess.DEVNULL, stdout=stream, stderr=stream, text=True)
355
+ afirma('· non-happy: a broken Codex TUI makes the gateway fail visibly',
356
+ espera(lambda: gateway.poll() is not None)
357
+ and gateway.returncode == 42
358
+ and 'fake Codex TUI startup failure' in lee(failure_log),
359
+ lee(failure_log))
360
+ failure = lee(failure_log)
361
+ afirma('· non-happy: failure cleans status without console or paste fallback',
362
+ not os.path.exists(status_path)
363
+ and 'city> ' not in failure
364
+ and 'terminalInjection' not in failure,
365
+ failure)
366
+
367
+ # Non-happy path: a stale global MCP entry may point to a local binary
368
+ # that was removed with an old worktree. The city must disable exactly
369
+ # that integration for both app-server and its remote TUI, without
370
+ # rewriting the owner's Codex config or disabling healthy MCPs.
371
+ if server and server.poll() is None:
372
+ server.terminate()
373
+ server.wait(timeout=3)
374
+ server = None
375
+ stale_bin = os.path.join(base, 'stale-bin')
376
+ os.makedirs(stale_bin)
377
+ stale_codex = os.path.join(stale_bin, 'codex')
378
+ stale_server_args = os.path.join(base, 'stale-server-args.txt')
379
+ stale_tui_args = os.path.join(base, 'stale-tui-args.txt')
380
+ stale_tui_pid = os.path.join(base, 'stale-tui.pid')
381
+ stale_capture = os.path.join(base, 'stale-codex.jsonl')
382
+ stale_behavior = os.path.join(base, 'stale-behavior.txt')
383
+ missing_mcp = os.path.join(base, 'removed-worktree', 'missing-sidecar')
384
+ mcp_listing = json.dumps([
385
+ {
386
+ 'name': 'broken-local', 'enabled': True,
387
+ 'transport': {'type': 'stdio', 'command': missing_mcp, 'cwd': None},
388
+ },
389
+ {
390
+ 'name': 'healthy-local', 'enabled': True,
391
+ 'transport': {'type': 'stdio', 'command': sys.executable, 'cwd': None},
392
+ },
393
+ ])
394
+ open(stale_behavior, 'w', encoding='utf-8').write('healthy\n')
395
+ open(stale_codex, 'w', encoding='utf-8').write(
396
+ f'#!{sys.executable}\n'
397
+ 'import os, signal, sys, time\n'
398
+ 'args = sys.argv[1:]\n'
399
+ f'listing = {mcp_listing!r}\n'
400
+ 'if args == ["mcp", "list", "--json"]:\n'
401
+ ' print(listing)\n'
402
+ ' raise SystemExit(0)\n'
403
+ 'if args and args[0] == "app-server":\n'
404
+ ' open(os.environ["CITY_TEST_SERVER_ARGS"], "w").write("\\n".join(args) + "\\n")\n'
405
+ ' url = args[args.index("--listen") + 1]\n'
406
+ ' port = url.rsplit(":", 1)[1]\n'
407
+ f' os.execv({shutil.which("node")!r}, '
408
+ f'[{shutil.which("node")!r}, {FAKE!r}, "codex", port, '
409
+ 'os.environ["CITY_TEST_STALE_CAPTURE"], '
410
+ 'os.environ["CITY_TEST_STALE_BEHAVIOR"]])\n'
411
+ 'if "--remote" in args:\n'
412
+ ' open(os.environ["CITY_TEST_STALE_TUI_ARGS"], "w").write('
413
+ '"\\n".join(args) + "\\n")\n'
414
+ ' open(os.environ["CITY_TEST_STALE_TUI_PID"], "w").write('
415
+ 'str(os.getpid()) + "\\n")\n'
416
+ ' def stop(_signal, _frame): raise SystemExit(0)\n'
417
+ ' signal.signal(signal.SIGINT, stop)\n'
418
+ ' signal.signal(signal.SIGTERM, stop)\n'
419
+ ' signal.signal(signal.SIGHUP, stop)\n'
420
+ ' while True: time.sleep(1)\n'
421
+ 'raise SystemExit(64)\n')
422
+ os.chmod(stale_codex, 0o755)
423
+ stale_env = dict(env,
424
+ CITY_TEST_SERVER_ARGS=stale_server_args,
425
+ CITY_TEST_STALE_TUI_ARGS=stale_tui_args,
426
+ CITY_TEST_STALE_TUI_PID=stale_tui_pid,
427
+ CITY_TEST_STALE_CAPTURE=stale_capture,
428
+ CITY_TEST_STALE_BEHAVIOR=stale_behavior)
429
+ stale_env.pop('CITY_CODEX_APP_SERVER_URL', None)
430
+ stale_env.pop('CITY_OUTER_CAGE', None)
431
+ stale_log = os.path.join(base, 'gateway-stale-mcp.log')
432
+ stream.close()
433
+ stream = open(stale_log, 'w+', encoding='utf-8')
434
+ gateway = subprocess.Popen(
435
+ ['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
436
+ '--cwd', repo, '--command', stale_codex,
437
+ '--auto', '1', '--interactive', '1'],
438
+ env=stale_env, stdin=subprocess.DEVNULL,
439
+ stdout=stream, stderr=stream, text=True)
440
+ afirma('· non-happy: Codex starts even when one global MCP executable is gone',
441
+ espera(lambda: os.path.exists(status_path)
442
+ and os.path.exists(stale_server_args)
443
+ and os.path.exists(stale_tui_args)
444
+ and os.path.exists(stale_tui_pid)),
445
+ lee(stale_log))
446
+ server_arguments = lee(stale_server_args).splitlines()
447
+ stale_ui_arguments = lee(stale_tui_args).splitlines()
448
+ expected_override = 'mcp_servers.broken-local.enabled=false'
449
+ afirma('· only the unavailable MCP is disabled for app-server and the TUI',
450
+ expected_override in server_arguments
451
+ and expected_override in stale_ui_arguments
452
+ and not any('healthy-local' in value
453
+ for value in server_arguments + stale_ui_arguments),
454
+ json.dumps({'server': server_arguments, 'tui': stale_ui_arguments}))
455
+ afirma('· the owner config is untouched and the scoped MCP decision is diagnostic',
456
+ any(row.get('event') == 'codex.mcp.unavailable.disabled'
457
+ and row.get('message') == 'broken-local'
458
+ for row in lineas_json(diagnostic_path)),
459
+ lee(diagnostic_path)[-1200:])
460
+ stale_child_pid = int(lee(stale_tui_pid).strip() or 0)
461
+ afirma('· stale-MCP test gateway closes with its official TUI',
462
+ detiene_proceso(stale_child_pid)
463
+ and espera(lambda: gateway.poll() is not None
464
+ and not os.path.exists(status_path)),
465
+ lee(stale_log))
466
+ finally:
467
+ if gateway and gateway.poll() is None:
468
+ gateway.terminate()
469
+ try:
470
+ gateway.wait(timeout=3)
471
+ except subprocess.TimeoutExpired:
472
+ gateway.kill()
473
+ if server and server.poll() is None:
474
+ server.terminate()
475
+ try:
476
+ server.wait(timeout=3)
477
+ except subprocess.TimeoutExpired:
478
+ server.kill()
479
+ if stream:
480
+ stream.close()
481
+ afirma('· interactive cleanup leaves no orphan city hub',
482
+ detiene_hubs_de_ciudad(city), str(city))
483
+ shutil.rmtree(base, ignore_errors=True)
484
+ return resumen('runtime-ui')
485
+
486
+
487
+ if __name__ == '__main__':
488
+ sys.exit(main())