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,140 @@
1
+ #!/usr/bin/env python3
2
+ """What makes a house grow depends on who lives in it.
3
+
4
+ The map grew every building from merged pull requests. That is right for a code
5
+ agent and meaningless for everyone else — a knowledge agent whose work is a
6
+ folder of documents, or a coordinator whose work is decisions, would forever
7
+ show an empty lot no matter how much they did. So growth is polymorphic: each
8
+ agent kind declares how its floors are counted, over its own workspace, with no
9
+ assumption of git.
10
+
11
+ - **code**: floors = merged PRs (the existing signal), bricks = commits with no
12
+ PR. Counted from the mounted repos, exactly as before.
13
+ - **knowledge**: floors = documents in the workspace, bricks = recent edits.
14
+ A person with no git at all now has a house that grows as their knowledge does.
15
+ - **coordinator**: floors = recorded decisions (deliberations), bricks = notices.
16
+
17
+ Every counter takes the same `(agente, data)` and returns the same
18
+ `{floors, bricks, activity30, signal}` shape, so the map renders one model and
19
+ the reporter pushes one row regardless of kind. Pure counting over the
20
+ filesystem; the git-backed `code` counter is injected so this module needs no
21
+ network and stays trivially testable.
22
+ """
23
+
24
+ import os
25
+ import time
26
+
27
+ import deliberations
28
+ import workspace
29
+
30
+ #: Extensions that count as a knowledge document.
31
+ DOC_EXT = ('.md', '.txt', '.rst', '.org', '.pdf', '.doc', '.docx', '.adoc')
32
+ DIAS_RECIENTE = 30
33
+ _SEG_RECIENTE = DIAS_RECIENTE * 24 * 3600
34
+
35
+
36
+ def _ahora():
37
+ # Injected via env for deterministic tests; falls back to wall clock.
38
+ forzado = os.environ.get('CITY_GROWTH_NOW')
39
+ return float(forzado) if forzado else time.time()
40
+
41
+
42
+ def _cuenta_docs(raiz):
43
+ """(total docs, docs edited within DIAS_RECIENTE) under a folder tree."""
44
+ total = recientes = 0
45
+ ahora = _ahora()
46
+ for base, dirs, files in os.walk(raiz):
47
+ dirs[:] = [d for d in dirs if d not in ('.git', 'node_modules', '.cache')]
48
+ for f in files:
49
+ if f.lower().endswith(DOC_EXT):
50
+ total += 1
51
+ try:
52
+ if ahora - os.path.getmtime(os.path.join(base, f)) < _SEG_RECIENTE:
53
+ recientes += 1
54
+ except OSError:
55
+ pass
56
+ return total, recientes
57
+
58
+
59
+ def _resultado(floors, bricks, actividad, signal):
60
+ return {'floors': floors, 'bricks': bricks, 'activity30': actividad, 'signal': signal}
61
+
62
+
63
+ def _knowledge(agente, data, contador_git=None):
64
+ total = recientes = 0
65
+ vistos = set()
66
+ # The workspace always counts and mounts add to it — `or`-falling-back made
67
+ # the agent's own documents vanish the moment it mounted anything.
68
+ for destino in [agente.workspace] + list(workspace.mount_targets(agente, data) or []):
69
+ real = os.path.realpath(destino) if destino else ''
70
+ if not real or real in vistos or not os.path.isdir(real):
71
+ continue
72
+ vistos.add(real)
73
+ t, r = _cuenta_docs(real)
74
+ total += t
75
+ recientes += r
76
+ return _resultado(total, recientes, recientes, 'documents')
77
+
78
+
79
+ def _coordinator(agente, data, contador_git=None):
80
+ # Count decisions through the canonical deliberation reader, not a re-glob:
81
+ # the store is a tree of `deliberations/<id>/` dirs, so listing `*.json`
82
+ # would always find nothing.
83
+ registros = deliberations.lista(data)
84
+ ahora = _ahora()
85
+ recientes = 0
86
+ for d in registros:
87
+ cuando = d.get('updated_at') or d.get('created_at')
88
+ try:
89
+ if cuando and ahora - _epoch(cuando) < _SEG_RECIENTE:
90
+ recientes += 1
91
+ except (ValueError, TypeError):
92
+ pass
93
+ return _resultado(len(registros), 0, recientes, 'decisions')
94
+
95
+
96
+ def _code(agente, data, contador_git=None):
97
+ """Floors from merged PRs via an injected git counter; 0 without one.
98
+
99
+ Kept injectable so this module needs no git and no network: the map/reporter
100
+ passes the existing PR counter, the tests pass a stub.
101
+ """
102
+ if contador_git is None:
103
+ return _resultado(0, 0, 0, 'pull-requests')
104
+ prs, commits, act = contador_git(agente, data)
105
+ return _resultado(prs, commits, act, 'pull-requests')
106
+
107
+
108
+ #: One counter per kind. Every counter has the same (agente, data, contador_git)
109
+ #: signature — the non-code ones ignore contador_git — so dispatch is uniform,
110
+ #: no special case. Keyed off workspace.CLASES so the kind vocabulary lives once;
111
+ #: `crece` degrades an unknown kind to _knowledge rather than crashing, so a new
112
+ #: kind added to workspace.CLASES stays safe here until a counter is written.
113
+ _CONTADORES = {
114
+ 'code': _code,
115
+ 'knowledge': _knowledge,
116
+ 'coordinator': _coordinator,
117
+ }
118
+
119
+
120
+ def _epoch(valor):
121
+ """An ISO-8601 or epoch-ish timestamp -> epoch seconds. Raises on garbage.
122
+
123
+ A timezone-naive timestamp is read as UTC (not local time), so a coordinator's
124
+ recency window does not drift by the machine's UTC offset.
125
+ """
126
+ import datetime
127
+ texto = str(valor)
128
+ try:
129
+ return float(texto)
130
+ except ValueError:
131
+ dt = datetime.datetime.fromisoformat(texto.replace('Z', '+00:00'))
132
+ if dt.tzinfo is None:
133
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
134
+ return dt.timestamp()
135
+
136
+
137
+ def crece(agente, data, contador_git=None):
138
+ """The growth of one agent's house, dispatched uniformly on its kind."""
139
+ contador = _CONTADORES.get(agente.clase, _knowledge)
140
+ return contador(agente, data, contador_git)
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only summaries of durable committee acts for the local Hall."""
3
+ import json
4
+ import os
5
+ import re
6
+
7
+ ID_RE = re.compile(r'^delib_[a-z0-9][a-z0-9_-]{5,79}$')
8
+
9
+
10
+ def lista(datos):
11
+ root = os.path.join(os.path.realpath(datos), 'deliberations')
12
+ try:
13
+ nombres = sorted(os.listdir(root), reverse=True)
14
+ except OSError:
15
+ return []
16
+ fuera = []
17
+ for ident in nombres:
18
+ if not ID_RE.match(ident):
19
+ continue
20
+ try:
21
+ state = json.load(open(os.path.join(root, ident, 'state.json'),
22
+ encoding='utf-8'))
23
+ if state.get('schema') != 'agents-city/deliberation@1':
24
+ continue
25
+ brief = state['brief']
26
+ decisions = state.get('decisions') or []
27
+ decision = decisions[-1] if decisions else {}
28
+ verification = decision.get('verification') or {}
29
+ fuera.append({
30
+ 'id': ident,
31
+ 'status': state.get('status', 'unknown'),
32
+ 'question': brief.get('question', ''),
33
+ 'desired_outcome': brief.get('desiredOutcome', ''),
34
+ 'participants': brief.get('participants', []),
35
+ 'participant_roles': state.get('participantRoles', {}),
36
+ 'received': len(state.get('positions') or {}),
37
+ 'total': len(brief.get('participants') or []),
38
+ 'revision': (state.get('progress') or {}).get('revision', 1),
39
+ 'decision': decision.get('outcome', ''),
40
+ 'contributors': decision.get('decisiveContributors', []),
41
+ 'verifier': decision.get('verifier', ''),
42
+ 'verification': verification.get('result', ''),
43
+ 'updated_at': state.get('updatedAt', ''),
44
+ 'act': os.path.join(root, ident, 'ACT.md'),
45
+ })
46
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
47
+ continue
48
+ return fuera
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """doctor: detect an old config shape, explain it, back it up, rewrite it.
3
+
4
+ The runtime reads one config schema — the current one. It keeps no long-lived
5
+ aliases that silently accept renamed or malformed keys, because a silent alias
6
+ is how two subtly different shapes drift apart. Instead, every change that can
7
+ invalidate an existing config ships a migration here: `doctor` finds the old
8
+ shape, says in one line what it is doing, writes a timestamped backup, and
9
+ rewrites the file to canonical form.
10
+
11
+ Each migration is a small object with three parts — does it apply, what does it
12
+ change, and the rewrite — so adding one is additive and never touches the
13
+ others (open for extension). `--dry-run` reports without writing; the default
14
+ writes a `.bak-<stamp>` next to the file first. Pure enough to test against a
15
+ temp file with no real config in sight.
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+ from dataclasses import dataclass
23
+ from typing import Callable
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Migracion:
28
+ id: str
29
+ explica: str
30
+ aplica: Callable[[dict], bool]
31
+ reescribe: Callable[[dict], dict]
32
+
33
+
34
+ def _renombra_bus_url(cfg):
35
+ cfg = dict(cfg)
36
+ cfg['roadsUrl'] = cfg.pop('busUrl')
37
+ return cfg
38
+
39
+
40
+ def _sella_bind(cfg):
41
+ cfg = dict(cfg)
42
+ gw = dict(cfg.get('gateway', {}))
43
+ gw.setdefault('bind', 'loopback')
44
+ cfg['gateway'] = gw
45
+ return cfg
46
+
47
+
48
+ def _cage_por_defecto(cfg):
49
+ cfg = dict(cfg)
50
+ seg = dict(cfg.get('security', {}))
51
+ if 'cage' not in seg:
52
+ seg['cage'] = True
53
+ cfg['security'] = seg
54
+ return cfg
55
+
56
+
57
+ #: Ordered so a file needing several migrations converges in one pass. Each is
58
+ #: idempotent: re-running doctor on a canonical file changes nothing.
59
+ MIGRACIONES = [
60
+ Migracion(
61
+ 'bus-url-renamed-to-roads-url',
62
+ 'the transport key `busUrl` is now `roadsUrl`',
63
+ lambda c: 'busUrl' in c,
64
+ _renombra_bus_url,
65
+ ),
66
+ Migracion(
67
+ 'gateway-bind-defaults-to-loopback',
68
+ 'a gateway block without an explicit `bind` now pins `loopback`',
69
+ lambda c: isinstance(c.get('gateway'), dict) and 'bind' not in c['gateway'],
70
+ _sella_bind,
71
+ ),
72
+ Migracion(
73
+ 'cage-on-by-default',
74
+ 'the per-window cage is recorded as on unless the owner set it',
75
+ # Guard the type like migration 2: a `security` that is present but not
76
+ # a dict (null/str/list) must not crash the tool that exists to fix it.
77
+ # Absent security counts as an empty dict, so the default is still added.
78
+ lambda c: isinstance(c.get('security', {}), dict) and 'cage' not in c.get('security', {}),
79
+ _cage_por_defecto,
80
+ ),
81
+ ]
82
+
83
+
84
+ def _aplica(cfg):
85
+ """The single migration walk: return (nuevo, [Migracion aplicadas]).
86
+
87
+ Everything else is a view of this — `diagnostica` wants the objects,
88
+ `cura` wants their ids, `cura_fichero` wants both — so the apply/rewrite
89
+ logic lives here once and never runs twice per file.
90
+ """
91
+ aplicadas = []
92
+ trabajo = dict(cfg)
93
+ for m in MIGRACIONES:
94
+ if m.aplica(trabajo):
95
+ trabajo = m.reescribe(trabajo)
96
+ aplicadas.append(m)
97
+ return trabajo, aplicadas
98
+
99
+
100
+ def diagnostica(cfg):
101
+ """Return the ordered migrations that still apply to `cfg`."""
102
+ return _aplica(cfg)[1]
103
+
104
+
105
+ def cura(cfg):
106
+ """Apply every applicable migration in order; return (nuevo, ids_aplicadas)."""
107
+ nuevo, aplicadas = _aplica(cfg)
108
+ return nuevo, [m.id for m in aplicadas]
109
+
110
+
111
+ def _stamp_backup(ruta, marca):
112
+ """Copy the file to a timestamped sibling before rewriting. Marca is passed
113
+ in (never read from the clock here) so a caller controls naming and tests
114
+ stay deterministic."""
115
+ destino = f'{ruta}.bak-{marca}'
116
+ with open(ruta, encoding='utf-8') as f:
117
+ contenido = f.read()
118
+ with open(destino, 'w', encoding='utf-8') as f:
119
+ f.write(contenido)
120
+ os.chmod(destino, 0o600)
121
+ return destino
122
+
123
+
124
+ def cura_fichero(ruta, marca, dry_run=False):
125
+ """Diagnose and (unless dry-run) rewrite a config file. Returns a report."""
126
+ with open(ruta, encoding='utf-8') as f:
127
+ cfg = json.load(f)
128
+ nuevo, pendientes = _aplica(cfg) # one walk: report and rewrite come from it
129
+ reporte = {'file': ruta, 'migrations': [m.id for m in pendientes],
130
+ 'explains': [m.explica for m in pendientes], 'wrote': False, 'backup': None}
131
+ if not pendientes or dry_run:
132
+ return reporte
133
+ reporte['backup'] = _stamp_backup(ruta, marca)
134
+ tmp = f'{ruta}.tmp'
135
+ with open(tmp, 'w', encoding='utf-8') as f:
136
+ json.dump(nuevo, f, indent=2, sort_keys=True)
137
+ # Preserve the original file's mode rather than inheriting the umask default
138
+ # (0644): a config that was 0600 must not be silently widened by a migration.
139
+ try:
140
+ os.chmod(tmp, os.stat(ruta).st_mode & 0o777)
141
+ except OSError:
142
+ os.chmod(tmp, 0o600)
143
+ os.replace(tmp, ruta)
144
+ reporte['wrote'] = True
145
+ return reporte
146
+
147
+
148
+ def main():
149
+ p = argparse.ArgumentParser(description='Detect, explain and migrate an old config shape.')
150
+ p.add_argument('fichero', help='the config JSON file to check')
151
+ p.add_argument('--fix', action='store_true', help='rewrite (default is dry-run report)')
152
+ p.add_argument('--stamp', default='manual', help='backup suffix (tests pass a fixed value)')
153
+ args = p.parse_args()
154
+ try:
155
+ reporte = cura_fichero(args.fichero, args.stamp, dry_run=not args.fix)
156
+ except (OSError, json.JSONDecodeError) as e:
157
+ print(f'{e}', file=sys.stderr)
158
+ return 1
159
+ if not reporte['migrations']:
160
+ print('config is already canonical')
161
+ return 0
162
+ for ident, porque in zip(reporte['migrations'], reporte['explains'], strict=True):
163
+ print(f' {ident}: {porque}')
164
+ if reporte['wrote']:
165
+ print(f'rewritten; backup at {reporte["backup"]}')
166
+ else:
167
+ print('dry-run: pass --fix to apply')
168
+ return 0
169
+
170
+
171
+ if __name__ == '__main__':
172
+ sys.exit(main())
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ """Built-in work domains and the knowledge packs installed into one city.
3
+
4
+ A domain is the context of the city (software, healthcare, legal, finance...).
5
+ A role is the responsibility of its one owner seat inside that domain. Keeping
6
+ those two choices separate is important: ``data`` means something different in a
7
+ software product from a clinical-data responsibility, while neither is the city
8
+ itself.
9
+
10
+ The catalogue is plain Markdown under ``plugin/domains``. On selection, the
11
+ chosen domain pack and every non-blank seat/repo role pack are copied into the
12
+ city's own ``domains/`` and ``roles/`` folders. They are ordinary, editable files
13
+ after that. This is city knowledge, not a repo skill: Agents City never installs
14
+ or changes skills in a referenced repo.
15
+ """
16
+
17
+ import glob
18
+ import os
19
+ import re
20
+ import shutil
21
+
22
+ import cities
23
+
24
+
25
+ AQUI = os.path.dirname(os.path.abspath(__file__))
26
+ PACKS = os.path.realpath(os.path.join(AQUI, "..", "domains"))
27
+ ROLE_PACKS = os.path.realpath(os.path.join(AQUI, "..", "roles", "examples"))
28
+
29
+ # Old cities called the domain ``kind`` and used these ids. Read them forever,
30
+ # but every new write uses the explicit ``domain`` field and canonical id.
31
+ ALIASES = {"product": "software", "blank": "custom"}
32
+
33
+
34
+ def canonico(valor):
35
+ return ALIASES.get(str(valor or "").strip().lower(), str(valor or "").strip().lower())
36
+
37
+
38
+ def _frontmatter(texto):
39
+ if not texto.startswith("---\n"):
40
+ return ""
41
+ partes = texto.split("---", 2)
42
+ return partes[1] if len(partes) == 3 else ""
43
+
44
+
45
+ def _escalar(fm, clave, defecto=""):
46
+ m = re.search(rf"^{re.escape(clave)}:[ \t]*(.*)$", fm, re.M)
47
+ return (m.group(1).strip() if m else "") or defecto
48
+
49
+
50
+ def _lista(fm, clave):
51
+ """Indented ``- value`` rows below one frontmatter key."""
52
+ m = re.search(rf"^{re.escape(clave)}:[ \t]*\n((?:[ \t]+-[^\n]*\n?)*)", fm, re.M)
53
+ if not m:
54
+ return []
55
+ fuera = []
56
+ for linea in m.group(1).splitlines():
57
+ valor = re.sub(r"^[ \t]+-[ \t]*", "", linea).strip()
58
+ if not valor:
59
+ continue
60
+ base, _, nota = valor.partition("#")
61
+ fuera.append((base.strip().rstrip(), "on by default" in nota.lower()))
62
+ return fuera
63
+
64
+
65
+ def _unidades(fm):
66
+ fuera = []
67
+ for valor, _ in _lista(fm, "units"):
68
+ nombre, sep, color = valor.rpartition(";")
69
+ if sep and re.fullmatch(r"[0-9a-fA-F]{6}", color.strip()):
70
+ fuera.append((nombre.strip(), color.strip().lower()))
71
+ return fuera
72
+
73
+
74
+ def lee(ruta):
75
+ try:
76
+ texto = open(ruta, encoding="utf-8").read()
77
+ except OSError:
78
+ return {}
79
+ fm = _frontmatter(texto)
80
+ ident = canonico(_escalar(fm, "id", os.path.basename(ruta)[:-3]))
81
+ roles = _lista(fm, "roles")
82
+ return {
83
+ "id": ident,
84
+ "order": int(_escalar(fm, "order", "999")),
85
+ "name": _escalar(fm, "name", ident.replace("-", " ").title()),
86
+ "summary": _escalar(fm, "summary"),
87
+ "parcel": _escalar(fm, "parcel", "a working repository"),
88
+ "source": _escalar(fm, "source", "github|disk"),
89
+ "grows": _escalar(fm, "grows_with"),
90
+ "grow_cmd": _escalar(fm, "grow_command"),
91
+ "roles": roles,
92
+ "units": _unidades(fm),
93
+ "path": ruta,
94
+ "text": texto,
95
+ }
96
+
97
+
98
+ def catalogo():
99
+ fuera = [lee(ruta) for ruta in glob.glob(os.path.join(PACKS, "*.md"))]
100
+ return sorted((p for p in fuera if p.get("id")), key=lambda p: (p["order"], p["name"].lower()))
101
+
102
+
103
+ def obtiene(ident):
104
+ buscado = canonico(ident)
105
+ return next((p for p in catalogo() if p["id"] == buscado), None)
106
+
107
+
108
+ def roles_de(ident):
109
+ pack = obtiene(ident)
110
+ return list(pack["roles"]) if pack else []
111
+
112
+
113
+ def dominio_de_rol(rol, defecto="software"):
114
+ for pack in catalogo():
115
+ if any(r == rol for r, _ in pack["roles"]):
116
+ return pack["id"]
117
+ return defecto
118
+
119
+
120
+ def de_ciudad(datos, defecto="software"):
121
+ explicito = cities.lee_clave(datos, "domain")
122
+ legado = cities.lee_clave(datos, "kind")
123
+ elegido = canonico(explicito or legado or defecto)
124
+ return elegido if obtiene(elegido) else canonico(defecto)
125
+
126
+
127
+ def selecciona(datos, ident):
128
+ """Persist the canonical domain without rewriting city identity metadata."""
129
+ ident = canonico(ident)
130
+ if not obtiene(ident):
131
+ raise ValueError(f"unknown city domain: {ident}")
132
+ ruta = os.path.join(datos, "city.yml")
133
+ try:
134
+ texto = open(ruta, encoding="utf-8").read()
135
+ except OSError:
136
+ texto = ""
137
+ if re.search(r"^domain:", texto, re.M):
138
+ texto = re.sub(r"^domain:.*$", f"domain: {ident}", texto, count=1, flags=re.M)
139
+ else:
140
+ if texto and not texto.endswith("\n"):
141
+ texto += "\n"
142
+ texto += f"domain: {ident}\n"
143
+ cities.escribe_atomico(ruta, texto)
144
+ return ident
145
+
146
+
147
+ INSTRUCCIONES = """# Agents City seat
148
+
149
+ This folder is one autonomous city, not a source-code repository and not a team
150
+ of people. Its only public actor is the owner seat; referenced repos are private
151
+ support agents behind that chair.
152
+
153
+ Before acting for this city:
154
+
155
+ 1. Read `city.yml` for the selected `domain` and owner.
156
+ 2. Read the owner card `<owner>.md` for the role, goal and support repos.
157
+ 3. Read `domains/<domain>.md` and `roles/<role>.md` when that role file exists.
158
+ `blank` deliberately has no role file. These are transparent, editable
159
+ operating knowledge for this city, not hidden prompts.
160
+ 4. Read each support agent's `role.<repo>` assignment from the owner card before
161
+ asking it for evidence. Use a chaired committee when several matter.
162
+ Repo agents do not talk directly to each other.
163
+
164
+ Skills remain installed in the referenced repos. Agents City only recognises
165
+ them; it never copies, installs or enables a repo skill from this folder.
166
+ """
167
+
168
+
169
+ def materializa(datos, ident, rol):
170
+ """Copy selected built-in knowledge into a city, never over user edits.
171
+
172
+ A later role/domain switch gets its own file, so changing back restores the
173
+ city's edited copy. Existing files are deliberately not refreshed in place.
174
+ """
175
+ pack = obtiene(ident)
176
+ if not pack:
177
+ return []
178
+ hechos = []
179
+ carpeta_dominios = os.path.join(datos, "domains")
180
+ carpeta_roles = os.path.join(datos, "roles")
181
+ os.makedirs(carpeta_dominios, exist_ok=True)
182
+ os.makedirs(carpeta_roles, exist_ok=True)
183
+
184
+ destino_dominio = os.path.join(carpeta_dominios, f"{pack['id']}.md")
185
+ if not os.path.exists(destino_dominio):
186
+ shutil.copy2(pack["path"], destino_dominio)
187
+ hechos.append(f"domains/{pack['id']}.md")
188
+
189
+ rol_seguro = str(rol or "")
190
+ if re.fullmatch(r"[a-z0-9-]+", rol_seguro):
191
+ fuente_rol = os.path.join(ROLE_PACKS, f"{rol_seguro}.md")
192
+ destino_rol = os.path.join(carpeta_roles, f"{rol_seguro}.md")
193
+ if os.path.isfile(fuente_rol) and not os.path.exists(destino_rol):
194
+ shutil.copy2(fuente_rol, destino_rol)
195
+ hechos.append(f"roles/{rol_seguro}.md")
196
+
197
+ instrucciones = os.path.join(datos, "AGENTS.md")
198
+ if not os.path.exists(instrucciones):
199
+ cities.escribe_atomico(instrucciones, INSTRUCCIONES)
200
+ hechos.append("AGENTS.md")
201
+ return hechos
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """The evidence vocabulary: what an audit line actually proves.
3
+
4
+ A hash chain gives tamper-evidence — that a line was not changed after the
5
+ fact. It says nothing about whether the thing the line describes was truly
6
+ authorised, merely observed, or never established at all. Those are different
7
+ claims, and collapsing them into a boolean `ok` is how a missing check comes to
8
+ read as a pass.
9
+
10
+ So every consequential record carries one of these states, and two rules hold
11
+ everywhere they are read:
12
+
13
+ - `DESCONOCIDO` never means allowed. A missing, stale, reused or unverifiable
14
+ binding is unknown, and unknown is not consent.
15
+ - `AMBIGUO` is never resolved by picking the first or latest candidate. If the
16
+ producer cannot say which decision governed, the record says so.
17
+
18
+ Only `IMPUESTO` — a decision that actually changed the outcome — is proof that
19
+ a control fired. `ATRIBUIDO` means the actor is known but no control was
20
+ exercised; `NO_IMPUESTO` means a control could have fired and did not.
21
+ """
22
+
23
+ from enum import Enum
24
+
25
+
26
+ class Evidencia(str, Enum):
27
+ IMPUESTO = 'enforced' # a decision that changed the outcome
28
+ ATRIBUIDO = 'attribution-only' # actor known, no control exercised
29
+ NO_IMPUESTO = 'unattributed' # a control could apply, none did
30
+ DESCONOCIDO = 'unknown' # missing/stale/reused/unverifiable — never allowed
31
+ AMBIGUO = 'ambiguous' # producer cannot say which decision governed
32
+ NO_SOPORTADO = 'unsupported' # this path has no authoritative integration
33
+
34
+ def __str__(self):
35
+ return self.value
36
+
37
+
38
+ def autoriza(estado):
39
+ """True only for an evidence state that positively authorises the action.
40
+
41
+ A strict whitelist, so a new evidence state authorises nothing until it is
42
+ deliberately added here: only `IMPUESTO` passes. `ATRIBUIDO` deliberately
43
+ does NOT — knowing who asked is not the same as a control having permitted
44
+ it, so the caller must pair attribution with an explicit enforced decision.
45
+
46
+ Fails closed on unrecognised input via `normaliza`: an unknown wire string
47
+ becomes `DESCONOCIDO` (which does not authorise) rather than raising, so a
48
+ caller that skipped normalisation gets `False`, never an exception.
49
+ """
50
+ return normaliza(estado) is Evidencia.IMPUESTO
51
+
52
+
53
+ def normaliza(valor, defecto=Evidencia.DESCONOCIDO):
54
+ """Coerce arbitrary input to an Evidencia, failing safe to DESCONOCIDO.
55
+
56
+ Unrecognised input is never guessed toward a permissive state: an unknown
57
+ string becomes `DESCONOCIDO`, which authorises nothing.
58
+ """
59
+ if isinstance(valor, Evidencia):
60
+ return valor
61
+ try:
62
+ return Evidencia(str(valor))
63
+ except ValueError:
64
+ return defecto