agents-city 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2226 -0
  5. package/README.md +2196 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +119 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/doctor +3 -0
  28. package/bin/exit +5 -0
  29. package/bin/hall +73 -0
  30. package/bin/hall.html +498 -0
  31. package/bin/logs +3 -0
  32. package/bin/navegador.mjs +243 -0
  33. package/bin/report.py +14 -0
  34. package/bin/reset +3 -0
  35. package/bin/road +3 -0
  36. package/bin/seat +10 -0
  37. package/bin/serve.py +1579 -0
  38. package/bin/setup.py +753 -0
  39. package/bin/shortcut +3 -0
  40. package/bin/skills +3 -0
  41. package/bin/test +133 -0
  42. package/bin/test-actualiza.py +130 -0
  43. package/bin/test-adapter.py +183 -0
  44. package/bin/test-admision.py +99 -0
  45. package/bin/test-atajos.py +301 -0
  46. package/bin/test-avatar.py +58 -0
  47. package/bin/test-benchmark.py +40 -0
  48. package/bin/test-broker.py +334 -0
  49. package/bin/test-cage.py +374 -0
  50. package/bin/test-card.py +402 -0
  51. package/bin/test-channel.py +379 -0
  52. package/bin/test-cities.py +453 -0
  53. package/bin/test-claude-runtime.py +252 -0
  54. package/bin/test-committee.py +340 -0
  55. package/bin/test-contracts.py +551 -0
  56. package/bin/test-crecimiento.py +164 -0
  57. package/bin/test-demo.py +308 -0
  58. package/bin/test-doctor.py +139 -0
  59. package/bin/test-domains.py +151 -0
  60. package/bin/test-evidencia.py +50 -0
  61. package/bin/test-exit.py +128 -0
  62. package/bin/test-hall-protocol.py +93 -0
  63. package/bin/test-launch.py +169 -0
  64. package/bin/test-live-feed.py +508 -0
  65. package/bin/test-navegador.py +140 -0
  66. package/bin/test-pairing.py +99 -0
  67. package/bin/test-parcels.py +283 -0
  68. package/bin/test-runtime-failures.py +252 -0
  69. package/bin/test-runtime-ui.py +488 -0
  70. package/bin/test-runtime.py +268 -0
  71. package/bin/test-rutas.py +80 -0
  72. package/bin/test-seat.py +1598 -0
  73. package/bin/test-security.py +150 -0
  74. package/bin/test-serve.py +1251 -0
  75. package/bin/test-stress.py +64 -0
  76. package/bin/test-widgets.py +143 -0
  77. package/bin/test-workspace.py +251 -0
  78. package/bin/testlib.py +118 -0
  79. package/bin/tokens.py +14 -0
  80. package/bin/update +3 -0
  81. package/bus/scripts/deploy.sh +60 -0
  82. package/bus/scripts/mint-token.sh +68 -0
  83. package/bus/scripts/setup-dev.sh +79 -0
  84. package/bus/scripts/test-channel.ts +8 -0
  85. package/bus/scripts/test-hub.ts +266 -0
  86. package/bus/scripts/test-local.sh +40 -0
  87. package/bus/scripts/test-queue.ts +132 -0
  88. package/bus/worker/package-lock.json +1971 -0
  89. package/bus/worker/package.json +17 -0
  90. package/bus/worker/src/index.ts +485 -0
  91. package/bus/worker/tsconfig.json +13 -0
  92. package/bus/worker/wrangler.toml +32 -0
  93. package/city/oven/README.md +59 -0
  94. package/city/oven/collect.py +43 -0
  95. package/city/oven/oven.html +4 -0
  96. package/city/scripts/history.py +120 -0
  97. package/city/scripts/seed.py +442 -0
  98. package/city/web/assets/modelos/License.txt +28 -0
  99. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  100. package/city/web/assets/modelos/building-a.glb +0 -0
  101. package/city/web/assets/modelos/building-b.glb +0 -0
  102. package/city/web/assets/modelos/building-c.glb +0 -0
  103. package/city/web/assets/modelos/building-d.glb +0 -0
  104. package/city/web/assets/modelos/building-e.glb +0 -0
  105. package/city/web/assets/modelos/building-f.glb +0 -0
  106. package/city/web/assets/modelos/building-g.glb +0 -0
  107. package/city/web/assets/modelos/building-h.glb +0 -0
  108. package/city/web/assets/sprites/building-a.png +0 -0
  109. package/city/web/assets/sprites/building-b.png +0 -0
  110. package/city/web/assets/sprites/building-c.png +0 -0
  111. package/city/web/assets/sprites/building-d.png +0 -0
  112. package/city/web/assets/sprites/building-e.png +0 -0
  113. package/city/web/assets/sprites/building-f.png +0 -0
  114. package/city/web/assets/sprites/building-g.png +0 -0
  115. package/city/web/assets/sprites/building-h.png +0 -0
  116. package/city/web/assets/sprites/building-i.png +0 -0
  117. package/city/web/assets/sprites/building-j.png +0 -0
  118. package/city/web/assets/sprites/building-k.png +0 -0
  119. package/city/web/assets/sprites/building-l.png +0 -0
  120. package/city/web/assets/sprites/building-m.png +0 -0
  121. package/city/web/assets/sprites/building-n.png +0 -0
  122. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  123. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  124. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  125. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  126. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  127. package/city/web/assets/sprites/catalogo.json +47 -0
  128. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  129. package/city/web/assets/sprites/detail-awning.png +0 -0
  130. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  131. package/city/web/assets/sprites/detail-overhang.png +0 -0
  132. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  133. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  143. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  144. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  145. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  146. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  147. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  148. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  149. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  150. package/city/web/assets/sprites/medidas.json +248 -0
  151. package/city/web/dist/city.js +939 -0
  152. package/city/web/dist/index.html +294 -0
  153. package/city/web/dist/modelos/License.txt +28 -0
  154. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  155. package/city/web/dist/modelos/building-a.glb +0 -0
  156. package/city/web/dist/modelos/building-b.glb +0 -0
  157. package/city/web/dist/modelos/building-c.glb +0 -0
  158. package/city/web/dist/modelos/building-d.glb +0 -0
  159. package/city/web/dist/modelos/building-e.glb +0 -0
  160. package/city/web/dist/modelos/building-f.glb +0 -0
  161. package/city/web/dist/modelos/building-g.glb +0 -0
  162. package/city/web/dist/modelos/building-h.glb +0 -0
  163. package/city/web/dist/sprites/building-a.png +0 -0
  164. package/city/web/dist/sprites/building-b.png +0 -0
  165. package/city/web/dist/sprites/building-c.png +0 -0
  166. package/city/web/dist/sprites/building-d.png +0 -0
  167. package/city/web/dist/sprites/building-e.png +0 -0
  168. package/city/web/dist/sprites/building-f.png +0 -0
  169. package/city/web/dist/sprites/building-g.png +0 -0
  170. package/city/web/dist/sprites/building-h.png +0 -0
  171. package/city/web/dist/sprites/building-i.png +0 -0
  172. package/city/web/dist/sprites/building-j.png +0 -0
  173. package/city/web/dist/sprites/building-k.png +0 -0
  174. package/city/web/dist/sprites/building-l.png +0 -0
  175. package/city/web/dist/sprites/building-m.png +0 -0
  176. package/city/web/dist/sprites/building-n.png +0 -0
  177. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  178. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  179. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  180. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  181. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  182. package/city/web/dist/sprites/catalogo.json +47 -0
  183. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  184. package/city/web/dist/sprites/detail-awning.png +0 -0
  185. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  186. package/city/web/dist/sprites/detail-overhang.png +0 -0
  187. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  188. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  198. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  199. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  200. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  201. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  202. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  203. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  204. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  205. package/city/web/dist/sprites/medidas.json +248 -0
  206. package/city/web/dist-hall/hall.js +1363 -0
  207. package/city/web/index.html +294 -0
  208. package/city/web/package.json +18 -0
  209. package/city/web/sella.py +21 -0
  210. package/city/web/src/activity-actors.ts +47 -0
  211. package/city/web/src/activity.ts +172 -0
  212. package/city/web/src/ayuntamiento.ts +580 -0
  213. package/city/web/src/draw.ts +640 -0
  214. package/city/web/src/game-speech.ts +79 -0
  215. package/city/web/src/hall.ts +1959 -0
  216. package/city/web/src/main.ts +2252 -0
  217. package/city/web/src/oven.ts +124 -0
  218. package/city/web/src/people.ts +244 -0
  219. package/city/web/src/presencia.ts +193 -0
  220. package/city/web/src/puertas.ts +180 -0
  221. package/city/web/src/repo-roles.ts +59 -0
  222. package/city/worker/package.json +16 -0
  223. package/city/worker/schema.sql +117 -0
  224. package/city/worker/src/index.ts +702 -0
  225. package/city/worker/src/square.ts +218 -0
  226. package/city/worker/wrangler.toml +58 -0
  227. package/demo/ada.md +50 -0
  228. package/demo/bruno.md +62 -0
  229. package/demo/camila.md +56 -0
  230. package/demo/city/ada.md +40 -0
  231. package/demo/city/city.yml +8 -0
  232. package/demo/city/parcels.yml +15 -0
  233. package/demo/city/roads.json +4 -0
  234. package/demo/city/units.yml +10 -0
  235. package/demo/clinica/city.yml +8 -0
  236. package/demo/clinica/parcels.yml +15 -0
  237. package/demo/clinica/roads.json +4 -0
  238. package/demo/clinica/units.yml +9 -0
  239. package/demo/clinica/vera.md +46 -0
  240. package/demo/dante.md +54 -0
  241. package/demo/despacho/city.yml +8 -0
  242. package/demo/despacho/marta.md +46 -0
  243. package/demo/despacho/parcels.yml +15 -0
  244. package/demo/despacho/roads.json +4 -0
  245. package/demo/despacho/units.yml +9 -0
  246. package/demo/elsa.md +50 -0
  247. package/demo/farid.md +52 -0
  248. package/demo/greta.md +50 -0
  249. package/demo/hugo.md +49 -0
  250. package/demo/iris.md +49 -0
  251. package/demo/jonas.md +51 -0
  252. package/demo/kira.md +51 -0
  253. package/demo/luca.md +49 -0
  254. package/demo/parcels.yml +92 -0
  255. package/demo/seed.py +20 -0
  256. package/demo/show.py +161 -0
  257. package/demo/stories.py +824 -0
  258. package/demo/units.yml +33 -0
  259. package/docs/agents-first.md +106 -0
  260. package/docs/glossary.md +55 -0
  261. package/docs/map-live-layers.md +73 -0
  262. package/docs/security.md +265 -0
  263. package/docs/self-host.md +151 -0
  264. package/docs/testing.md +205 -0
  265. package/package.json +73 -0
  266. package/plugin/.claude-plugin/plugin.json +62 -0
  267. package/plugin/.mcp.json +15 -0
  268. package/plugin/channel/activity-cli.ts +16 -0
  269. package/plugin/channel/adapter-prompts.ts +113 -0
  270. package/plugin/channel/adapter.js +4292 -0
  271. package/plugin/channel/adapter.ts +133 -0
  272. package/plugin/channel/bus.js +20188 -0
  273. package/plugin/channel/bus.ts +131 -0
  274. package/plugin/channel/city-config.ts +125 -0
  275. package/plugin/channel/claude-channel.ts +58 -0
  276. package/plugin/channel/cli-args.ts +75 -0
  277. package/plugin/channel/client.js +4200 -0
  278. package/plugin/channel/client.ts +38 -0
  279. package/plugin/channel/committee/activity.ts +256 -0
  280. package/plugin/channel/committee/collection.ts +154 -0
  281. package/plugin/channel/committee/decision.ts +175 -0
  282. package/plugin/channel/committee/floor.ts +135 -0
  283. package/plugin/channel/committee/guards.ts +30 -0
  284. package/plugin/channel/committee/history.ts +38 -0
  285. package/plugin/channel/committee/render.ts +102 -0
  286. package/plugin/channel/committee/service.ts +72 -0
  287. package/plugin/channel/committee/storage.ts +92 -0
  288. package/plugin/channel/committee/types.ts +163 -0
  289. package/plugin/channel/committee/view.ts +63 -0
  290. package/plugin/channel/committee-cli.ts +109 -0
  291. package/plugin/channel/delivery-metrics.ts +61 -0
  292. package/plugin/channel/delivery-queue.ts +118 -0
  293. package/plugin/channel/hub/activity-controller.ts +72 -0
  294. package/plugin/channel/hub/activity-feed.ts +169 -0
  295. package/plugin/channel/hub/committee-controller.ts +60 -0
  296. package/plugin/channel/hub/connections.ts +48 -0
  297. package/plugin/channel/hub/diagnostics.ts +67 -0
  298. package/plugin/channel/hub/envelope-validity.ts +109 -0
  299. package/plugin/channel/hub/envelopes.ts +66 -0
  300. package/plugin/channel/hub/lifecycle.ts +91 -0
  301. package/plugin/channel/hub/local-roads.ts +89 -0
  302. package/plugin/channel/hub/remote-roads.ts +165 -0
  303. package/plugin/channel/hub/road-controller.ts +99 -0
  304. package/plugin/channel/hub-client.ts +149 -0
  305. package/plugin/channel/local-hub.js +5995 -0
  306. package/plugin/channel/local-hub.ts +349 -0
  307. package/plugin/channel/map-reporter.ts +48 -0
  308. package/plugin/channel/package-lock.json +1731 -0
  309. package/plugin/channel/package.json +19 -0
  310. package/plugin/channel/protocol.ts +98 -0
  311. package/plugin/channel/road-cli.ts +18 -0
  312. package/plugin/channel/run.sh +15 -0
  313. package/plugin/channel/runtime/claude.ts +458 -0
  314. package/plugin/channel/runtime/codex-config.ts +77 -0
  315. package/plugin/channel/runtime/codex.ts +669 -0
  316. package/plugin/channel/runtime/command.ts +62 -0
  317. package/plugin/channel/runtime/factory.ts +15 -0
  318. package/plugin/channel/runtime/json-rpc.ts +154 -0
  319. package/plugin/channel/runtime/kimi.ts +277 -0
  320. package/plugin/channel/runtime/opencode.ts +248 -0
  321. package/plugin/channel/runtime/process.ts +84 -0
  322. package/plugin/channel/runtime/types.ts +45 -0
  323. package/plugin/channel/runtime-files.ts +73 -0
  324. package/plugin/channel/runtime-gateway.js +6396 -0
  325. package/plugin/channel/runtime-gateway.ts +358 -0
  326. package/plugin/channel/runtime-metrics.ts +52 -0
  327. package/plugin/channel/runtime-subscription.ts +180 -0
  328. package/plugin/channel/terminal-delivery.ts +131 -0
  329. package/plugin/channel/untrusted.ts +60 -0
  330. package/plugin/commands/committee.md +31 -0
  331. package/plugin/commands/exit.md +32 -0
  332. package/plugin/commands/goals.md +20 -0
  333. package/plugin/commands/join.md +18 -0
  334. package/plugin/commands/notice.md +20 -0
  335. package/plugin/commands/propose.md +19 -0
  336. package/plugin/commands/round.md +17 -0
  337. package/plugin/commands/session.md +22 -0
  338. package/plugin/commands/settings.md +25 -0
  339. package/plugin/commands/setup.md +21 -0
  340. package/plugin/commands/team.md +23 -0
  341. package/plugin/domains/custom.md +27 -0
  342. package/plugin/domains/finance.md +31 -0
  343. package/plugin/domains/healthcare.md +34 -0
  344. package/plugin/domains/legal.md +31 -0
  345. package/plugin/domains/marketing.md +33 -0
  346. package/plugin/domains/operations.md +31 -0
  347. package/plugin/domains/research.md +32 -0
  348. package/plugin/domains/sales.md +30 -0
  349. package/plugin/domains/software.md +37 -0
  350. package/plugin/hooks/activity.sh +6 -0
  351. package/plugin/hooks/digging.sh +68 -0
  352. package/plugin/hooks/growth.sh +42 -0
  353. package/plugin/hooks/hooks.json +106 -0
  354. package/plugin/hooks/notice-on-pr.sh +35 -0
  355. package/plugin/hooks/notice-on-stop.sh +80 -0
  356. package/plugin/hooks/notice-pending.sh +58 -0
  357. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  358. package/plugin/hooks/tokens.sh +47 -0
  359. package/plugin/roles/examples/account-executive.md +25 -0
  360. package/plugin/roles/examples/ai-manager.md +34 -0
  361. package/plugin/roles/examples/associate.md +24 -0
  362. package/plugin/roles/examples/brand-lead.md +20 -0
  363. package/plugin/roles/examples/cfo.md +20 -0
  364. package/plugin/roles/examples/city-lead.md +25 -0
  365. package/plugin/roles/examples/clinical-director.md +26 -0
  366. package/plugin/roles/examples/clinical-ops.md +25 -0
  367. package/plugin/roles/examples/clinician.md +25 -0
  368. package/plugin/roles/examples/compliance.md +27 -0
  369. package/plugin/roles/examples/content.md +24 -0
  370. package/plugin/roles/examples/controller.md +27 -0
  371. package/plugin/roles/examples/cpto.md +41 -0
  372. package/plugin/roles/examples/customer-success.md +25 -0
  373. package/plugin/roles/examples/data-engineer.md +37 -0
  374. package/plugin/roles/examples/data.md +60 -0
  375. package/plugin/roles/examples/dev.md +37 -0
  376. package/plugin/roles/examples/devops.md +39 -0
  377. package/plugin/roles/examples/enablement.md +24 -0
  378. package/plugin/roles/examples/ethics.md +24 -0
  379. package/plugin/roles/examples/fin-analytics.md +27 -0
  380. package/plugin/roles/examples/health-compliance.md +25 -0
  381. package/plugin/roles/examples/health-data.md +26 -0
  382. package/plugin/roles/examples/knowledge.md +25 -0
  383. package/plugin/roles/examples/lifecycle.md +26 -0
  384. package/plugin/roles/examples/llm-engineer.md +34 -0
  385. package/plugin/roles/examples/managing-partner.md +20 -0
  386. package/plugin/roles/examples/methods.md +23 -0
  387. package/plugin/roles/examples/operations-lead.md +23 -0
  388. package/plugin/roles/examples/ops.md +24 -0
  389. package/plugin/roles/examples/patient-safety.md +26 -0
  390. package/plugin/roles/examples/performance.md +27 -0
  391. package/plugin/roles/examples/po.md +36 -0
  392. package/plugin/roles/examples/process-owner.md +23 -0
  393. package/plugin/roles/examples/product-design.md +39 -0
  394. package/plugin/roles/examples/program-manager.md +24 -0
  395. package/plugin/roles/examples/quality.md +25 -0
  396. package/plugin/roles/examples/research-director.md +24 -0
  397. package/plugin/roles/examples/research-ops.md +25 -0
  398. package/plugin/roles/examples/researcher.md +25 -0
  399. package/plugin/roles/examples/revenue-lead.md +25 -0
  400. package/plugin/roles/examples/revops.md +26 -0
  401. package/plugin/roles/examples/seo.md +27 -0
  402. package/plugin/roles/examples/specialist.md +24 -0
  403. package/plugin/scripts/actualiza.py +198 -0
  404. package/plugin/scripts/admision.py +165 -0
  405. package/plugin/scripts/apaga.py +232 -0
  406. package/plugin/scripts/atajos.py +506 -0
  407. package/plugin/scripts/avatar.py +211 -0
  408. package/plugin/scripts/broker.py +531 -0
  409. package/plugin/scripts/cage.py +492 -0
  410. package/plugin/scripts/capabilities.py +210 -0
  411. package/plugin/scripts/card.py +322 -0
  412. package/plugin/scripts/cities.py +520 -0
  413. package/plugin/scripts/city-env.sh +90 -0
  414. package/plugin/scripts/city-runtime.sh +87 -0
  415. package/plugin/scripts/city-session.sh +552 -0
  416. package/plugin/scripts/city_env.py +67 -0
  417. package/plugin/scripts/crecimiento.py +140 -0
  418. package/plugin/scripts/deliberations.py +48 -0
  419. package/plugin/scripts/doctor.py +294 -0
  420. package/plugin/scripts/domains.py +201 -0
  421. package/plugin/scripts/evidencia.py +64 -0
  422. package/plugin/scripts/find-repos.sh +111 -0
  423. package/plugin/scripts/gh.py +159 -0
  424. package/plugin/scripts/hall_protocol.py +147 -0
  425. package/plugin/scripts/hook_activity.py +139 -0
  426. package/plugin/scripts/launch.py +94 -0
  427. package/plugin/scripts/logs.py +98 -0
  428. package/plugin/scripts/pairing.py +185 -0
  429. package/plugin/scripts/parcels.py +132 -0
  430. package/plugin/scripts/read-card.py +55 -0
  431. package/plugin/scripts/report.py +201 -0
  432. package/plugin/scripts/reset.py +180 -0
  433. package/plugin/scripts/roads.py +230 -0
  434. package/plugin/scripts/roles.py +274 -0
  435. package/plugin/scripts/runtime_log.py +69 -0
  436. package/plugin/scripts/runtime_processes.py +132 -0
  437. package/plugin/scripts/rutas.py +83 -0
  438. package/plugin/scripts/seat.py +1696 -0
  439. package/plugin/scripts/tokens.py +194 -0
  440. package/plugin/scripts/trust-repos.py +59 -0
  441. package/plugin/scripts/ui.py +280 -0
  442. package/plugin/scripts/units.py +80 -0
  443. package/plugin/scripts/workspace.py +561 -0
  444. package/plugin/skills/city/SKILL.md +191 -0
  445. package/templates/blank.md +39 -0
  446. package/templates/finance.md +44 -0
  447. package/templates/legal.md +45 -0
  448. package/templates/marketing.md +48 -0
  449. package/templates/product.md +45 -0
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python3
2
+ """The roles offered to an owner seat, across the built-in work domains.
3
+
4
+ The v2 road address is stable ``owner/city`` and never depends on role. The short
5
+ role suffixes remain only to read and import v1 cards such as ``alice/lead``.
6
+
7
+ The catalogue is shared by the Hall and seat so they cannot label the same role
8
+ differently. Which subset is relevant is owned by ``domains.py``: a city first
9
+ chooses its work domain, then one role inside it. Role descriptions may still be
10
+ overridden by a city's own role files.
11
+
12
+ Only picker labels and one-line summaries live here. The operating knowledge and
13
+ trade come from each role's Markdown file, looked up in your city first and the
14
+ plugin's examples second — duplicating those in code is how a managing partner
15
+ once got labelled a master builder.
16
+ """
17
+
18
+ import os
19
+ import re
20
+
21
+ AQUI = os.path.dirname(os.path.abspath(__file__))
22
+
23
+ # (id, what the city calls it, its one line, whether it owns a property of every
24
+ # folder rather than folders of its own).
25
+ CATALOGO = [
26
+ # Deliberately no role pack. ``blank`` is an explicit choice to start with
27
+ # no predefined responsibility or operating knowledge; it is offered in
28
+ # every domain and can also be assigned to an individual repo agent.
29
+ ("blank", "Blank role", "no predefined responsibility or role knowledge", True),
30
+ # Software development
31
+ ("cpto", "Product & technology lead", "direction, priorities and product outcomes", True),
32
+ ("dev", "Software engineer", "owns repositories and answers for their code", False),
33
+ (
34
+ "data-engineer",
35
+ "Data engineer",
36
+ "pipelines, transformations, storage contracts and data reliability",
37
+ False,
38
+ ),
39
+ ("devops", "Platform / DevOps lead", "security, reliability, performance, cost and CI", True),
40
+ ("data", "Data & analytics lead", "events, schemas and comparable measurement", True),
41
+ ("product-design", "Product designer", "design system, accessibility and user flows", True),
42
+ ("po", "Product manager", "scope, priority and delivery coherence", True),
43
+ ("llm-engineer", "AI / LLM engineer", "model calls, prompts, evaluations and token cost", True),
44
+ ("ai-manager", "AI portfolio lead", "initiative overlap, adoption and portfolio value", True),
45
+ # Marketing, legal and finance
46
+ ("brand-lead", "Brand lead", "positioning, priorities and brand coherence", True),
47
+ ("content", "Content lead", "owns pieces, channels and editorial intent", False),
48
+ ("performance", "Performance marketing lead", "paid acquisition, spend and conversion", True),
49
+ ("seo", "SEO lead", "organic structure, indexing, links and durable URLs", True),
50
+ (
51
+ "lifecycle",
52
+ "Lifecycle / CRM lead",
53
+ "segments, triggers, sequences and contact pressure",
54
+ True,
55
+ ),
56
+ ("managing-partner", "Managing partner", "practice direction, matters and staffing", True),
57
+ ("associate", "Lawyer / associate", "owns matters and their legal context", False),
58
+ ("compliance", "Compliance lead", "obligations, deadlines, conflicts and records", True),
59
+ ("knowledge", "Knowledge lead", "precedents, reusable work and institutional memory", True),
60
+ ("cfo", "CFO / finance lead", "financial direction, priorities and resource choices", True),
61
+ ("controller", "Controller", "close, reconciliations, controls and audit trail", True),
62
+ (
63
+ "fin-analytics",
64
+ "Financial analyst",
65
+ "definitions, comparability, forecasts and scenarios",
66
+ True,
67
+ ),
68
+ ("ops", "Operations specialist", "owns operational processes and their exceptions", False),
69
+ # Healthcare
70
+ (
71
+ "clinical-director",
72
+ "Clinical director",
73
+ "clinical direction, accountability and care priorities",
74
+ True,
75
+ ),
76
+ ("clinician", "Clinician", "owns patient or pathway context and clinical judgement", False),
77
+ (
78
+ "patient-safety",
79
+ "Patient safety & quality lead",
80
+ "harm prevention, incidents and safeguards",
81
+ True,
82
+ ),
83
+ (
84
+ "clinical-ops",
85
+ "Clinical operations lead",
86
+ "capacity, pathways, handoffs and service delivery",
87
+ True,
88
+ ),
89
+ ("health-data", "Clinical data lead", "health measures, provenance and data quality", True),
90
+ (
91
+ "health-compliance",
92
+ "Health privacy & compliance lead",
93
+ "privacy, consent, access and regulation",
94
+ True,
95
+ ),
96
+ # Revenue
97
+ ("revenue-lead", "Revenue leader", "commercial direction, forecast and commitments", True),
98
+ ("account-executive", "Account executive", "owns opportunities and buyer context", False),
99
+ (
100
+ "revops",
101
+ "Revenue operations lead",
102
+ "pipeline definitions, routing, systems and forecast hygiene",
103
+ True,
104
+ ),
105
+ (
106
+ "customer-success",
107
+ "Customer success lead",
108
+ "adoption, outcomes, renewal and churn risk",
109
+ True,
110
+ ),
111
+ (
112
+ "enablement",
113
+ "Sales enablement lead",
114
+ "playbooks, evidence and reusable field knowledge",
115
+ True,
116
+ ),
117
+ # Research and general operations
118
+ (
119
+ "research-director",
120
+ "Research director",
121
+ "questions, portfolio direction and research decisions",
122
+ True,
123
+ ),
124
+ ("researcher", "Researcher", "owns studies, sources and interpretation", False),
125
+ (
126
+ "methods",
127
+ "Methods / statistics lead",
128
+ "study design, validity, uncertainty and analysis",
129
+ True,
130
+ ),
131
+ (
132
+ "research-ops",
133
+ "Research operations lead",
134
+ "participants, tooling, data and study delivery",
135
+ True,
136
+ ),
137
+ ("ethics", "Ethics lead", "consent, proportionality, vulnerable groups and review", True),
138
+ ("operations-lead", "Operations lead", "operating direction, outcomes and trade-offs", True),
139
+ ("program-manager", "Programme manager", "dependencies, scope, milestones and delivery", True),
140
+ ("process-owner", "Process owner", "owns a process, its exceptions and its outcomes", False),
141
+ (
142
+ "quality",
143
+ "Quality & risk lead",
144
+ "acceptance, failure modes, controls and verification",
145
+ True,
146
+ ),
147
+ ("city-lead", "City lead", "sets direction and chairs decisions in a custom domain", True),
148
+ ("specialist", "Domain specialist", "owns a body of work and answers from its evidence", False),
149
+ ]
150
+
151
+ # Short, lowercase and stable. A role nobody anticipated still gets a sensible name
152
+ # from `sufijo()` rather than nothing.
153
+ SUFIJOS = {
154
+ "blank": "blank",
155
+ "cpto": "lead",
156
+ "devops": "ops",
157
+ "data": "data",
158
+ "product-design": "design",
159
+ "po": "po",
160
+ "llm-engineer": "llm",
161
+ "ai-manager": "ai",
162
+ "dev": "dev",
163
+ "data-engineer": "dataeng",
164
+ "brand-lead": "brand",
165
+ "content": "content",
166
+ "performance": "ads",
167
+ "seo": "seo",
168
+ "lifecycle": "crm",
169
+ "managing-partner": "partner",
170
+ "associate": "assoc",
171
+ "compliance": "comp",
172
+ "knowledge": "know",
173
+ "cfo": "cfo",
174
+ "controller": "ctrl",
175
+ "fin-analytics": "fin",
176
+ "ops": "ops",
177
+ "clinical-director": "clinical",
178
+ "clinician": "clinician",
179
+ "patient-safety": "safety",
180
+ "clinical-ops": "careops",
181
+ "health-data": "healthdata",
182
+ "health-compliance": "privacy",
183
+ "revenue-lead": "revenue",
184
+ "account-executive": "account",
185
+ "revops": "revops",
186
+ "customer-success": "success",
187
+ "enablement": "enable",
188
+ "research-director": "research",
189
+ "researcher": "researcher",
190
+ "methods": "methods",
191
+ "research-ops": "resops",
192
+ "ethics": "ethics",
193
+ "operations-lead": "operations",
194
+ "program-manager": "program",
195
+ "process-owner": "process",
196
+ "quality": "quality",
197
+ "city-lead": "lead",
198
+ "specialist": "specialist",
199
+ }
200
+
201
+ # Whoever sets the goals. Not a style rule: goals are the one thing this system keeps
202
+ # whatever kind of city you build, and they are agreed between the architect and each
203
+ # city seat. Without that seat there is nobody to agree them with, and every round
204
+ # degrades into a status report.
205
+ ARQUITECTOS = {
206
+ "cpto",
207
+ "brand-lead",
208
+ "managing-partner",
209
+ "cfo",
210
+ "clinical-director",
211
+ "revenue-lead",
212
+ "research-director",
213
+ "operations-lead",
214
+ "city-lead",
215
+ }
216
+
217
+
218
+ def sufijo(rol):
219
+ """The agent-name suffix for a role."""
220
+ return SUFIJOS.get(rol, rol.split("-")[0][:8])
221
+
222
+
223
+ def agente(usuario, rol, ciudad=""):
224
+ """A stable owner/city address, or the legacy user/role address.
225
+
226
+ Repo agents stay private behind the seat. Once a city is known, the public
227
+ endpoint is ``user/city`` so changing its role cannot break every road.
228
+ """
229
+ if ciudad:
230
+ segura = re.sub(r"[^a-z0-9-]+", "-", ciudad.lower()).strip("-") or "city"
231
+ return f"{usuario}/{segura}"
232
+ return f"{usuario}/{sufijo(rol)}"
233
+
234
+
235
+ def nombre(rol):
236
+ """What the city calls this role, or a readable version of its id."""
237
+ for r, n, _, _ in CATALOGO:
238
+ if r == rol:
239
+ return n
240
+ return rol.replace("-", " ").title()
241
+
242
+
243
+ def oficio(rol, datos=""):
244
+ """The trade, read from the role's own file — never from a table in code.
245
+
246
+ A hardcoded map labels every unknown role as the same thing, which is exactly
247
+ what once turned a managing partner into a master builder. Looked up in the data
248
+ repo first (roles are data, and a team renames them), then in the examples this
249
+ plugin ships. `../roles/examples` and not a path through the clone, because the
250
+ installed plugin has no clone — this is the one lookup that works at both doors.
251
+
252
+ There were two copies of this: the wizard's skipped the data repo, so a re-run
253
+ against renamed roles showed the stale trade. One copy, the complete one.
254
+ """
255
+ if rol == "blank":
256
+ return "No preset"
257
+ bases = [os.path.join(datos, "roles")] if datos else []
258
+ bases.append(os.path.join(AQUI, "..", "roles", "examples"))
259
+ for base in bases:
260
+ f = os.path.join(base, f"{rol}.md")
261
+ if os.path.exists(f):
262
+ m = re.search(r"^trade:\s*(.+)$", open(f).read(), re.M)
263
+ if m:
264
+ return m.group(1).strip()
265
+ # No role file: say what the role is called rather than inventing a trade.
266
+ return rol.replace("-", " ").title()
267
+
268
+
269
+ def transversal(rol):
270
+ """Whether this role owns a property of every folder rather than folders."""
271
+ for r, _, _, t in CATALOGO:
272
+ if r == rol:
273
+ return t
274
+ return True
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """Append secret-scrubbed operational diagnostics for one local city."""
3
+ import argparse
4
+ import datetime
5
+ import json
6
+ import os
7
+ import re
8
+ import uuid
9
+
10
+ import runtime_processes
11
+
12
+
13
+ def _clean(value, limit=2000):
14
+ text = str(value or '').strip()
15
+ text = re.sub(r'([?&](?:token|secret|key)=)[^&\s]+', r'\1[redacted]', text,
16
+ flags=re.IGNORECASE)
17
+ text = re.sub(r'Bearer\s+\S+', 'Bearer [redacted]', text, flags=re.IGNORECASE)
18
+ text = re.sub(r'(Authorization:\s*)\S+(?:\s+\S+)?', r'\1[redacted]', text,
19
+ flags=re.IGNORECASE)
20
+ text = re.sub(r'(--(?:token|password|secret|api-key)(?:=|\s+))\S+',
21
+ r'\1[redacted]', text, flags=re.IGNORECASE)
22
+ text = re.sub(r'((?:TOKEN|SECRET|PASSWORD|API_KEY)=)[^\s]+',
23
+ r'\1[redacted]', text, flags=re.IGNORECASE)
24
+ return text[:limit]
25
+
26
+
27
+ def append(data, component, event, **fields):
28
+ """Best-effort logging: diagnostics must never stop an agent."""
29
+ try:
30
+ directory = runtime_processes.ruta(data)
31
+ os.makedirs(directory, mode=0o700, exist_ok=True)
32
+ path = os.path.join(directory, 'diagnostics.jsonl')
33
+ record = {
34
+ 'protocol': 'agents-city-diagnostic/1',
35
+ 'id': 'diagnostic_' + uuid.uuid4().hex,
36
+ 'at': datetime.datetime.now(datetime.timezone.utc).isoformat(),
37
+ 'pid': os.getpid(),
38
+ 'component': _clean(component, 80),
39
+ 'event': _clean(event, 120),
40
+ }
41
+ for key, value in fields.items():
42
+ if value is None:
43
+ continue
44
+ record[key] = ('[redacted]' if re.search(
45
+ r'token|secret|authorization|credential', key, re.IGNORECASE)
46
+ else _clean(value))
47
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
48
+ with os.fdopen(fd, 'a', encoding='utf-8') as stream:
49
+ stream.write(json.dumps(record, ensure_ascii=False) + '\n')
50
+ except Exception:
51
+ pass
52
+
53
+
54
+ def main():
55
+ parser = argparse.ArgumentParser()
56
+ parser.add_argument('event')
57
+ parser.add_argument('--data', default=os.environ.get('AGENTS_CITY_DATA', ''))
58
+ parser.add_argument('--component', default='launcher')
59
+ parser.add_argument('--actor', default=os.environ.get('CITY_BUS_ACTOR', ''))
60
+ parser.add_argument('--outcome', default='')
61
+ parser.add_argument('--message', default='')
62
+ args = parser.parse_args()
63
+ if args.data:
64
+ append(args.data, args.component, args.event, actor=args.actor,
65
+ outcome=args.outcome, message=args.message)
66
+
67
+
68
+ if __name__ == '__main__':
69
+ main()
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """Exact process markers for one city's local bus and runtime gateways."""
3
+ import json
4
+ import os
5
+ import re
6
+ import shlex
7
+ import subprocess
8
+
9
+ import cities
10
+
11
+
12
+ def ruta(datos):
13
+ ident = cities.identidad(datos)
14
+ key = re.sub(r'[^a-z0-9-]+', '-', ident.lower()).strip('-')[:80] or 'city'
15
+ return os.path.join(cities.raiz(), '.runtime', 'bus', key)
16
+
17
+
18
+ def procesos(datos):
19
+ """Return every exact city runtime, including one that lost its marker.
20
+
21
+ Marker files are the fast path. A crashed or racing predecessor can lose
22
+ its endpoint while the process survives, so also inspect argv and accept
23
+ only a known entrypoint whose literal ``--data`` resolves to this city.
24
+ """
25
+ datos = os.path.realpath(datos)
26
+ base = ruta(datos)
27
+ candidatos = []
28
+ try:
29
+ endpoint = json.load(open(os.path.join(base, 'endpoint.json'), encoding='utf-8'))
30
+ candidatos.append(('bus hub', int(endpoint.get('pid', 0)), 'local-hub.js'))
31
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
32
+ pass
33
+ for folder, label, marker in (
34
+ ('gateways', 'gateway', 'runtime-gateway.js'),
35
+ ('adapters', 'terminal fallback', 'adapter.js')):
36
+ directory = os.path.join(base, folder)
37
+ try:
38
+ nombres = [n for n in os.listdir(directory) if n.endswith('.pid')]
39
+ except OSError:
40
+ nombres = []
41
+ for nombre in nombres:
42
+ try:
43
+ pid = int(open(os.path.join(directory, nombre), encoding='utf-8').read().strip())
44
+ except (OSError, ValueError):
45
+ continue
46
+ candidatos.append((f'{label} {nombre[:-4]}', pid, marker))
47
+
48
+ vistos = set()
49
+ fuera = []
50
+ for etiqueta, pid, componente in candidatos:
51
+ if pid <= 0:
52
+ continue
53
+ try:
54
+ comando = subprocess.run(
55
+ ['ps', '-p', str(pid), '-o', 'command='],
56
+ capture_output=True, text=True, timeout=3).stdout.strip()
57
+ except (OSError, subprocess.TimeoutExpired):
58
+ comando = ''
59
+ if componente in comando and datos in comando:
60
+ fuera.append({'label': etiqueta, 'pid': pid})
61
+ vistos.add(pid)
62
+ for proceso in _procesos_sin_marca(datos):
63
+ if proceso['pid'] not in vistos:
64
+ fuera.append(proceso)
65
+ vistos.add(proceso['pid'])
66
+ return fuera
67
+
68
+
69
+ def _procesos_sin_marca(datos):
70
+ """Discover only node entrypoints with an exact ``--data <city>`` argv."""
71
+ try:
72
+ salida = subprocess.run(
73
+ ['ps', '-Ao', 'pid=,command='], capture_output=True, text=True,
74
+ timeout=5).stdout
75
+ except (OSError, subprocess.TimeoutExpired):
76
+ return []
77
+ componentes = {
78
+ 'local-hub.js': 'bus hub',
79
+ 'runtime-gateway.js': 'gateway',
80
+ 'adapter.js': 'terminal fallback',
81
+ }
82
+ encontrados = []
83
+ for linea in salida.splitlines():
84
+ match = re.match(r'\s*(\d+)\s+(.*)', linea)
85
+ if not match:
86
+ continue
87
+ pid, comando = int(match.group(1)), match.group(2)
88
+ if pid in (os.getpid(), os.getppid()):
89
+ continue
90
+ try:
91
+ argv = shlex.split(comando)
92
+ except ValueError:
93
+ continue
94
+ indice = next(
95
+ (i for i, value in enumerate(argv[:3])
96
+ if os.path.basename(value) in componentes), None)
97
+ if indice is None:
98
+ continue
99
+ try:
100
+ data_index = argv.index('--data', indice + 1)
101
+ data = os.path.realpath(argv[data_index + 1])
102
+ except (ValueError, IndexError):
103
+ continue
104
+ if data != datos:
105
+ continue
106
+ componente = os.path.basename(argv[indice])
107
+ etiqueta = componentes[componente]
108
+ if componente in ('runtime-gateway.js', 'adapter.js'):
109
+ try:
110
+ actor = argv[argv.index('--actor', indice + 1) + 1]
111
+ etiqueta = f'{etiqueta} {actor}'
112
+ except (ValueError, IndexError):
113
+ pass
114
+ encontrados.append({'label': etiqueta, 'pid': pid})
115
+ return encontrados
116
+
117
+
118
+ def limpia_marcas(datos):
119
+ base = ruta(datos)
120
+ rutas = [os.path.join(base, 'endpoint.json'), os.path.join(base, 'hub.lock')]
121
+ for folder in ('gateways', 'adapters'):
122
+ directory = os.path.join(base, folder)
123
+ try:
124
+ rutas += [os.path.join(directory, n) for n in os.listdir(directory)
125
+ if n.endswith('.pid')]
126
+ except OSError:
127
+ pass
128
+ for path in rutas:
129
+ try:
130
+ os.unlink(path)
131
+ except OSError:
132
+ pass
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Path containment, resolved twice, in one place.
3
+
4
+ Every part of the security layer that trusts a path — the cage deciding what to
5
+ re-allow for writing, the broker deciding which repo a token may act on — needs
6
+ the same three questions answered the same way:
7
+
8
+ 1. Where does this path *really* point, even if its leaf does not exist yet?
9
+ A non-existent leaf under a symlinked parent still redirects, so we resolve
10
+ through the deepest ancestor that does exist.
11
+ 2. Is it inside a forbidden root?
12
+ 3. Does it *cover* a forbidden root (an ancestor of it)? Re-allowing `~` would
13
+ silently re-open `~/.ssh`; that is the bug this module exists to refuse.
14
+
15
+ Kept dependency-free and pure so both the cage and the broker import it, and so
16
+ the tests can drive it on any platform without a sandbox.
17
+ """
18
+
19
+ import os
20
+
21
+
22
+ def canonicaliza(ruta):
23
+ """Absolute real path, resolved through the deepest ancestor that exists.
24
+
25
+ `os.path.realpath` already does this on most inputs, but it stops resolving
26
+ once it meets a missing component; we walk up to the first existing ancestor,
27
+ resolve *that*, then re-attach the missing tail. A symlinked parent with a
28
+ not-yet-created child therefore still lands on the real target.
29
+ """
30
+ ruta = os.path.abspath(os.path.expanduser(ruta))
31
+ cola = []
32
+ actual = ruta
33
+ while not os.path.exists(actual):
34
+ padre = os.path.dirname(actual)
35
+ if padre == actual:
36
+ break
37
+ cola.append(os.path.basename(actual))
38
+ actual = padre
39
+ base = os.path.realpath(actual)
40
+ for segmento in reversed(cola):
41
+ base = os.path.join(base, segmento)
42
+ return base
43
+
44
+
45
+ def _segmentos(ruta):
46
+ return [s for s in canonicaliza(ruta).split(os.sep) if s]
47
+
48
+
49
+ def _dentro_seg(h, p):
50
+ return len(h) >= len(p) and h[: len(p)] == p
51
+
52
+
53
+ def _cubre_seg(a, d):
54
+ return len(a) < len(d) and d[: len(a)] == a
55
+
56
+
57
+ def dentro_de(hijo, padre):
58
+ """True when `hijo` is `padre` or lives under it — compared segment-wise so
59
+ `/a/bc` is never judged to be inside `/a/b`."""
60
+ return _dentro_seg(_segmentos(hijo), _segmentos(padre))
61
+
62
+
63
+ def cubre(ancestro, descendiente):
64
+ """True when `ancestro` is a strict parent of `descendiente` (covers it)."""
65
+ return _cubre_seg(_segmentos(ancestro), _segmentos(descendiente))
66
+
67
+
68
+ def motivo_bloqueo(ruta, bloqueados):
69
+ """Why `ruta` may not be trusted against a set of forbidden roots, or None.
70
+
71
+ Bidirectional on purpose: a path is refused both when it sits inside a
72
+ forbidden root and when it is an ancestor that would re-open one. `ruta` is
73
+ canonicalized once here and compared segment-wise against every root.
74
+ """
75
+ r_can = canonicaliza(ruta)
76
+ r_seg = [s for s in r_can.split(os.sep) if s]
77
+ for raiz in bloqueados:
78
+ raiz_seg = _segmentos(raiz)
79
+ if _dentro_seg(r_seg, raiz_seg):
80
+ return f'{r_can} is inside sealed {canonicaliza(raiz)}'
81
+ if _cubre_seg(r_seg, raiz_seg):
82
+ return f'{r_can} covers sealed {canonicaliza(raiz)}'
83
+ return None