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,164 @@
1
+ #!/usr/bin/env python3
2
+ """Polymorphic growth: a house grows by what its agent actually does.
3
+
4
+ Under a throwaway data dir with a fixed clock (CITY_GROWTH_NOW). The point of
5
+ the suite: a knowledge agent with zero git grows from its documents, a
6
+ coordinator from its decisions, and a code agent uses an injected PR counter —
7
+ so nobody is stuck at an empty lot for lacking a repo.
8
+ """
9
+
10
+ import contextlib
11
+ import os
12
+ import shutil
13
+ import sys
14
+ import tempfile
15
+
16
+ AQUI = os.path.dirname(os.path.abspath(__file__))
17
+ RAIZ = os.path.dirname(AQUI)
18
+ sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
19
+ sys.path.insert(0, AQUI)
20
+
21
+ import crecimiento as cr # noqa: E402
22
+ import workspace as ws # noqa: E402
23
+ from testlib import afirma, comprueba, resumen # noqa: E402
24
+
25
+
26
+ @contextlib.contextmanager
27
+ def _reloj_fijo(epoch="1000000000"):
28
+ """Pin CITY_GROWTH_NOW, restoring any prior value rather than clobbering it."""
29
+ previo = os.environ.get("CITY_GROWTH_NOW")
30
+ os.environ["CITY_GROWTH_NOW"] = epoch
31
+ try:
32
+ yield
33
+ finally:
34
+ if previo is None:
35
+ os.environ.pop("CITY_GROWTH_NOW", None)
36
+ else:
37
+ os.environ["CITY_GROWTH_NOW"] = previo
38
+
39
+
40
+ def _agente(nombre, clase, data, mounts=()):
41
+ return ws.Agente(nombre, nombre, "blank", "claude", clase,
42
+ ws.workspace_de(data, nombre), list(mounts), legacy=False)
43
+
44
+
45
+ def knowledge_crece_sin_git():
46
+ base = tempfile.mkdtemp(prefix="agents-city-grow-")
47
+ try:
48
+ with _reloj_fijo():
49
+ docs = os.path.join(base, "handbook")
50
+ os.makedirs(docs)
51
+ for n in ("a.md", "b.txt", "c.pdf", "ignore.py"):
52
+ with open(os.path.join(docs, n), "w") as f:
53
+ f.write("x")
54
+ os.utime(os.path.join(docs, "a.md"), (1000000000 - 100, 1000000000 - 100)) # recent
55
+ os.utime(os.path.join(docs, "b.txt"), (1, 1)) # old
56
+ ws.crea_workspace(base, "writer")
57
+ ws.monta(base, "writer", docs)
58
+ a = _agente("writer", "knowledge", base)
59
+ r = cr.crece(a, base)
60
+ comprueba("floors count the documents, not the code files", r["floors"], 3)
61
+ comprueba("the signal names documents", r["signal"], "documents")
62
+ afirma("recent edits count as activity", r["activity30"] >= 1)
63
+ finally:
64
+ shutil.rmtree(base, ignore_errors=True)
65
+
66
+
67
+ def knowledge_suma_workspace_y_mounts():
68
+ # Mounting a folder must ADD to the house, never replace the workspace:
69
+ # the `or`-fallback once made an agent's own documents vanish the moment
70
+ # it mounted anything — an empty mount shrank the house to zero.
71
+ base = tempfile.mkdtemp(prefix="agents-city-grow-")
72
+ try:
73
+ with _reloj_fijo():
74
+ ws.crea_workspace(base, "writer")
75
+ taller = ws.workspace_de(base, "writer")
76
+ for n in ("propio.md", "notas.txt"):
77
+ with open(os.path.join(taller, n), "w") as f:
78
+ f.write("x")
79
+ a = _agente("writer", "knowledge", base)
80
+ antes = cr.crece(a, base)["floors"]
81
+ vacio = os.path.join(base, "vacio")
82
+ os.makedirs(vacio)
83
+ ws.monta(base, "writer", vacio)
84
+ r = cr.crece(a, base)
85
+ comprueba("an empty mount never shrinks the house", r["floors"], antes)
86
+ lleno = os.path.join(base, "docs")
87
+ os.makedirs(lleno)
88
+ with open(os.path.join(lleno, "extra.md"), "w") as f:
89
+ f.write("x")
90
+ ws.monta(base, "writer", lleno)
91
+ comprueba("a mounted document adds to the workspace's own",
92
+ cr.crece(a, base)["floors"], antes + 1)
93
+ finally:
94
+ shutil.rmtree(base, ignore_errors=True)
95
+
96
+
97
+ def _pon_deliberacion(base, ident, updated_at):
98
+ # The real store shape: deliberations/<id>/state.json, schema-tagged.
99
+ import json
100
+ carpeta = os.path.join(base, "deliberations", ident)
101
+ os.makedirs(carpeta)
102
+ estado = {"schema": "agents-city/deliberation@1", "status": "decided",
103
+ "brief": {"question": "q", "participants": []},
104
+ "decisions": [{"outcome": "go"}], "updatedAt": updated_at}
105
+ with open(os.path.join(carpeta, "state.json"), "w") as f:
106
+ json.dump(estado, f)
107
+
108
+
109
+ def coordinator_crece_por_decisiones():
110
+ # The id must match deliberations.ID_RE: delib_<ts>_<hex>.
111
+ base = tempfile.mkdtemp(prefix="agents-city-grow-")
112
+ try:
113
+ with _reloj_fijo():
114
+ _pon_deliberacion(base, "delib_20260101010101_aaaaaaaa", "2001-09-09T01:00:00+00:00")
115
+ _pon_deliberacion(base, "delib_20260101010102_bbbbbbbb", "2001-09-09T01:00:00+00:00")
116
+ a = _agente("chair", "coordinator", base)
117
+ r = cr.crece(a, base)
118
+ comprueba("real deliberation directories become floors (not always 0)",
119
+ r["floors"], 2)
120
+ comprueba("the signal names decisions", r["signal"], "decisions")
121
+ finally:
122
+ shutil.rmtree(base, ignore_errors=True)
123
+
124
+
125
+ def code_usa_contador_inyectado():
126
+ base = tempfile.mkdtemp(prefix="agents-city-grow-")
127
+ try:
128
+ a = _agente("nova", "code", base)
129
+ comprueba("without a git counter a code house is empty, not crashed",
130
+ cr.crece(a, base)["floors"], 0)
131
+ r = cr.crece(a, base, contador_git=lambda ag, d: (7, 3, 5))
132
+ comprueba("with the injected counter, floors are merged PRs", r["floors"], 7)
133
+ comprueba("bricks are commits", r["bricks"], 3)
134
+ comprueba("the signal names pull requests", r["signal"], "pull-requests")
135
+ finally:
136
+ shutil.rmtree(base, ignore_errors=True)
137
+
138
+
139
+ def forma_uniforme():
140
+ base = tempfile.mkdtemp(prefix="agents-city-grow-")
141
+ try:
142
+ for clase in ("code", "knowledge", "coordinator"):
143
+ r = cr.crece(_agente("x", clase, base), base)
144
+ afirma(f"{clase} returns the uniform shape",
145
+ set(r) == {"floors", "bricks", "activity30", "signal"})
146
+ finally:
147
+ shutil.rmtree(base, ignore_errors=True)
148
+
149
+
150
+ def fecha_naive_es_utc():
151
+ # A naive timestamp must be read as UTC, not local, or recency drifts by the
152
+ # machine's offset. epoch 1e9 == 2001-09-09T01:46:40Z; a naive stamp one
153
+ # minute earlier is recent regardless of the runner's timezone.
154
+ comprueba("a naive ISO timestamp is parsed as UTC",
155
+ round(cr._epoch("2001-09-09T01:45:40")), 999999940)
156
+
157
+
158
+ knowledge_crece_sin_git()
159
+ knowledge_suma_workspace_y_mounts()
160
+ coordinator_crece_por_decisiones()
161
+ code_usa_contador_inyectado()
162
+ forma_uniforme()
163
+ fecha_naive_es_utc()
164
+ sys.exit(resumen("crecimiento"))
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ """The account-free product demo, over the same real local WebSocket bus.
3
+
4
+ The happy path runs Aurora's complete committee against an ephemeral city and
5
+ asserts the durable spectator stream. Non-happy checks prove the script refuses
6
+ an invalid roster, a missing Hall spectator and impossible timing without ever
7
+ opening a browser, starting tmux or contacting a model provider.
8
+ """
9
+ import glob
10
+ import json
11
+ import os
12
+ import shutil
13
+ import signal
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import time
18
+
19
+
20
+ AQUI = os.path.dirname(os.path.abspath(__file__))
21
+ RAIZ = os.path.dirname(AQUI)
22
+ SHOW = os.path.join(RAIZ, "demo", "show.py")
23
+ FIXTURE = os.path.join(RAIZ, "demo", "city")
24
+ sys.path.insert(0, AQUI)
25
+ from testlib import afirma, comprueba, resumen # noqa: E402
26
+
27
+
28
+ def city(valid=True):
29
+ base = tempfile.mkdtemp(prefix="agents-city-demo-test-")
30
+ data = os.path.join(base, "city")
31
+ app = os.path.join(base, "app")
32
+ shutil.copytree(FIXTURE, data)
33
+ os.makedirs(app)
34
+ if not valid:
35
+ card_path = os.path.join(data, "ada.md")
36
+ card_text = open(card_path, encoding="utf-8").read()
37
+ card_text = card_text.replace(
38
+ "repos: [nova, store-service, telemetry-collector, engine-core, launcher]",
39
+ "repos: [nova, store-service]",
40
+ )
41
+ open(card_path, "w", encoding="utf-8").write(card_text)
42
+ env = dict(
43
+ os.environ,
44
+ AGENTS_CITY_HOME=app,
45
+ AGENTS_CITY_DATA=data,
46
+ AGENTS_CITY_USER="ada",
47
+ CITY_ADDRESS="ada/aurora-games",
48
+ )
49
+ for key in ("CITY_BUS_URL", "CITY_BUS_TOKEN", "CITY_DIR"):
50
+ env.pop(key, None)
51
+ return base, data, app, env
52
+
53
+
54
+ def run(env, *args):
55
+ return subprocess.run(
56
+ [sys.executable, SHOW, *args],
57
+ capture_output=True,
58
+ text=True,
59
+ env=env,
60
+ timeout=25,
61
+ )
62
+
63
+
64
+ def activities(app):
65
+ paths = glob.glob(os.path.join(app, ".runtime", "bus", "*", "activity.jsonl"))
66
+ if not paths:
67
+ return []
68
+ return [json.loads(line) for line in open(paths[0], encoding="utf-8") if line.strip()]
69
+
70
+
71
+ def stop_hubs(app):
72
+ for path in glob.glob(os.path.join(app, ".runtime", "bus", "*", "endpoint.json")):
73
+ try:
74
+ pid = int(json.load(open(path, encoding="utf-8"))["pid"])
75
+ os.kill(pid, signal.SIGTERM)
76
+ except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
77
+ pass
78
+ time.sleep(0.1)
79
+
80
+
81
+ def ordered(actual, expected):
82
+ cursor = 0
83
+ for item in actual:
84
+ if cursor < len(expected) and item == expected[cursor]:
85
+ cursor += 1
86
+ return cursor == len(expected)
87
+
88
+
89
+ def main():
90
+ global FIXTURE
91
+ print("\n full Aurora demo on the real local bus")
92
+ roots = []
93
+ try:
94
+ base, data, app, env = city()
95
+ roots.append((base, app))
96
+ result = run(env, "--no-wait", "--step", "0")
97
+ events = activities(app)
98
+ kinds = [event.get("kind") for event in events]
99
+ afirma(
100
+ "· happy: the guided demo completes without a provider account",
101
+ result.returncode == 0 and "Guided committee complete" in result.stdout,
102
+ result.stderr,
103
+ )
104
+ comprueba("· it produces the complete visible conversation", len(events), 22)
105
+ afirma(
106
+ "· every item crossed the authenticated activity protocol",
107
+ all(event.get("protocol") == "agents-city-activity/1" for event in events),
108
+ )
109
+ afirma(
110
+ "· the first blind round reveals three repo agents only after its barrier",
111
+ kinds.count("committee.position.submitted") == 3
112
+ and kinds.count("committee.position.revealed") == 3
113
+ and kinds.index("committee.positions.revealed")
114
+ < kinds.index("committee.position.revealed"),
115
+ str(kinds),
116
+ )
117
+ afirma(
118
+ "· the game speakers are the chair and real repo actors, never fake people",
119
+ {event["actor"] for event in events}
120
+ == {"seat", "nova", "store-service", "telemetry-collector"},
121
+ str(sorted({event["actor"] for event in events})),
122
+ )
123
+ afirma(
124
+ "· two specialists request permission, receive it and speak to the committee",
125
+ kinds.count("committee.floor.requested") == 2
126
+ and kinds.count("committee.floor.granted") == 2
127
+ and kinds.count("committee.floor.spoke") == 2
128
+ and all(
129
+ event.get("target") == "committee"
130
+ for event in events
131
+ if event.get("kind") == "committee.floor.spoke"
132
+ ),
133
+ str(kinds),
134
+ )
135
+ afirma(
136
+ "· decision, independent verification and closure stay ordered",
137
+ ordered(
138
+ kinds,
139
+ [
140
+ "committee.opened",
141
+ "committee.positions.revealed",
142
+ "committee.synthesis.published",
143
+ "committee.floor.spoke",
144
+ "committee.decision.recorded",
145
+ "committee.verification.pass",
146
+ "committee.closed",
147
+ ],
148
+ ),
149
+ str(kinds),
150
+ )
151
+ seqs = [event["seq"] for event in events]
152
+ afirma("· its WebSocket ledger is monotonic and duplicate-free", seqs == list(range(1, 23)))
153
+ # The story deliberately walks the failure lane: a verification that
154
+ # fails, a replan, and only then a verified close.
155
+ afirma(
156
+ "· the first plan fails verification and the chair replans before closing",
157
+ kinds.count("committee.verification.fail") == 1
158
+ and kinds.count("committee.replanned") == 1
159
+ and kinds.index("committee.verification.fail")
160
+ < kinds.index("committee.replanned")
161
+ < kinds.index("committee.verification.pass"),
162
+ str(kinds),
163
+ )
164
+ acts = glob.glob(os.path.join(data, "deliberations", "*", "ACT.md"))
165
+ afirma(
166
+ "· the same demo leaves a human-readable verified act",
167
+ len(acts) == 1
168
+ and "Partidas restauradas" in open(acts[0], encoding="utf-8").read()
169
+ and "## Verification" in open(acts[0], encoding="utf-8").read(),
170
+ str(acts),
171
+ )
172
+
173
+ card = open(os.path.join(FIXTURE, "ada.md"), encoding="utf-8").read()
174
+ afirma(
175
+ "· Aurora demonstrates role-per-repo and mixed runtime metadata",
176
+ all(
177
+ token in card
178
+ for token in (
179
+ "role.nova: po",
180
+ "role.store-service: security",
181
+ "runs.nova: claude",
182
+ "runs.store-service: codex",
183
+ "runs.telemetry-collector: opencode",
184
+ "runs.engine-core: kimi",
185
+ )
186
+ ),
187
+ )
188
+
189
+ # Every domain's story plays on its own city, with its own cast. This
190
+ # is also the regression net for agents-first on the bus: the clinic
191
+ # and the firm declare `agents:` (knowledge/coordinator), not `repos:`,
192
+ # so these two runs fail loudly if the roster ever regresses to
193
+ # repos-only.
194
+ fixture_original = FIXTURE
195
+ for fixture, historia, reparto in (
196
+ ("clinica", "medicina", {"seat", "urgencias", "laboratorio", "farmacia"}),
197
+ ("despacho", "legal", {"seat", "contratos", "litigios", "archivo"}),
198
+ ):
199
+ FIXTURE = os.path.join(RAIZ, "demo", fixture)
200
+ dom_base, _, dom_app, dom_env = city()
201
+ roots.append((dom_base, dom_app))
202
+ dom_result = run(dom_env, "--no-wait", "--step", "0", "--story", historia)
203
+ dom_events = activities(dom_app)
204
+ dom_kinds = [event.get("kind") for event in dom_events]
205
+ afirma(
206
+ f"· the {historia} story plays on its own city with agents-first actors",
207
+ dom_result.returncode == 0
208
+ and len(dom_events) == 22
209
+ and {event["actor"] for event in dom_events} == reparto
210
+ and dom_kinds.count("committee.verification.fail") == 1
211
+ and dom_kinds.count("committee.replanned") == 1,
212
+ dom_result.stderr + str(sorted({event["actor"] for event in dom_events})),
213
+ )
214
+ FIXTURE = fixture_original
215
+
216
+ bad_base, _, bad_app, bad_env = city(valid=False)
217
+ roots.append((bad_base, bad_app))
218
+ rejected = run(bad_env, "--no-wait", "--step", "0")
219
+ rejected_events = activities(bad_app)
220
+ afirma(
221
+ "· non-happy: a story cannot impersonate an agent absent from the roster",
222
+ rejected.returncode != 0
223
+ and "not a repo support agent" in rejected.stderr
224
+ and [event.get("kind") for event in rejected_events]
225
+ == ["committee.command.rejected"],
226
+ rejected.stderr + json.dumps(rejected_events),
227
+ )
228
+
229
+ wait_base, _, wait_app, wait_env = city()
230
+ roots.append((wait_base, wait_app))
231
+ no_hall = run(wait_env, "--wait-timeout", "0", "--step", "0")
232
+ afirma(
233
+ "· non-happy: autoplay refuses to run before a Hall spectator connects",
234
+ no_hall.returncode != 0
235
+ and "Hall did not connect" in no_hall.stderr
236
+ and not activities(wait_app),
237
+ no_hall.stderr,
238
+ )
239
+
240
+ timing = run(env, "--step", "-1")
241
+ afirma(
242
+ "· non-happy: an impossible negative turn delay is rejected at the CLI",
243
+ timing.returncode != 0 and "zero or greater" in timing.stderr,
244
+ timing.stderr,
245
+ )
246
+
247
+ invented = run(env, "--no-wait", "--step", "0", "--story", "cocina")
248
+ afirma(
249
+ "· non-happy: a story that does not exist is rejected before touching the bus",
250
+ invented.returncode != 0 and "invalid choice" in invented.stderr,
251
+ invented.stderr,
252
+ )
253
+
254
+ # Two demos may seed at once (one per domain): the seeder must never
255
+ # write a fixed path two runs would overwrite mid-flight.
256
+ semilla = open(os.path.join(RAIZ, "city", "scripts", "seed.py"), encoding="utf-8").read()
257
+ afirma(
258
+ "· concurrent demos cannot clobber each other's seed SQL",
259
+ "mkstemp" in semilla and "/tmp/agents-city-seed.sql" not in semilla,
260
+ )
261
+
262
+ help_result = subprocess.run(
263
+ [os.path.join(RAIZ, "bin", "demo"), "--help"],
264
+ capture_output=True,
265
+ text=True,
266
+ timeout=5,
267
+ )
268
+ afirma(
269
+ "· the public demo explains Hall, WebSockets, bubbles and account-free use",
270
+ help_result.returncode == 0
271
+ and all(
272
+ token in help_result.stdout
273
+ for token in ("Town Hall", "WebSocket", "speech bubbles", "No Claude/Codex account")
274
+ ),
275
+ help_result.stdout + help_result.stderr,
276
+ )
277
+
278
+ launcher = open(os.path.join(RAIZ, "bin", "demo"), encoding="utf-8").read()
279
+ worker = open(
280
+ os.path.join(RAIZ, "city", "worker", "src", "index.ts"), encoding="utf-8"
281
+ ).read()
282
+ hall = open(os.path.join(RAIZ, "bin", "serve.py"), encoding="utf-8").read()
283
+ afirma(
284
+ "· the disposable launcher owns and cleans only its temporary runtime",
285
+ "mktemp -d" in launcher
286
+ and "trap cleanup EXIT" in launcher
287
+ and 'AGENTS_CITY_HOME=$DEMO_APP' in launcher
288
+ and 'if [ "$ABRIR" -eq 1 ]; then' in launcher
289
+ and "HALL_ARGS" not in launcher
290
+ and "~/.agents-city/state/demo" not in launcher,
291
+ )
292
+ afirma(
293
+ "· Hall discovers the matching city map instead of the first open map port",
294
+ "/api/identity" in worker
295
+ and "CITY_ID?: string" in worker
296
+ and "identidad == esperada" in hall
297
+ and "range(8787, 8797)" in hall,
298
+ )
299
+ finally:
300
+ for base, app in roots:
301
+ stop_hubs(app)
302
+ shutil.rmtree(base, ignore_errors=True)
303
+
304
+ return resumen("demo")
305
+
306
+
307
+ if __name__ == "__main__":
308
+ sys.exit(main())
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """doctor: an old config is detected, explained, backed up, rewritten — once.
3
+
4
+ Runs against a temp file. The load-bearing cases: migrations are idempotent
5
+ (re-running changes nothing), a dry-run never writes, a real fix leaves a
6
+ backup, and several migrations converge in one pass.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import shutil
12
+ import sys
13
+ import tempfile
14
+
15
+ AQUI = os.path.dirname(os.path.abspath(__file__))
16
+ RAIZ = os.path.dirname(AQUI)
17
+ sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
18
+ sys.path.insert(0, AQUI)
19
+
20
+ import doctor # noqa: E402
21
+ from testlib import afirma, comprueba, resumen # noqa: E402
22
+
23
+
24
+ def diagnostico():
25
+ viejo = {"busUrl": "https://x", "gateway": {}}
26
+ pendientes = [m.id for m in doctor.diagnostica(viejo)]
27
+ afirma("the renamed key is detected", "bus-url-renamed-to-roads-url" in pendientes)
28
+ afirma("the missing bind default is detected",
29
+ "gateway-bind-defaults-to-loopback" in pendientes)
30
+ nuevo, _ = doctor.cura(viejo)
31
+ afirma("busUrl becomes roadsUrl", "roadsUrl" in nuevo and "busUrl" not in nuevo)
32
+ comprueba("the value is carried over", nuevo["roadsUrl"], "https://x")
33
+ comprueba("the gateway bind is pinned to loopback", nuevo["gateway"]["bind"], "loopback")
34
+ afirma("the cage default is recorded on", nuevo["security"]["cage"] is True)
35
+
36
+
37
+ def configs_malformadas():
38
+ # doctor must not crash on the malformed config it exists to repair.
39
+ for malo in ({"security": None}, {"security": "on"}, {"security": ["x"]}, {"security": 3}):
40
+ try:
41
+ nuevo, _ = doctor.cura(malo)
42
+ afirma(f"a non-dict security ({malo['security']!r}) does not crash cura", True)
43
+ afirma("and such a config is left untouched (no bogus migration)",
44
+ nuevo == malo)
45
+ except (TypeError, ValueError):
46
+ afirma(f"a non-dict security ({malo['security']!r}) does not crash cura", False)
47
+
48
+
49
+ def idempotencia():
50
+ canonico = {"roadsUrl": "https://x", "gateway": {"bind": "loopback"},
51
+ "security": {"cage": True}}
52
+ comprueba("a canonical config needs no migration", doctor.diagnostica(canonico), [])
53
+ _, aplicadas = doctor.cura(canonico)
54
+ comprueba("curing a canonical config changes nothing", aplicadas, [])
55
+ curado, _ = doctor.cura({"busUrl": "u"})
56
+ _, otra_vez = doctor.cura(curado)
57
+ comprueba("a second pass is a no-op (idempotent)", otra_vez, [])
58
+
59
+
60
+ def fichero():
61
+ base = tempfile.mkdtemp(prefix="agents-city-doctor-")
62
+ try:
63
+ ruta = os.path.join(base, "openclaw.json")
64
+ with open(ruta, "w") as f:
65
+ json.dump({"busUrl": "https://x", "gateway": {}}, f)
66
+ seco = doctor.cura_fichero(ruta, "T1", dry_run=True)
67
+ afirma("a dry-run reports migrations", seco["migrations"])
68
+ afirma("a dry-run writes nothing", not seco["wrote"] and seco["backup"] is None)
69
+ antes = open(ruta).read()
70
+ afirma("the file is untouched after dry-run", json.loads(antes).get("busUrl") == "https://x")
71
+
72
+ rep = doctor.cura_fichero(ruta, "T1", dry_run=False)
73
+ afirma("a real fix wrote the file", rep["wrote"])
74
+ afirma("a backup was left", os.path.exists(rep["backup"]))
75
+ despues = json.load(open(ruta))
76
+ afirma("the rewritten file is canonical",
77
+ "roadsUrl" in despues and despues["gateway"]["bind"] == "loopback")
78
+ afirma("the backup preserved the original shape",
79
+ json.load(open(rep["backup"])).get("busUrl") == "https://x")
80
+
81
+ rep2 = doctor.cura_fichero(ruta, "T2", dry_run=False)
82
+ afirma("a canonical file needs no second rewrite", not rep2["wrote"])
83
+ finally:
84
+ shutil.rmtree(base, ignore_errors=True)
85
+
86
+
87
+ def preserva_permisos():
88
+ base = tempfile.mkdtemp(prefix="agents-city-doctor-")
89
+ try:
90
+ ruta = os.path.join(base, "openclaw.json")
91
+ with open(ruta, "w") as f:
92
+ json.dump({"busUrl": "https://x", "gateway": {}}, f)
93
+ os.chmod(ruta, 0o600)
94
+ doctor.cura_fichero(ruta, "T1", dry_run=False)
95
+ modo = oct(os.stat(ruta).st_mode & 0o777)
96
+ comprueba("a 0600 config stays 0600 after a rewrite (never widened)", modo, "0o600")
97
+ finally:
98
+ shutil.rmtree(base, ignore_errors=True)
99
+
100
+
101
+ diagnostico()
102
+ configs_malformadas()
103
+ idempotencia()
104
+ fichero()
105
+ preserva_permisos()
106
+ sys.exit(resumen("doctor"))