agents-city 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2226 -0
  5. package/README.md +2196 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +119 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/doctor +3 -0
  28. package/bin/exit +5 -0
  29. package/bin/hall +73 -0
  30. package/bin/hall.html +498 -0
  31. package/bin/logs +3 -0
  32. package/bin/navegador.mjs +243 -0
  33. package/bin/report.py +14 -0
  34. package/bin/reset +3 -0
  35. package/bin/road +3 -0
  36. package/bin/seat +10 -0
  37. package/bin/serve.py +1579 -0
  38. package/bin/setup.py +753 -0
  39. package/bin/shortcut +3 -0
  40. package/bin/skills +3 -0
  41. package/bin/test +133 -0
  42. package/bin/test-actualiza.py +130 -0
  43. package/bin/test-adapter.py +183 -0
  44. package/bin/test-admision.py +99 -0
  45. package/bin/test-atajos.py +301 -0
  46. package/bin/test-avatar.py +58 -0
  47. package/bin/test-benchmark.py +40 -0
  48. package/bin/test-broker.py +334 -0
  49. package/bin/test-cage.py +374 -0
  50. package/bin/test-card.py +402 -0
  51. package/bin/test-channel.py +379 -0
  52. package/bin/test-cities.py +453 -0
  53. package/bin/test-claude-runtime.py +252 -0
  54. package/bin/test-committee.py +340 -0
  55. package/bin/test-contracts.py +551 -0
  56. package/bin/test-crecimiento.py +164 -0
  57. package/bin/test-demo.py +308 -0
  58. package/bin/test-doctor.py +139 -0
  59. package/bin/test-domains.py +151 -0
  60. package/bin/test-evidencia.py +50 -0
  61. package/bin/test-exit.py +128 -0
  62. package/bin/test-hall-protocol.py +93 -0
  63. package/bin/test-launch.py +169 -0
  64. package/bin/test-live-feed.py +508 -0
  65. package/bin/test-navegador.py +140 -0
  66. package/bin/test-pairing.py +99 -0
  67. package/bin/test-parcels.py +283 -0
  68. package/bin/test-runtime-failures.py +252 -0
  69. package/bin/test-runtime-ui.py +488 -0
  70. package/bin/test-runtime.py +268 -0
  71. package/bin/test-rutas.py +80 -0
  72. package/bin/test-seat.py +1598 -0
  73. package/bin/test-security.py +150 -0
  74. package/bin/test-serve.py +1251 -0
  75. package/bin/test-stress.py +64 -0
  76. package/bin/test-widgets.py +143 -0
  77. package/bin/test-workspace.py +251 -0
  78. package/bin/testlib.py +118 -0
  79. package/bin/tokens.py +14 -0
  80. package/bin/update +3 -0
  81. package/bus/scripts/deploy.sh +60 -0
  82. package/bus/scripts/mint-token.sh +68 -0
  83. package/bus/scripts/setup-dev.sh +79 -0
  84. package/bus/scripts/test-channel.ts +8 -0
  85. package/bus/scripts/test-hub.ts +266 -0
  86. package/bus/scripts/test-local.sh +40 -0
  87. package/bus/scripts/test-queue.ts +132 -0
  88. package/bus/worker/package-lock.json +1971 -0
  89. package/bus/worker/package.json +17 -0
  90. package/bus/worker/src/index.ts +485 -0
  91. package/bus/worker/tsconfig.json +13 -0
  92. package/bus/worker/wrangler.toml +32 -0
  93. package/city/oven/README.md +59 -0
  94. package/city/oven/collect.py +43 -0
  95. package/city/oven/oven.html +4 -0
  96. package/city/scripts/history.py +120 -0
  97. package/city/scripts/seed.py +442 -0
  98. package/city/web/assets/modelos/License.txt +28 -0
  99. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  100. package/city/web/assets/modelos/building-a.glb +0 -0
  101. package/city/web/assets/modelos/building-b.glb +0 -0
  102. package/city/web/assets/modelos/building-c.glb +0 -0
  103. package/city/web/assets/modelos/building-d.glb +0 -0
  104. package/city/web/assets/modelos/building-e.glb +0 -0
  105. package/city/web/assets/modelos/building-f.glb +0 -0
  106. package/city/web/assets/modelos/building-g.glb +0 -0
  107. package/city/web/assets/modelos/building-h.glb +0 -0
  108. package/city/web/assets/sprites/building-a.png +0 -0
  109. package/city/web/assets/sprites/building-b.png +0 -0
  110. package/city/web/assets/sprites/building-c.png +0 -0
  111. package/city/web/assets/sprites/building-d.png +0 -0
  112. package/city/web/assets/sprites/building-e.png +0 -0
  113. package/city/web/assets/sprites/building-f.png +0 -0
  114. package/city/web/assets/sprites/building-g.png +0 -0
  115. package/city/web/assets/sprites/building-h.png +0 -0
  116. package/city/web/assets/sprites/building-i.png +0 -0
  117. package/city/web/assets/sprites/building-j.png +0 -0
  118. package/city/web/assets/sprites/building-k.png +0 -0
  119. package/city/web/assets/sprites/building-l.png +0 -0
  120. package/city/web/assets/sprites/building-m.png +0 -0
  121. package/city/web/assets/sprites/building-n.png +0 -0
  122. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  123. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  124. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  125. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  126. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  127. package/city/web/assets/sprites/catalogo.json +47 -0
  128. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  129. package/city/web/assets/sprites/detail-awning.png +0 -0
  130. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  131. package/city/web/assets/sprites/detail-overhang.png +0 -0
  132. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  133. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  143. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  144. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  145. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  146. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  147. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  148. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  149. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  150. package/city/web/assets/sprites/medidas.json +248 -0
  151. package/city/web/dist/city.js +939 -0
  152. package/city/web/dist/index.html +294 -0
  153. package/city/web/dist/modelos/License.txt +28 -0
  154. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  155. package/city/web/dist/modelos/building-a.glb +0 -0
  156. package/city/web/dist/modelos/building-b.glb +0 -0
  157. package/city/web/dist/modelos/building-c.glb +0 -0
  158. package/city/web/dist/modelos/building-d.glb +0 -0
  159. package/city/web/dist/modelos/building-e.glb +0 -0
  160. package/city/web/dist/modelos/building-f.glb +0 -0
  161. package/city/web/dist/modelos/building-g.glb +0 -0
  162. package/city/web/dist/modelos/building-h.glb +0 -0
  163. package/city/web/dist/sprites/building-a.png +0 -0
  164. package/city/web/dist/sprites/building-b.png +0 -0
  165. package/city/web/dist/sprites/building-c.png +0 -0
  166. package/city/web/dist/sprites/building-d.png +0 -0
  167. package/city/web/dist/sprites/building-e.png +0 -0
  168. package/city/web/dist/sprites/building-f.png +0 -0
  169. package/city/web/dist/sprites/building-g.png +0 -0
  170. package/city/web/dist/sprites/building-h.png +0 -0
  171. package/city/web/dist/sprites/building-i.png +0 -0
  172. package/city/web/dist/sprites/building-j.png +0 -0
  173. package/city/web/dist/sprites/building-k.png +0 -0
  174. package/city/web/dist/sprites/building-l.png +0 -0
  175. package/city/web/dist/sprites/building-m.png +0 -0
  176. package/city/web/dist/sprites/building-n.png +0 -0
  177. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  178. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  179. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  180. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  181. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  182. package/city/web/dist/sprites/catalogo.json +47 -0
  183. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  184. package/city/web/dist/sprites/detail-awning.png +0 -0
  185. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  186. package/city/web/dist/sprites/detail-overhang.png +0 -0
  187. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  188. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  198. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  199. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  200. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  201. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  202. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  203. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  204. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  205. package/city/web/dist/sprites/medidas.json +248 -0
  206. package/city/web/dist-hall/hall.js +1363 -0
  207. package/city/web/index.html +294 -0
  208. package/city/web/package.json +18 -0
  209. package/city/web/sella.py +21 -0
  210. package/city/web/src/activity-actors.ts +47 -0
  211. package/city/web/src/activity.ts +172 -0
  212. package/city/web/src/ayuntamiento.ts +580 -0
  213. package/city/web/src/draw.ts +640 -0
  214. package/city/web/src/game-speech.ts +79 -0
  215. package/city/web/src/hall.ts +1959 -0
  216. package/city/web/src/main.ts +2252 -0
  217. package/city/web/src/oven.ts +124 -0
  218. package/city/web/src/people.ts +244 -0
  219. package/city/web/src/presencia.ts +193 -0
  220. package/city/web/src/puertas.ts +180 -0
  221. package/city/web/src/repo-roles.ts +59 -0
  222. package/city/worker/package.json +16 -0
  223. package/city/worker/schema.sql +117 -0
  224. package/city/worker/src/index.ts +702 -0
  225. package/city/worker/src/square.ts +218 -0
  226. package/city/worker/wrangler.toml +58 -0
  227. package/demo/ada.md +50 -0
  228. package/demo/bruno.md +62 -0
  229. package/demo/camila.md +56 -0
  230. package/demo/city/ada.md +40 -0
  231. package/demo/city/city.yml +8 -0
  232. package/demo/city/parcels.yml +15 -0
  233. package/demo/city/roads.json +4 -0
  234. package/demo/city/units.yml +10 -0
  235. package/demo/clinica/city.yml +8 -0
  236. package/demo/clinica/parcels.yml +15 -0
  237. package/demo/clinica/roads.json +4 -0
  238. package/demo/clinica/units.yml +9 -0
  239. package/demo/clinica/vera.md +46 -0
  240. package/demo/dante.md +54 -0
  241. package/demo/despacho/city.yml +8 -0
  242. package/demo/despacho/marta.md +46 -0
  243. package/demo/despacho/parcels.yml +15 -0
  244. package/demo/despacho/roads.json +4 -0
  245. package/demo/despacho/units.yml +9 -0
  246. package/demo/elsa.md +50 -0
  247. package/demo/farid.md +52 -0
  248. package/demo/greta.md +50 -0
  249. package/demo/hugo.md +49 -0
  250. package/demo/iris.md +49 -0
  251. package/demo/jonas.md +51 -0
  252. package/demo/kira.md +51 -0
  253. package/demo/luca.md +49 -0
  254. package/demo/parcels.yml +92 -0
  255. package/demo/seed.py +20 -0
  256. package/demo/show.py +161 -0
  257. package/demo/stories.py +824 -0
  258. package/demo/units.yml +33 -0
  259. package/docs/agents-first.md +106 -0
  260. package/docs/glossary.md +55 -0
  261. package/docs/map-live-layers.md +73 -0
  262. package/docs/security.md +265 -0
  263. package/docs/self-host.md +151 -0
  264. package/docs/testing.md +205 -0
  265. package/package.json +73 -0
  266. package/plugin/.claude-plugin/plugin.json +62 -0
  267. package/plugin/.mcp.json +15 -0
  268. package/plugin/channel/activity-cli.ts +16 -0
  269. package/plugin/channel/adapter-prompts.ts +113 -0
  270. package/plugin/channel/adapter.js +4292 -0
  271. package/plugin/channel/adapter.ts +133 -0
  272. package/plugin/channel/bus.js +20188 -0
  273. package/plugin/channel/bus.ts +131 -0
  274. package/plugin/channel/city-config.ts +125 -0
  275. package/plugin/channel/claude-channel.ts +58 -0
  276. package/plugin/channel/cli-args.ts +75 -0
  277. package/plugin/channel/client.js +4200 -0
  278. package/plugin/channel/client.ts +38 -0
  279. package/plugin/channel/committee/activity.ts +256 -0
  280. package/plugin/channel/committee/collection.ts +154 -0
  281. package/plugin/channel/committee/decision.ts +175 -0
  282. package/plugin/channel/committee/floor.ts +135 -0
  283. package/plugin/channel/committee/guards.ts +30 -0
  284. package/plugin/channel/committee/history.ts +38 -0
  285. package/plugin/channel/committee/render.ts +102 -0
  286. package/plugin/channel/committee/service.ts +72 -0
  287. package/plugin/channel/committee/storage.ts +92 -0
  288. package/plugin/channel/committee/types.ts +163 -0
  289. package/plugin/channel/committee/view.ts +63 -0
  290. package/plugin/channel/committee-cli.ts +109 -0
  291. package/plugin/channel/delivery-metrics.ts +61 -0
  292. package/plugin/channel/delivery-queue.ts +118 -0
  293. package/plugin/channel/hub/activity-controller.ts +72 -0
  294. package/plugin/channel/hub/activity-feed.ts +169 -0
  295. package/plugin/channel/hub/committee-controller.ts +60 -0
  296. package/plugin/channel/hub/connections.ts +48 -0
  297. package/plugin/channel/hub/diagnostics.ts +67 -0
  298. package/plugin/channel/hub/envelope-validity.ts +109 -0
  299. package/plugin/channel/hub/envelopes.ts +66 -0
  300. package/plugin/channel/hub/lifecycle.ts +91 -0
  301. package/plugin/channel/hub/local-roads.ts +89 -0
  302. package/plugin/channel/hub/remote-roads.ts +165 -0
  303. package/plugin/channel/hub/road-controller.ts +99 -0
  304. package/plugin/channel/hub-client.ts +149 -0
  305. package/plugin/channel/local-hub.js +5995 -0
  306. package/plugin/channel/local-hub.ts +349 -0
  307. package/plugin/channel/map-reporter.ts +48 -0
  308. package/plugin/channel/package-lock.json +1731 -0
  309. package/plugin/channel/package.json +19 -0
  310. package/plugin/channel/protocol.ts +98 -0
  311. package/plugin/channel/road-cli.ts +18 -0
  312. package/plugin/channel/run.sh +15 -0
  313. package/plugin/channel/runtime/claude.ts +458 -0
  314. package/plugin/channel/runtime/codex-config.ts +77 -0
  315. package/plugin/channel/runtime/codex.ts +669 -0
  316. package/plugin/channel/runtime/command.ts +62 -0
  317. package/plugin/channel/runtime/factory.ts +15 -0
  318. package/plugin/channel/runtime/json-rpc.ts +154 -0
  319. package/plugin/channel/runtime/kimi.ts +277 -0
  320. package/plugin/channel/runtime/opencode.ts +248 -0
  321. package/plugin/channel/runtime/process.ts +84 -0
  322. package/plugin/channel/runtime/types.ts +45 -0
  323. package/plugin/channel/runtime-files.ts +73 -0
  324. package/plugin/channel/runtime-gateway.js +6396 -0
  325. package/plugin/channel/runtime-gateway.ts +358 -0
  326. package/plugin/channel/runtime-metrics.ts +52 -0
  327. package/plugin/channel/runtime-subscription.ts +180 -0
  328. package/plugin/channel/terminal-delivery.ts +131 -0
  329. package/plugin/channel/untrusted.ts +60 -0
  330. package/plugin/commands/committee.md +31 -0
  331. package/plugin/commands/exit.md +32 -0
  332. package/plugin/commands/goals.md +20 -0
  333. package/plugin/commands/join.md +18 -0
  334. package/plugin/commands/notice.md +20 -0
  335. package/plugin/commands/propose.md +19 -0
  336. package/plugin/commands/round.md +17 -0
  337. package/plugin/commands/session.md +22 -0
  338. package/plugin/commands/settings.md +25 -0
  339. package/plugin/commands/setup.md +21 -0
  340. package/plugin/commands/team.md +23 -0
  341. package/plugin/domains/custom.md +27 -0
  342. package/plugin/domains/finance.md +31 -0
  343. package/plugin/domains/healthcare.md +34 -0
  344. package/plugin/domains/legal.md +31 -0
  345. package/plugin/domains/marketing.md +33 -0
  346. package/plugin/domains/operations.md +31 -0
  347. package/plugin/domains/research.md +32 -0
  348. package/plugin/domains/sales.md +30 -0
  349. package/plugin/domains/software.md +37 -0
  350. package/plugin/hooks/activity.sh +6 -0
  351. package/plugin/hooks/digging.sh +68 -0
  352. package/plugin/hooks/growth.sh +42 -0
  353. package/plugin/hooks/hooks.json +106 -0
  354. package/plugin/hooks/notice-on-pr.sh +35 -0
  355. package/plugin/hooks/notice-on-stop.sh +80 -0
  356. package/plugin/hooks/notice-pending.sh +58 -0
  357. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  358. package/plugin/hooks/tokens.sh +47 -0
  359. package/plugin/roles/examples/account-executive.md +25 -0
  360. package/plugin/roles/examples/ai-manager.md +34 -0
  361. package/plugin/roles/examples/associate.md +24 -0
  362. package/plugin/roles/examples/brand-lead.md +20 -0
  363. package/plugin/roles/examples/cfo.md +20 -0
  364. package/plugin/roles/examples/city-lead.md +25 -0
  365. package/plugin/roles/examples/clinical-director.md +26 -0
  366. package/plugin/roles/examples/clinical-ops.md +25 -0
  367. package/plugin/roles/examples/clinician.md +25 -0
  368. package/plugin/roles/examples/compliance.md +27 -0
  369. package/plugin/roles/examples/content.md +24 -0
  370. package/plugin/roles/examples/controller.md +27 -0
  371. package/plugin/roles/examples/cpto.md +41 -0
  372. package/plugin/roles/examples/customer-success.md +25 -0
  373. package/plugin/roles/examples/data-engineer.md +37 -0
  374. package/plugin/roles/examples/data.md +60 -0
  375. package/plugin/roles/examples/dev.md +37 -0
  376. package/plugin/roles/examples/devops.md +39 -0
  377. package/plugin/roles/examples/enablement.md +24 -0
  378. package/plugin/roles/examples/ethics.md +24 -0
  379. package/plugin/roles/examples/fin-analytics.md +27 -0
  380. package/plugin/roles/examples/health-compliance.md +25 -0
  381. package/plugin/roles/examples/health-data.md +26 -0
  382. package/plugin/roles/examples/knowledge.md +25 -0
  383. package/plugin/roles/examples/lifecycle.md +26 -0
  384. package/plugin/roles/examples/llm-engineer.md +34 -0
  385. package/plugin/roles/examples/managing-partner.md +20 -0
  386. package/plugin/roles/examples/methods.md +23 -0
  387. package/plugin/roles/examples/operations-lead.md +23 -0
  388. package/plugin/roles/examples/ops.md +24 -0
  389. package/plugin/roles/examples/patient-safety.md +26 -0
  390. package/plugin/roles/examples/performance.md +27 -0
  391. package/plugin/roles/examples/po.md +36 -0
  392. package/plugin/roles/examples/process-owner.md +23 -0
  393. package/plugin/roles/examples/product-design.md +39 -0
  394. package/plugin/roles/examples/program-manager.md +24 -0
  395. package/plugin/roles/examples/quality.md +25 -0
  396. package/plugin/roles/examples/research-director.md +24 -0
  397. package/plugin/roles/examples/research-ops.md +25 -0
  398. package/plugin/roles/examples/researcher.md +25 -0
  399. package/plugin/roles/examples/revenue-lead.md +25 -0
  400. package/plugin/roles/examples/revops.md +26 -0
  401. package/plugin/roles/examples/seo.md +27 -0
  402. package/plugin/roles/examples/specialist.md +24 -0
  403. package/plugin/scripts/actualiza.py +198 -0
  404. package/plugin/scripts/admision.py +165 -0
  405. package/plugin/scripts/apaga.py +232 -0
  406. package/plugin/scripts/atajos.py +506 -0
  407. package/plugin/scripts/avatar.py +211 -0
  408. package/plugin/scripts/broker.py +531 -0
  409. package/plugin/scripts/cage.py +492 -0
  410. package/plugin/scripts/capabilities.py +210 -0
  411. package/plugin/scripts/card.py +322 -0
  412. package/plugin/scripts/cities.py +520 -0
  413. package/plugin/scripts/city-env.sh +90 -0
  414. package/plugin/scripts/city-runtime.sh +87 -0
  415. package/plugin/scripts/city-session.sh +552 -0
  416. package/plugin/scripts/city_env.py +67 -0
  417. package/plugin/scripts/crecimiento.py +140 -0
  418. package/plugin/scripts/deliberations.py +48 -0
  419. package/plugin/scripts/doctor.py +294 -0
  420. package/plugin/scripts/domains.py +201 -0
  421. package/plugin/scripts/evidencia.py +64 -0
  422. package/plugin/scripts/find-repos.sh +111 -0
  423. package/plugin/scripts/gh.py +159 -0
  424. package/plugin/scripts/hall_protocol.py +147 -0
  425. package/plugin/scripts/hook_activity.py +139 -0
  426. package/plugin/scripts/launch.py +94 -0
  427. package/plugin/scripts/logs.py +98 -0
  428. package/plugin/scripts/pairing.py +185 -0
  429. package/plugin/scripts/parcels.py +132 -0
  430. package/plugin/scripts/read-card.py +55 -0
  431. package/plugin/scripts/report.py +201 -0
  432. package/plugin/scripts/reset.py +180 -0
  433. package/plugin/scripts/roads.py +230 -0
  434. package/plugin/scripts/roles.py +274 -0
  435. package/plugin/scripts/runtime_log.py +69 -0
  436. package/plugin/scripts/runtime_processes.py +132 -0
  437. package/plugin/scripts/rutas.py +83 -0
  438. package/plugin/scripts/seat.py +1696 -0
  439. package/plugin/scripts/tokens.py +194 -0
  440. package/plugin/scripts/trust-repos.py +59 -0
  441. package/plugin/scripts/ui.py +280 -0
  442. package/plugin/scripts/units.py +80 -0
  443. package/plugin/scripts/workspace.py +561 -0
  444. package/plugin/skills/city/SKILL.md +191 -0
  445. package/templates/blank.md +39 -0
  446. package/templates/finance.md +44 -0
  447. package/templates/legal.md +45 -0
  448. package/templates/marketing.md +48 -0
  449. package/templates/product.md +45 -0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """Create a short, auditable launcher so tmux never has to paste long commands."""
3
+ import argparse
4
+ import base64
5
+ import json
6
+ import os
7
+ import shlex
8
+ import time
9
+
10
+ import runtime_processes
11
+
12
+
13
+ def safe(value):
14
+ cleaned = ''.join(c if c.isalnum() or c in '-_' else '-' for c in value.lower())
15
+ return cleaned.strip('-')[:80] or 'actor'
16
+
17
+
18
+ def make(args):
19
+ directory = os.path.join(runtime_processes.ruta(args.data), 'launchers')
20
+ app_home = os.path.realpath(os.environ.get('AGENTS_CITY_HOME') or
21
+ os.path.join(args.data, '..', '..'))
22
+ os.makedirs(directory, mode=0o700, exist_ok=True)
23
+ os.chmod(directory, 0o700)
24
+ path = os.path.join(directory, f'{safe(args.actor)}-{os.getpid()}-{time.time_ns()}.sh')
25
+ command = base64.b64encode(args.command.encode('utf-8')).decode('ascii')
26
+ failure = base64.b64encode(json.dumps({
27
+ 'sourceId': f'launcher:{args.actor}:{time.time_ns()}:failed',
28
+ 'kind': 'runtime.launch.failed',
29
+ 'phase': 'failed',
30
+ 'tone': 'error',
31
+ 'title': f'{args.actor} failed to start',
32
+ 'summary': 'The agent command exited before a usable session was ready.',
33
+ }).encode('utf-8')).decode('ascii')
34
+ log = os.path.realpath(os.path.join(os.path.dirname(__file__), 'runtime_log.py'))
35
+ client = os.path.realpath(args.client)
36
+ lines = [
37
+ '#!/usr/bin/env bash',
38
+ 'set +e',
39
+ f'DATA={shlex.quote(os.path.realpath(args.data))}',
40
+ f'APP_HOME={shlex.quote(app_home)}',
41
+ f'ACTOR={shlex.quote(args.actor)}',
42
+ f'WORKDIR={shlex.quote(os.path.realpath(args.cwd))}',
43
+ f'RUNTIME_DIR={shlex.quote(os.path.dirname(directory))}',
44
+ f'CLIENT={shlex.quote(client)}',
45
+ f'RUNTIME_LOG={shlex.quote(log)}',
46
+ f'COMMAND_B64={shlex.quote(command)}',
47
+ f'FAILURE_B64={shlex.quote(failure)}',
48
+ 'export AGENTS_CITY_HOME="$APP_HOME" AGENTS_CITY_DATA="$DATA" CITY_BUS_ACTOR="$ACTOR"',
49
+ 'python3 "$RUNTIME_LOG" launch.started --data "$DATA" --component launcher '
50
+ ' --actor "$ACTOR" --outcome starting',
51
+ 'cd "$WORKDIR"',
52
+ 'cd_status=$?',
53
+ 'if [ "$cd_status" -eq 0 ]; then',
54
+ ' command_text="$(printf %s "$COMMAND_B64" | base64 --decode)"',
55
+ ' eval "$command_text"',
56
+ ' status=$?',
57
+ 'else',
58
+ ' status=$cd_status',
59
+ 'fi',
60
+ 'if [ "$status" -eq 0 ]; then',
61
+ ' python3 "$RUNTIME_LOG" launch.exited --data "$DATA" --component launcher '
62
+ ' --actor "$ACTOR" --outcome ok --message "exit 0"',
63
+ 'else',
64
+ ' python3 "$RUNTIME_LOG" launch.failed --data "$DATA" --component launcher '
65
+ ' --actor "$ACTOR" --outcome failed --message "exit $status"',
66
+ ' printf %s "$FAILURE_B64" | base64 --decode | env AGENTS_CITY_DATA="$DATA" '
67
+ ' CITY_BUS_ACTOR="$ACTOR" node "$CLIENT" activity publish '
68
+ ' --input - >/dev/null 2>&1 || true',
69
+ ' printf "\\n Agents City: %s failed to start (exit %s).\\n" "$ACTOR" "$status" >&2',
70
+ ' printf " Diagnostics: %s/diagnostics.jsonl\\n\\n" "$RUNTIME_DIR" >&2',
71
+ 'fi',
72
+ 'exit "$status"',
73
+ '',
74
+ ]
75
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o700)
76
+ with os.fdopen(fd, 'w', encoding='utf-8') as stream:
77
+ stream.write('\n'.join(lines))
78
+ print(path)
79
+
80
+
81
+ def main():
82
+ parser = argparse.ArgumentParser()
83
+ parser.add_argument('verb', choices=['create'])
84
+ parser.add_argument('--data', required=True)
85
+ parser.add_argument('--actor', required=True)
86
+ parser.add_argument('--cwd', required=True)
87
+ parser.add_argument('--client', required=True)
88
+ parser.add_argument('--command', required=True)
89
+ args = parser.parse_args()
90
+ make(args)
91
+
92
+
93
+ if __name__ == '__main__':
94
+ main()
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """Read or follow one city's durable activity and diagnostic logs."""
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ import time
8
+
9
+ import city_env
10
+ import runtime_processes
11
+
12
+
13
+ def read(path):
14
+ try:
15
+ with open(path, encoding='utf-8') as stream:
16
+ return [json.loads(line) for line in stream if line.strip()]
17
+ except (OSError, ValueError, json.JSONDecodeError):
18
+ return []
19
+
20
+
21
+ def display(record, raw=False):
22
+ if raw:
23
+ print(json.dumps(record, ensure_ascii=False), flush=True)
24
+ return
25
+ at = str(record.get('at', ''))[11:19] or '--:--:--'
26
+ source = 'activity' if record.get('protocol') == 'agents-city-activity/1' else 'diagnostic'
27
+ actor = record.get('actor') or record.get('component') or 'city'
28
+ event = record.get('kind') or record.get('event') or 'event'
29
+ message = record.get('summary') or record.get('message') or record.get('outcome') or ''
30
+ print(f'{at} {source:<10} {str(actor):<18} {event} {message}', flush=True)
31
+
32
+
33
+ def main(): # noqa: C901 - CLI selection and follow mode share one stream lifecycle
34
+ parser = argparse.ArgumentParser(
35
+ description='show persistent city conversations and operational diagnostics')
36
+ group = parser.add_mutually_exclusive_group()
37
+ group.add_argument(
38
+ '--activity', action='store_true', help='only visible conversation/work events')
39
+ group.add_argument(
40
+ '--diagnostics', action='store_true', help='only operational diagnostics')
41
+ parser.add_argument('-f', '--follow', action='store_true', help='keep following new events')
42
+ parser.add_argument(
43
+ '-n', '--lines', type=int, default=100, help='initial records (default: 100)')
44
+ parser.add_argument('--json', action='store_true', help='emit JSONL unchanged')
45
+ args = parser.parse_args()
46
+
47
+ data = city_env.datos()
48
+ if not data:
49
+ print('agents-city: no selected city', file=sys.stderr)
50
+ return 2
51
+ directory = runtime_processes.ruta(data)
52
+ selected = []
53
+ if not args.diagnostics:
54
+ selected.append(os.path.join(directory, 'activity.jsonl'))
55
+ if not args.activity:
56
+ selected.append(os.path.join(directory, 'diagnostics.jsonl'))
57
+
58
+ rows = []
59
+ positions = {}
60
+ for path in selected:
61
+ values = read(path)
62
+ rows.extend(values)
63
+ try:
64
+ positions[path] = os.path.getsize(path)
65
+ except OSError:
66
+ positions[path] = 0
67
+ rows.sort(key=lambda row: str(row.get('at', '')))
68
+ for row in rows[-max(0, args.lines):]:
69
+ display(row, args.json)
70
+ if not args.follow:
71
+ return 0
72
+
73
+ try:
74
+ while True:
75
+ for path in selected:
76
+ try:
77
+ size = os.path.getsize(path)
78
+ if size < positions[path]:
79
+ positions[path] = 0
80
+ if size == positions[path]:
81
+ continue
82
+ with open(path, encoding='utf-8') as stream:
83
+ stream.seek(positions[path])
84
+ for line in stream:
85
+ try:
86
+ display(json.loads(line), args.json)
87
+ except json.JSONDecodeError:
88
+ pass
89
+ positions[path] = stream.tell()
90
+ except OSError:
91
+ continue
92
+ time.sleep(.2)
93
+ except KeyboardInterrupt:
94
+ return 0
95
+
96
+
97
+ if __name__ == '__main__':
98
+ sys.exit(main())
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python3
2
+ """Road pairing: how an unknown city earns the right to write to a seat.
3
+
4
+ Exchanging invitation files works, but it is friction, and friction is why
5
+ nobody connects two cities. Pairing is the low-friction path: an unknown seat's
6
+ first message yields a short code the owner approves once. Approving grants one
7
+ thing only — permission for that address to send to this seat — never road
8
+ membership, never chair authority, never anything the code's holder did not
9
+ already have.
10
+
11
+ The safety comes from four constants and two rules. A code is short and
12
+ unambiguous (no 0/O/1/I) so it can be read aloud; it expires; only a bounded
13
+ number can be pending per city, so a flood cannot mint an unbounded backlog;
14
+ and the human code is never echoed by tooling — approvals happen by an opaque
15
+ id derived from it. The two rules: a pending request is created (and its code
16
+ revealed) at most once per sender per window, and an approval is only ever
17
+ consulted under the pairing policy, never to widen an allowlist.
18
+
19
+ State is one JSON file per city under its runtime dir. No network, no daemon:
20
+ the seat reads and writes it, the tests drive it on any platform.
21
+ """
22
+
23
+ import hashlib
24
+ import json
25
+ import os
26
+ import secrets
27
+ import time
28
+
29
+ CODIGO_LARGO = 8
30
+ CODIGO_ALFABETO = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' # no 0/O/1/I, read-aloud safe
31
+ PENDIENTE_TTL_MS = 60 * 60 * 1000 # a code is good for one hour
32
+ PENDIENTE_MAX = 3 # at most this many pending per city
33
+ CONCEDIDO_MAX = 500 # a generous cap on approved senders
34
+
35
+
36
+ def _ahora_ms():
37
+ return int(time.time() * 1000)
38
+
39
+
40
+ def _estado(datos):
41
+ real = os.path.realpath(os.path.expanduser(datos))
42
+ ruta = os.path.join(real, '.runtime')
43
+ os.makedirs(ruta, mode=0o700, exist_ok=True)
44
+ return os.path.join(ruta, 'pairing.json')
45
+
46
+
47
+ def _lee(datos):
48
+ try:
49
+ with open(_estado(datos), encoding='utf-8') as f:
50
+ objeto = json.load(f)
51
+ except (OSError, json.JSONDecodeError):
52
+ objeto = {}
53
+ objeto.setdefault('pending', {})
54
+ objeto.setdefault('granted', {})
55
+ return objeto
56
+
57
+
58
+ def _escribe(datos, objeto):
59
+ ruta = _estado(datos)
60
+ tmp = f'{ruta}.tmp'
61
+ with open(tmp, 'w', encoding='utf-8') as f:
62
+ json.dump(objeto, f, indent=2, sort_keys=True)
63
+ os.chmod(tmp, 0o600)
64
+ os.replace(tmp, ruta)
65
+
66
+
67
+ def _codigo():
68
+ return ''.join(secrets.choice(CODIGO_ALFABETO) for _ in range(CODIGO_LARGO))
69
+
70
+
71
+ def id_solicitud(direccion, codigo):
72
+ """The opaque id a tool uses to approve, so the human code never travels."""
73
+ crudo = f'{direccion}\0{codigo}'.encode()
74
+ return 'pr_' + hashlib.sha256(crudo).hexdigest()[:20]
75
+
76
+
77
+ def _purga(objeto, ahora):
78
+ objeto['pending'] = {
79
+ d: p for d, p in objeto['pending'].items()
80
+ if ahora - int(p.get('ts', 0)) < PENDIENTE_TTL_MS
81
+ }
82
+
83
+
84
+ def concedido(datos, direccion):
85
+ """True when this seat address has already been approved to write here."""
86
+ objeto = _lee(datos)
87
+ return direccion in objeto['granted']
88
+
89
+
90
+ def solicita(datos, direccion):
91
+ """Record a pending pairing for `direccion`, returning the code exactly once.
92
+
93
+ Returns `{"code", "id", "created": True}` the first time within the TTL, and
94
+ `{"id", "created": False}` on any repeat — the code is revealed once, so
95
+ re-messaging cannot re-spam it, and a flood cannot exceed the pending cap.
96
+ """
97
+ objeto = _lee(datos)
98
+ if direccion in objeto['granted']:
99
+ return {'created': False, 'already': True}
100
+ ahora = _ahora_ms()
101
+ _purga(objeto, ahora)
102
+ existente = objeto['pending'].get(direccion)
103
+ if existente:
104
+ return {'created': False, 'id': existente['id']}
105
+ if len(objeto['pending']) >= PENDIENTE_MAX:
106
+ return {'created': False, 'full': True}
107
+ codigo = _codigo()
108
+ entrada = {'code': codigo, 'id': id_solicitud(direccion, codigo), 'ts': ahora}
109
+ objeto['pending'][direccion] = entrada
110
+ _escribe(datos, objeto)
111
+ return {'created': True, 'code': codigo, 'id': entrada['id']}
112
+
113
+
114
+ def _concede(datos, seleccion):
115
+ """Grant one pending request in a single read/write, given a picker.
116
+
117
+ `seleccion(pending) -> direccion|None` is the only thing that differs
118
+ between approving by code and approving by opaque id, so both entry points
119
+ share the read, purge, cap check, grant and write exactly once.
120
+ """
121
+ objeto = _lee(datos)
122
+ ahora = _ahora_ms()
123
+ _purga(objeto, ahora)
124
+ encontrado = seleccion(objeto['pending'])
125
+ if not encontrado:
126
+ _escribe(datos, objeto)
127
+ return None
128
+ if len(objeto['granted']) >= CONCEDIDO_MAX:
129
+ raise ValueError('the pairing grant table is full; revoke unused senders first')
130
+ objeto['granted'][encontrado] = {'ts': ahora}
131
+ del objeto['pending'][encontrado]
132
+ _escribe(datos, objeto)
133
+ return encontrado
134
+
135
+
136
+ def _por_campo(campo, objetivo):
137
+ """A picker matching one pending field in constant time — no timing signal.
138
+
139
+ The target is compared as UTF-8 bytes so a non-ASCII or non-string input
140
+ (e.g. a client-supplied opaque id) yields a clean no-match instead of the
141
+ TypeError `compare_digest` raises on non-ASCII strings.
142
+ """
143
+ objetivo_b = str(objetivo or '').encode('utf-8')
144
+
145
+ def elige(pending):
146
+ hallado = None
147
+ for direccion, p in pending.items():
148
+ if secrets.compare_digest(str(p.get(campo, '')).encode('utf-8'), objetivo_b):
149
+ hallado = direccion
150
+ return hallado
151
+ return elige
152
+
153
+
154
+ def aprueba(datos, codigo):
155
+ """Approve a pending request by its human code. Returns the address or None.
156
+
157
+ A non-string or empty code matches nothing and returns None — the
158
+ documented 'a wrong code approves nothing', never a crash.
159
+ """
160
+ return _concede(datos, _por_campo('code', str(codigo or '').strip().upper()))
161
+
162
+
163
+ def aprueba_por_id(datos, id_):
164
+ """Approve by opaque id (what tooling holds), never echoing the code."""
165
+ return _concede(datos, _por_campo('id', id_))
166
+
167
+
168
+ def revoca(datos, direccion):
169
+ """Withdraw a previously granted sender. Returns True if one was removed."""
170
+ objeto = _lee(datos)
171
+ quitado = objeto['granted'].pop(direccion, None) is not None
172
+ objeto['pending'].pop(direccion, None)
173
+ _escribe(datos, objeto)
174
+ return quitado
175
+
176
+
177
+ def pendientes(datos):
178
+ """Opaque view of pending requests: id + age, never the code."""
179
+ objeto = _lee(datos)
180
+ ahora = _ahora_ms()
181
+ _purga(objeto, ahora)
182
+ return [
183
+ {'address': d, 'id': p['id'], 'ageMinutes': (ahora - int(p['ts'])) // 60000}
184
+ for d, p in sorted(objeto['pending'].items())
185
+ ]
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """The one reader of `parcels.yml`.
3
+
4
+ There were two, and they were not the same reader. Both walked the format with the
5
+ same byte-identical regex, and then they disagreed: the seeder handled the `lab:`
6
+ section and complained about lines it could not parse; the reporter did neither,
7
+ and read the folder out of a key called `path` that this format has never had. So
8
+ the map and the growth reporter had different ideas about the same file, and the
9
+ reporter's idea was wrong in a way that stopped it working at all.
10
+
11
+ Two readers of one format is a bug waiting for someone to edit only one of them.
12
+
13
+ It lives here, in `plugin/scripts/`, and not next to the seeder, because this
14
+ directory is what gets installed on everybody's machine — the plugin ships without
15
+ `city/`, so the plugin cannot import from there. The seeder runs from a clone and
16
+ can import from here, so this is the only direction that works.
17
+
18
+ Deliberately not YAML: the format is a line at a time on purpose, so this runs
19
+ anywhere with no dependency to install.
20
+
21
+ repos:
22
+ main-site:
23
+ - ruta: "content/**/deposits/**" ; unidad: banking ; nombre: "main · deposits"
24
+ - ruta: "" ; unidad: none ; nombre: "main · platform"
25
+ lab: []
26
+ """
27
+ import re
28
+
29
+ # A parcel line. Values may be quoted, fields are separated by `;`, and a missing
30
+ # field is a default rather than an error.
31
+ CAMPO = re.compile(r'(\w+):\s*"?([^";\n]*?)"?\s*(?:;|$)')
32
+ CABECERA = re.compile(r'^[a-zA-Z0-9_.\-]+:\s*$') # `repos:` / `lab:`
33
+ REPO = re.compile(r'^ [a-zA-Z0-9_.\-]+:\s*$') # ` main-site:`
34
+
35
+
36
+ def campos(linea):
37
+ """`- ruta: "x" ; unidad: y ; nombre: "z"` -> {'ruta': 'x', …}"""
38
+ return dict(CAMPO.findall(linea.strip().lstrip('- ')))
39
+
40
+
41
+ def identidad(repo, ruta):
42
+ """A parcel's id, which has to be computed the same way everywhere.
43
+
44
+ The reporter pushes growth keyed on this and the map stores rows keyed on this.
45
+ If the two ever disagree, growth lands on nothing and neither side says so.
46
+ """
47
+ return f'{repo}:{ruta}' if ruta else repo
48
+
49
+
50
+ def lee(ruta_fichero):
51
+ """Walk the file once. Returns (parcelas, lab, raras).
52
+
53
+ parcelas: [{'id', 'repo', 'ruta', 'unidad', 'nombre'}] in file order
54
+ lab: {repo, …} listed under `lab:`
55
+ raras: lines under a repo that are not parcels — the caller decides whether
56
+ to complain, because a silent one is how a repo ends up in the wrong
57
+ district for a reason nobody can find
58
+ """
59
+ try:
60
+ with open(ruta_fichero, encoding='utf-8') as f:
61
+ txt = f.read()
62
+ except (FileNotFoundError, IsADirectoryError):
63
+ return [], set(), []
64
+
65
+ fuera, lab, raras = [], set(), []
66
+ repo, en_lab = None, False
67
+ for linea in txt.splitlines():
68
+ if re.match(r'^lab:\s*$', linea):
69
+ en_lab, repo = True, None
70
+ continue
71
+ if CABECERA.match(linea):
72
+ en_lab = False
73
+ continue
74
+ if REPO.match(linea):
75
+ repo = linea.strip().rstrip(':')
76
+ elif linea.strip().startswith('- ruta:') and repo:
77
+ c = campos(linea)
78
+ r = c.get('ruta', '')
79
+ fuera.append({'id': identidad(repo, r), 'repo': repo, 'ruta': r,
80
+ 'unidad': c.get('unidad', 'none'),
81
+ 'nombre': c.get('nombre', repo)})
82
+ elif linea.strip().startswith('- '):
83
+ suelto = linea.strip().lstrip('- ').strip()
84
+ if en_lab:
85
+ if suelto:
86
+ lab.add(suelto)
87
+ elif repo:
88
+ raras.append(linea.strip()[:70])
89
+ return fuera, lab, raras
90
+
91
+
92
+ def escribe(ruta_fichero, porRepo, lab=()):
93
+ """Write the whole file back, in the shape `lee` reads.
94
+
95
+ `porRepo` is {repo: [{'ruta', 'unidad', 'nombre'}, …]}. This exists so the
96
+ modelling — the one thing no tool can do for you — can be edited with a form
97
+ instead of by hand in a format nobody remembers. Round-tripping through `lee`
98
+ is pinned by a test, because a writer that drifts from its reader is exactly
99
+ the bug this module was created to end.
100
+ """
101
+ lineas = ['# Which slice of which repo serves which unit.',
102
+ '#',
103
+ '# A house is not a repo: it is a **parcel**, a slice of a repo serving one',
104
+ '# unit. One repo can hold several, and that is the central fact of most',
105
+ '# codebases.',
106
+ '',
107
+ 'repos:']
108
+ for repo in sorted(porRepo):
109
+ filas = porRepo[repo] or [{'ruta': '', 'unidad': 'none', 'nombre': repo}]
110
+ lineas.append(f' {repo}:')
111
+ for f in filas:
112
+ ruta = str(f.get('ruta', '')).replace(';', '').replace('"', '')
113
+ unidad = str(f.get('unidad', 'none')).replace(';', '') or 'none'
114
+ nombre = str(f.get('nombre') or repo).replace(';', ',').replace('"', "'")
115
+ lineas.append(f' - ruta: "{ruta}" ; unidad: {unidad} ; nombre: "{nombre}"')
116
+ lineas += ['', '# Research that does not ship yet gets its own district instead of',
117
+ '# reading as waste among the rest.']
118
+ lineas.append('lab:' if lab else 'lab: []')
119
+ for r in sorted(lab):
120
+ lineas.append(f' - {r}')
121
+ with open(ruta_fichero, 'w', encoding='utf-8') as f:
122
+ f.write('\n'.join(lineas) + '\n')
123
+
124
+
125
+ def aviso(raras):
126
+ """What to say about lines that could not be read, or '' if there were none."""
127
+ if not raras:
128
+ return ''
129
+ lineas = [f'parcels.yml: {len(raras)} line(s) I could not read — they are being '
130
+ f'ignored. Use: - ruta: "<glob>" ; unidad: <unit> ; nombre: "<name>"']
131
+ lineas += [f' {r}' for r in raras[:5]]
132
+ return '\n'.join(lineas)
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """Read one field out of a card's frontmatter, for the shell scripts.
3
+
4
+ read-card.py <card.md> agent
5
+ read-card.py <card.md> repos -> comma-separated list
6
+ read-card.py --window <repo> -> canonical tmux/bus actor slug
7
+ read-card.py --actor-role <card.md> <actor> -> safe operating role or blank
8
+
9
+ A thin front on card.py, which is where the parsing lives. It used to hold its own
10
+ copy, and that copy is why the tmux session asked for `agente` — a field no card in
11
+ this repo has ever carried — and got nothing every single time.
12
+
13
+ `agente` still answers, for a card somebody wrote by hand.
14
+ """
15
+ import os
16
+ import sys
17
+
18
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
+ import card # noqa: E402
20
+
21
+
22
+ def main():
23
+ if len(sys.argv) == 3 and sys.argv[1] == '--window':
24
+ print(card.ventana(sys.argv[2]))
25
+ return
26
+ if len(sys.argv) == 4 and sys.argv[1] == '--actor-role':
27
+ try:
28
+ texto = open(sys.argv[2], encoding='utf-8').read()
29
+ except OSError as e:
30
+ sys.exit(f'cannot read {sys.argv[2]}: {e}')
31
+ actor = card.ventana(sys.argv[3])
32
+ print(card.rol_seguro(card.campo(texto, f'role.{actor}')))
33
+ return
34
+ if len(sys.argv) < 3:
35
+ sys.exit(
36
+ 'usage: read-card.py <card.md> <field> | --window <repo> | '
37
+ '--actor-role <card.md> <actor>'
38
+ )
39
+ ruta, campo = sys.argv[1], sys.argv[2]
40
+ try:
41
+ with open(ruta, encoding='utf-8') as f:
42
+ texto = f.read()
43
+ except OSError as e:
44
+ sys.exit(f'cannot read {ruta}: {e}')
45
+ valor = card.campo(texto, campo)
46
+ # A bracketed value is a list everywhere else in this product — `repos:`,
47
+ # `agents:`, `mounts.<agent>:` — and the shell wants one line. Deciding by
48
+ # SHAPE rather than by a list of blessed field names is what stopped this
49
+ # from having to be edited every time the card grew another list.
50
+ esLista = valor.strip().startswith('[') and valor.strip().endswith(']')
51
+ print(','.join(card.lista(valor)) if esLista else valor)
52
+
53
+
54
+ if __name__ == '__main__':
55
+ main()