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,180 @@
1
+ #!/usr/bin/env python3
2
+ """Reset one managed city to first-run state, recoverably.
3
+
4
+ The target is always explicit. Repos are references and are never touched. The
5
+ city folder and its local map state move to a timestamped backup, incident local
6
+ roads are removed at both ends, and an empty city with the same stable identity
7
+ is created in its place. The next ``agents-city seat --city <name>`` runs the
8
+ five onboarding questions again.
9
+ """
10
+
11
+ import argparse
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import time
17
+
18
+ import cities
19
+ import domains
20
+ import roads
21
+ import runtime_processes
22
+
23
+
24
+ def _backup_path(usuario, datos):
25
+ sello = time.strftime("%Y%m%d-%H%M%S")
26
+ base = os.path.join(cities.carpeta_usuario(usuario), ".backups")
27
+ nombre = f"{cities.slug_ciudad(datos)}-{sello}"
28
+ ruta = os.path.join(base, nombre)
29
+ n = 2
30
+ while os.path.exists(ruta):
31
+ ruta = os.path.join(base, f"{nombre}-{n}")
32
+ n += 1
33
+ return ruta
34
+
35
+
36
+ def _deten(datos, dry_run=False):
37
+ guion = os.path.join(os.path.dirname(__file__), "apaga.py")
38
+ args = [sys.executable, guion, datos]
39
+ if dry_run:
40
+ args.append("--dry-run")
41
+ return subprocess.run(args, capture_output=True, text=True)
42
+
43
+
44
+ def _valida(datos, usuario):
45
+ usuario = usuario or cities.usuario_actual()
46
+ datos = os.path.realpath(datos)
47
+ if not cities.gestionada(datos, usuario):
48
+ raise ValueError(
49
+ "reset only accepts a managed city under "
50
+ f"{cities.carpeta_usuario(usuario)}; external/shared "
51
+ "folders are never erased"
52
+ )
53
+ if not cities.es_ciudad(datos):
54
+ raise ValueError(f"{datos} is not a city")
55
+ owner_en_disco = cities.lee_clave(datos, "owner")
56
+ if owner_en_disco and owner_en_disco != usuario:
57
+ raise ValueError(f"this city belongs to {owner_en_disco!r}, not {usuario!r}")
58
+ return datos, usuario
59
+
60
+
61
+ def _contexto(datos, usuario):
62
+ return {
63
+ "datos": datos,
64
+ "usuario": usuario,
65
+ "ident": cities.identidad(datos),
66
+ "nombre": cities.nombre(datos),
67
+ "slug": cities.slug_ciudad(datos),
68
+ "owner": cities.lee_clave(datos, "owner") or usuario,
69
+ "domain": domains.de_ciudad(datos),
70
+ "grow": cities.lee_clave(
71
+ datos, "grow_command", "git log --since=30.days --oneline | wc -l"
72
+ ),
73
+ "backup": _backup_path(usuario, datos),
74
+ "estado": os.path.join(cities.raiz(), "state", cities.slug(datos)),
75
+ "runtime": runtime_processes.ruta(datos),
76
+ "roads": roads.lee(datos),
77
+ }
78
+
79
+
80
+ def _preview(ctx):
81
+ print(f" Would reset city `{ctx['slug']}` at {ctx['datos']}")
82
+ print(f" Would preserve it at {ctx['backup']}")
83
+ print(" Would leave every referenced repo untouched")
84
+ if ctx["roads"]:
85
+ print(f" Would close {len(ctx['roads'])} road(s) at both ends")
86
+ parada = _deten(ctx["datos"], dry_run=True)
87
+ if parada.stdout.strip():
88
+ print(parada.stdout.rstrip())
89
+
90
+
91
+ def _para(datos):
92
+ parada = _deten(datos)
93
+ if parada.returncode != 0:
94
+ detalle = (parada.stderr or parada.stdout or "unknown stop error").strip()
95
+ raise ValueError(f"could not stop the city before reset: {detalle}")
96
+
97
+
98
+ def _cierra_carreteras(ctx):
99
+ """Back up and remove the other half of every incident local road."""
100
+ backlinks = os.path.join(ctx["backup"], "road-backlinks")
101
+ for camino in ctx["roads"]:
102
+ otro = roads.destino_local(camino, ctx["usuario"])
103
+ if not otro:
104
+ continue
105
+ os.makedirs(backlinks, exist_ok=True)
106
+ if os.path.isfile(roads.ruta(otro)):
107
+ destino = os.path.join(backlinks, f"{cities.slug_ciudad(otro)}.roads.json")
108
+ shutil.copy2(roads.ruta(otro), destino)
109
+ roads.desconecta(otro, ctx["ident"])
110
+
111
+
112
+ def _mueve_si_existe(origen, destino):
113
+ if os.path.exists(origen):
114
+ shutil.move(origen, destino)
115
+
116
+
117
+ def _respalda(ctx):
118
+ os.makedirs(ctx["backup"], exist_ok=False)
119
+ _cierra_carreteras(ctx)
120
+ shutil.move(ctx["datos"], os.path.join(ctx["backup"], "city"))
121
+ _mueve_si_existe(ctx["estado"], os.path.join(ctx["backup"], "state"))
122
+ _mueve_si_existe(ctx["runtime"], os.path.join(ctx["backup"], "runtime"))
123
+
124
+
125
+ def _crea_vacia(ctx):
126
+ datos = ctx["datos"]
127
+ os.makedirs(datos, exist_ok=True)
128
+ cities.asegura_metadata(datos, ctx["owner"], ctx["nombre"], ctx["ident"])
129
+ # Keep stable identity and growth measure, but no seat, agents, repos or roads.
130
+ city_yml = os.path.join(datos, "city.yml")
131
+ texto = open(city_yml, encoding="utf-8").read()
132
+ if "\ndomain:" not in "\n" + texto:
133
+ texto += f"domain: {ctx['domain']}\n"
134
+ if "\ngrow_command:" not in "\n" + texto:
135
+ texto += f"grow_command: {ctx['grow']}\n"
136
+ cities.escribe_atomico(city_yml, texto)
137
+ roads.escribe(datos, [])
138
+ cities.selecciona(ctx["usuario"], datos)
139
+
140
+
141
+ def reinicia(datos, usuario="", dry_run=False):
142
+ datos, usuario = _valida(datos, usuario)
143
+ ctx = _contexto(datos, usuario)
144
+ if dry_run:
145
+ _preview(ctx)
146
+ return ctx["backup"]
147
+
148
+ _para(datos)
149
+ _respalda(ctx)
150
+ _crea_vacia(ctx)
151
+ return ctx["backup"]
152
+
153
+
154
+ def main():
155
+ ap = argparse.ArgumentParser(
156
+ description="Reset one city to onboarding; repos stay untouched and data is backed up"
157
+ )
158
+ ap.add_argument("city", help="city name or path (required)")
159
+ ap.add_argument("--dry-run", action="store_true", help="show every effect, change nothing")
160
+ a = ap.parse_args()
161
+ usuario = cities.usuario_actual()
162
+ datos = cities.resuelve(a.city, usuario)
163
+ if not datos:
164
+ conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
165
+ print(f" No city called {a.city!r}. Known here: {conocidas}.", file=sys.stderr)
166
+ return 1
167
+ try:
168
+ backup = reinicia(datos, usuario, a.dry_run)
169
+ except ValueError as e:
170
+ print(f" {e}", file=sys.stderr)
171
+ return 1
172
+ if not a.dry_run:
173
+ corto = backup.replace(os.path.expanduser("~"), "~")
174
+ print(f" City `{cities.slug_ciudad(datos)}` reset. Recovery copy: {corto}")
175
+ print(f" Run `agents-city seat --city {cities.slug_ciudad(datos)}` for onboarding.")
176
+ return 0
177
+
178
+
179
+ if __name__ == "__main__":
180
+ sys.exit(main())
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ """Explicit roads between autonomous cities.
3
+
4
+ Roads connect city seats, never users and never repo windows. A local road is
5
+ written at both ends. A remote road is added from a small public invitation;
6
+ the other end accepts this city's invitation independently. No token or skill
7
+ content is ever stored here.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+
15
+ import card
16
+ import cities
17
+ import domains
18
+
19
+
20
+ ADDRESS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}/[a-z0-9][a-z0-9_-]{0,63}$")
21
+ ID_RE = re.compile(r"^[A-Za-z0-9_.:-]{1,128}$")
22
+ INVITATION_KEYS = {"version", "id", "name", "owner", "address", "domain", "role"}
23
+
24
+
25
+ def _limpia(camino):
26
+ """Validate untrusted road metadata and return its storage shape."""
27
+ if not isinstance(camino, dict):
28
+ return None
29
+ ident = str(camino.get("id", "")).strip()
30
+ address = str(camino.get("address", "")).strip().lower()
31
+ if not ID_RE.fullmatch(ident) or not ADDRESS_RE.fullmatch(address):
32
+ return None
33
+ dueno, slug = address.split("/", 1)
34
+ owner = str(camino.get("owner") or dueno).strip().lower()
35
+ if owner != dueno:
36
+ return None
37
+ name = " ".join(str(camino.get("name") or slug).split())[:80] or slug
38
+ limpio = {"id": ident, "name": name, "owner": owner, "address": address}
39
+ dominio = domains.canonico(camino.get("domain"))
40
+ rol = str(camino.get("role") or "").strip().lower()
41
+ if dominio and re.fullmatch(r"[a-z0-9-]{1,80}", dominio):
42
+ limpio["domain"] = dominio
43
+ if rol and re.fullmatch(r"[a-z0-9-]{1,80}", rol):
44
+ limpio["role"] = rol
45
+ if camino.get("local"):
46
+ limpio["local"] = True
47
+ return limpio
48
+
49
+
50
+ def ruta(datos):
51
+ return os.path.join(os.path.realpath(datos), "roads.json")
52
+
53
+
54
+ def _atomico(ruta_, objeto):
55
+ os.makedirs(os.path.dirname(ruta_), exist_ok=True)
56
+ temporal = ruta_ + f".tmp-{os.getpid()}"
57
+ with open(temporal, "w", encoding="utf-8") as f:
58
+ json.dump(objeto, f, indent=2, ensure_ascii=False, sort_keys=True)
59
+ f.write("\n")
60
+ os.replace(temporal, ruta_)
61
+
62
+
63
+ def lee(datos):
64
+ try:
65
+ objeto = json.load(open(ruta(datos), encoding="utf-8"))
66
+ except (OSError, json.JSONDecodeError, TypeError):
67
+ return []
68
+ caminos = objeto.get("roads", []) if isinstance(objeto, dict) else []
69
+ return [limpio for r in caminos if (limpio := _limpia(r)) is not None]
70
+
71
+
72
+ def escribe(datos, caminos):
73
+ limpios, ids, direcciones = [], set(), set()
74
+ for camino in caminos:
75
+ limpio = _limpia(camino)
76
+ if not limpio or limpio["id"] in ids or limpio["address"] in direcciones:
77
+ continue
78
+ ids.add(limpio["id"])
79
+ direcciones.add(limpio["address"])
80
+ limpios.append(limpio)
81
+ limpios.sort(key=lambda r: (r["owner"], r["name"].lower(), r["id"]))
82
+ _atomico(ruta(datos), {"version": 1, "roads": limpios})
83
+
84
+
85
+ def invitacion(datos, usuario=""):
86
+ usuario = usuario or cities.usuario_actual()
87
+ owner = cities.lee_clave(datos, "owner") or usuario
88
+ ficha = card.lee(os.path.join(datos, f"{owner}.md"))
89
+ invitacion_ = {
90
+ "version": 1,
91
+ "id": cities.identidad(datos),
92
+ "name": cities.nombre(datos),
93
+ "owner": owner,
94
+ "address": cities.direccion(usuario, datos),
95
+ "domain": domains.de_ciudad(datos),
96
+ }
97
+ if ficha.get("role"):
98
+ invitacion_["role"] = ficha["role"]
99
+ return invitacion_
100
+
101
+
102
+ def _pon(datos, destino, local=False):
103
+ caminos = [r for r in lee(datos) if r["id"] != destino["id"]]
104
+ nuevo = dict(destino)
105
+ if local:
106
+ nuevo["local"] = True
107
+ caminos.append(nuevo)
108
+ escribe(datos, caminos)
109
+
110
+
111
+ def conecta(origen, destino, usuario=""):
112
+ """Connect two cities on this machine, symmetrically."""
113
+ usuario = usuario or cities.usuario_actual()
114
+ if cities.identidad(origen) == cities.identidad(destino):
115
+ raise ValueError("a city cannot build a road to itself")
116
+ _pon(origen, invitacion(destino, usuario), local=True)
117
+ _pon(destino, invitacion(origen, usuario), local=True)
118
+
119
+
120
+ def conecta_invitacion(origen, destino):
121
+ """Add the local half of a road to a remote city invitation."""
122
+ necesarios = {"id", "name", "owner", "address"}
123
+ if not isinstance(destino, dict) or not necesarios <= set(destino):
124
+ raise ValueError("the invitation is missing id, name, owner or address")
125
+ if not set(destino) <= INVITATION_KEYS:
126
+ raise ValueError("the invitation contains fields other than public city identity")
127
+ limpia = _limpia(destino)
128
+ if not limpia:
129
+ raise ValueError("the invitation has invalid or inconsistent identity")
130
+ origen_publico = invitacion(origen)
131
+ if limpia["id"] == origen_publico["id"] or limpia["address"] == origen_publico["address"]:
132
+ raise ValueError("a city cannot build a road to itself")
133
+ _pon(origen, limpia, local=False)
134
+
135
+
136
+ def desconecta(origen, destino_id):
137
+ antes = lee(origen)
138
+ despues = [r for r in antes if r["id"] != destino_id]
139
+ escribe(origen, despues)
140
+ return len(antes) != len(despues)
141
+
142
+
143
+ def desconecta_local(origen, destino):
144
+ a = desconecta(origen, cities.identidad(destino))
145
+ b = desconecta(destino, cities.identidad(origen))
146
+ return a or b
147
+
148
+
149
+ def quita_incidentes(datos, usuario=""):
150
+ """Remove every local edge touching a city; used by a scoped reset."""
151
+ usuario = usuario or cities.usuario_actual()
152
+ ident = cities.identidad(datos)
153
+ for otra in cities.lista(usuario):
154
+ if os.path.realpath(otra["ruta"]) != os.path.realpath(datos):
155
+ desconecta(otra["ruta"], ident)
156
+ escribe(datos, [])
157
+
158
+
159
+ def destino_local(camino, usuario=""):
160
+ """Resolve a road record to a city currently present on this machine."""
161
+ if not camino.get("local"):
162
+ return ""
163
+ usuario = usuario or cities.usuario_actual()
164
+ return next((c["ruta"] for c in cities.lista(usuario) if c["id"] == camino.get("id")), "")
165
+
166
+
167
+ def _resuelve(nombre, usuario):
168
+ datos = cities.resuelve(nombre, usuario)
169
+ if not datos:
170
+ conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
171
+ raise ValueError(f"no city called {nombre!r}; known here: {conocidas}")
172
+ return datos
173
+
174
+
175
+ def _ayuda():
176
+ return """usage:
177
+ agents-city road list <city>
178
+ agents-city road connect <city> <local-city|invitation.json>
179
+ agents-city road disconnect <city> <local-city|city-id>
180
+ agents-city road invite <city>
181
+
182
+ Roads are explicit. Connecting two local cities writes both ends. For a remote
183
+ city, exchange the public JSON printed by `road invite`; it contains no token."""
184
+
185
+
186
+ def main():
187
+ args = sys.argv[1:]
188
+ if not args or args[0] in ("-h", "--help", "help"):
189
+ print(_ayuda())
190
+ return 0
191
+ usuario = cities.usuario_actual()
192
+ try:
193
+ if args[0] == "list" and len(args) == 2:
194
+ origen = _resuelve(args[1], usuario)
195
+ caminos = lee(origen)
196
+ if not caminos:
197
+ print(f" {cities.nombre(origen)} has no roads.")
198
+ for r in caminos:
199
+ local = "local" if destino_local(r, usuario) else "remote"
200
+ print(f" {r['name']} {r['address']} [{local}]")
201
+ return 0
202
+ if args[0] == "invite" and len(args) == 2:
203
+ print(json.dumps(invitacion(_resuelve(args[1], usuario)), indent=2))
204
+ return 0
205
+ if args[0] == "connect" and len(args) == 3:
206
+ origen = _resuelve(args[1], usuario)
207
+ destino = cities.resuelve(args[2], usuario)
208
+ if destino:
209
+ conecta(origen, destino, usuario)
210
+ print(f" Road open: {cities.nombre(origen)} <-> {cities.nombre(destino)}")
211
+ else:
212
+ with open(os.path.expanduser(args[2]), encoding="utf-8") as f:
213
+ conecta_invitacion(origen, json.load(f))
214
+ print(f" Remote road added to {cities.nombre(origen)}.")
215
+ return 0
216
+ if args[0] == "disconnect" and len(args) == 3:
217
+ origen = _resuelve(args[1], usuario)
218
+ destino = cities.resuelve(args[2], usuario)
219
+ cambiado = desconecta_local(origen, destino) if destino else desconecta(origen, args[2])
220
+ print(" Road closed." if cambiado else " No such road.")
221
+ return 0
222
+ except (OSError, ValueError, json.JSONDecodeError) as e:
223
+ print(f" {e}", file=sys.stderr)
224
+ return 1
225
+ print(_ayuda(), file=sys.stderr)
226
+ return 1
227
+
228
+
229
+ if __name__ == "__main__":
230
+ sys.exit(main())
@@ -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