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,111 @@
1
+ #!/usr/bin/env bash
2
+ # Index the git repos already cloned on this machine, wherever they are, and
3
+ # cache it. One line each: <name-on-the-remote><TAB><local-path>
4
+ #
5
+ # find-repos.sh uses the cache while it is less than a day old
6
+ # find-repos.sh --refresh rebuilds it
7
+ # find-repos.sh <repo> prints that repo's path, or nothing
8
+ #
9
+ # The name comes from the `origin` remote and not from the folder name, so it
10
+ # finds the repo even when the folder is called something else — which is the
11
+ # normal case for anybody with more than ten of them.
12
+ #
13
+ # AGENTS_CITY_ORG only index repos of this organisation. Unset = all of
14
+ # them; the filter is for machines that also hold work
15
+ # from somewhere else.
16
+ # CITY_SEARCH_IN colon-separated roots to search instead of the defaults
17
+ # CITY_SEARCH_DEPTH how deep to go under each root (default 4)
18
+
19
+ set -uo pipefail
20
+
21
+ ORG="${AGENTS_CITY_ORG:-}"
22
+ CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/agents-city"
23
+ CACHE="$CACHE_DIR/repos.tsv"
24
+ RAICES_DEF="$HOME/codigo:$HOME/code:$HOME/dev:$HOME/src:$HOME/projects:$HOME/proyectos:$HOME/work:$HOME/trabajo:$HOME/repos:$HOME/git:$HOME/Documents:$HOME/Desktop:$HOME/Developer:$HOME"
25
+ RAICES="${CITY_SEARCH_IN:-$RAICES_DEF}"
26
+ PROFUNDIDAD="${CITY_SEARCH_DEPTH:-4}"
27
+
28
+ construir() {
29
+ mkdir -p "$CACHE_DIR"
30
+ local tmp="$CACHE.tmp.$$"
31
+ : > "$tmp"
32
+ local vistos=""
33
+ local IFS=':'
34
+ for raiz in $RAICES; do
35
+ unset IFS
36
+ [ -d "$raiz" ] || continue
37
+ # Do not walk a root another one already covered.
38
+ case ":$vistos:" in *":$raiz:"*) continue ;; esac
39
+ vistos="$vistos:$raiz"
40
+
41
+ # `.git` is a directory in a normal clone and a FILE in a linked worktree —
42
+ # and a worktree is exactly the folder an isolated agent works in, so missing
43
+ # them missed the people using agents the most. Both are indexed; a worktree
44
+ # is named `repo@branch` so the two are distinct things to pick.
45
+ find "$raiz" -maxdepth "$PROFUNDIDAD" \( -type d -o -type f \) -name .git \
46
+ -not -path "*/node_modules/*" \
47
+ -not -path "*/Library/*" \
48
+ -not -path "*/.Trash/*" \
49
+ -not -path "*/vendor/*" \
50
+ -not -path "*/.cache/*" \
51
+ -not -path "*/plugins/cache/*" \
52
+ -not -path "*/.cargo/*" -not -path "*/.asdf/*" -not -path "*/miniconda3/*" \
53
+ -not -path "*/.rbenv/*" -not -path "*/.pyenv/*" -not -path "*/.nvm/*" \
54
+ 2>/dev/null \
55
+ | while read -r g; do
56
+ repo="$(dirname "$g")"
57
+ url="$(git -C "$repo" remote get-url origin 2>/dev/null)" || continue
58
+ [ -z "$url" ] && continue
59
+ # git@host:org/name.git | https://host/org/name(.git)
60
+ # The version excluded by an organisation whose default was the literal
61
+ # "<your-org>", so the index came back empty for everybody who never set
62
+ # the variable. Unset now means: index what is there.
63
+ if [ -n "$ORG" ]; then
64
+ case "$url" in
65
+ *[:/]"$ORG"/*) ;;
66
+ *) continue ;;
67
+ esac
68
+ fi
69
+ nombre="$(basename "$url" .git)"
70
+ if [ -f "$g" ]; then
71
+ rama="$(git -C "$repo" branch --show-current 2>/dev/null)"
72
+ [ -z "$rama" ] && rama="$(basename "$repo")"
73
+ nombre="$nombre@$rama"
74
+ fi
75
+ [ -n "$nombre" ] && printf '%s\t%s\n' "$nombre" "$repo"
76
+ done >> "$tmp"
77
+ IFS=':'
78
+ done
79
+ unset IFS
80
+ # First one wins on a duplicate: the roots run from most specific to least.
81
+ sort -u -t$'\t' -k1,1 "$tmp" > "$CACHE"
82
+ rm -f "$tmp"
83
+ }
84
+
85
+ caduca() {
86
+ [ ! -f "$CACHE" ] && return 0
87
+ [ -n "$(find "$CACHE" -mtime +1 2>/dev/null)" ] && return 0 # older than a day
88
+ return 1
89
+ }
90
+
91
+ case "${1:-}" in
92
+ --refresh|--refrescar)
93
+ echo "Looking for cloned repos…" >&2
94
+ construir
95
+ echo "$(wc -l < "$CACHE" | tr -d ' ') repos indexed in $CACHE" >&2
96
+ ;;
97
+ "")
98
+ caduca && construir
99
+ cat "$CACHE"
100
+ ;;
101
+ *)
102
+ caduca && construir
103
+ ruta="$(awk -F'\t' -v n="$1" '$1==n {print $2; exit}' "$CACHE")"
104
+ # Not there? It may have been cloned a minute ago: rebuild once and retry.
105
+ if [ -z "$ruta" ]; then
106
+ construir
107
+ ruta="$(awk -F'\t' -v n="$1" '$1==n {print $2; exit}' "$CACHE")"
108
+ fi
109
+ [ -n "$ruta" ] && printf '%s\n' "$ruta"
110
+ ;;
111
+ esac
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """Who and what, according to GitHub and according to git.
3
+
4
+ Used by both doors — `./bin/seat` and the wizard — so it sits in the folder that
5
+ ships. Everything here is optional: nothing in this project requires an account, and
6
+ every function returns empty rather than raising when `gh` is missing or logged out.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import re
12
+ import shutil
13
+ import subprocess
14
+ import unicodedata
15
+ from collections import Counter
16
+
17
+
18
+ def sh(args, timeout=180):
19
+ """Run something and take its stdout, or '' if it failed in any way."""
20
+ try:
21
+ r = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
22
+ return r.stdout if r.returncode == 0 else ""
23
+ except (OSError, subprocess.TimeoutExpired):
24
+ return ""
25
+
26
+
27
+ def hay(programa):
28
+ return bool(shutil.which(programa))
29
+
30
+
31
+ def conectado():
32
+ """`gh` present *and* logged in. Both, because an installed-but-anonymous gh
33
+ fails on the first list with a message about scopes that reads like a bug."""
34
+ if not hay("gh"):
35
+ return False
36
+ try:
37
+ r = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True, timeout=20)
38
+ return r.returncode == 0
39
+ except (OSError, subprocess.TimeoutExpired):
40
+ return False
41
+
42
+
43
+ def autentica_web():
44
+ """Run GitHub CLI's browser/device OAuth conversation in the foreground."""
45
+ if not hay("gh"):
46
+ return False
47
+ try:
48
+ r = subprocess.run(["gh", "auth", "login", "--web", "--git-protocol", "https"])
49
+ return r.returncode == 0 and conectado()
50
+ except OSError:
51
+ return False
52
+
53
+
54
+ def login():
55
+ """Your GitHub login, which is *not* the same string as your card's user.
56
+
57
+ A card's user comes from a git email's local part. Mine is `joseluiscases`; my
58
+ login is `jlcases`. Anything asking GitHub `author:<who>` needs this one — the
59
+ other gives a 422 that the caller swallows, so the number silently never arrives.
60
+ """
61
+ return sh(["gh", "api", "user", "--jq", ".login"]).strip()
62
+
63
+
64
+ def orgs():
65
+ out = sh(["gh", "api", "user/orgs", "--jq", ".[].login"])
66
+ return [l.strip() for l in out.splitlines() if l.strip()]
67
+
68
+
69
+ def repos(dueno):
70
+ """A user's or an organisation's repos: (name, description, last pushed)."""
71
+ out = sh(
72
+ [
73
+ "gh",
74
+ "repo",
75
+ "list",
76
+ dueno,
77
+ "--limit",
78
+ "400",
79
+ "--no-archived",
80
+ "--json",
81
+ "name,pushedAt,description",
82
+ ]
83
+ )
84
+ try:
85
+ return [
86
+ (r["name"], (r.get("description") or "")[:60], r.get("pushedAt", "")[:10])
87
+ for r in json.loads(out or "[]")
88
+ ]
89
+ except json.JSONDecodeError:
90
+ return []
91
+
92
+
93
+ def miembros(org):
94
+ out = sh(["gh", "api", f"orgs/{org}/members", "--paginate", "--jq", ".[].login"])
95
+ return [l.strip() for l in out.splitlines() if l.strip()]
96
+
97
+
98
+ # ── identity, from a git email ─────────────────────────────────────────────
99
+ def usuario_de_correo(correo):
100
+ """The username inside an email. One rule, shared by every door.
101
+
102
+ A `+` means one of two opposite things and the digits tell you which:
103
+
104
+ 12345678+alice@users.noreply.github.com -> alice
105
+ GitHub's private-email setting. The number is the account id, so the name
106
+ is after the plus. Taking the whole thing gives a card called
107
+ `12345678+alice.md` and an agent name nobody can type.
108
+
109
+ alice+work@example.com -> alice
110
+ Ordinary plus-addressing. Here the account is before the plus.
111
+
112
+ Then down to something safe as a filename and as a name on the bus, but
113
+ transliterated rather than stripped: `josé` should be `jose`, not `jos`.
114
+ """
115
+ u = correo.split("@")[0].strip().lower()
116
+ if "+" in u:
117
+ izq, der = u.split("+", 1)
118
+ u = der if izq.isdigit() else izq
119
+ # NFKD splits an accented letter into letter + mark, and dropping the marks is
120
+ # what turns Máx into max instead of Mx.
121
+ u = unicodedata.normalize("NFKD", u).encode("ascii", "ignore").decode()
122
+ return re.sub(r"[^a-z0-9._-]", "", u)
123
+
124
+
125
+ def es_maquina(usuario, nombre):
126
+ """Whether this author is a bot. `[bot]` is the GitHub App convention, but plenty
127
+ of CI identities just call themselves some flavour of bot, and a card for one of
128
+ those is a card nobody can ask anything."""
129
+ return any(
130
+ s in f"{usuario} {nombre}".lower()
131
+ for s in ("[bot]", "dependabot", "renovate", "github-actions", "noreply")
132
+ )
133
+
134
+
135
+ def gente_de_repos(rutas):
136
+ """Whoever has commits in these folders. Works with no account at all.
137
+
138
+ Grouped by username alone, not by (username, name). One person writes their own
139
+ name three ways across a decade of commits — "Max Carrion", "Máx Carrión",
140
+ "maxc" — and counting those separately offered the same person three times and
141
+ then wrote their card three times over the top of itself. The name shown is
142
+ whichever spelling they used most.
143
+ """
144
+ porUsuario = {}
145
+ for r in rutas:
146
+ if not os.path.isdir(r):
147
+ continue
148
+ for l in sh(["git", "-C", r, "log", "--since=1.year", "--format=%ae|%an"]).splitlines():
149
+ if "|" not in l:
150
+ continue
151
+ correo, nombre = l.split("|", 1)
152
+ u = usuario_de_correo(correo)
153
+ if u and not es_maquina(u, nombre):
154
+ porUsuario.setdefault(u, Counter())[nombre.strip()] += 1
155
+ fuera = [
156
+ (u, nombres.most_common(1)[0][0], sum(nombres.values()))
157
+ for u, nombres in porUsuario.items()
158
+ ]
159
+ return sorted(fuera, key=lambda x: -x[2])[:40]
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """The Hall protocol: three frames, a per-connection sequence, no replay.
3
+
4
+ The Hall is the local web page that watches the cities and writes to the seat.
5
+ It talks to the bus the page is served from — same origin, one socket — and
6
+ this module is the transport-free contract both ends share: the frame shapes,
7
+ the monotonic per-connection sequence, and the one rule that removes a whole
8
+ class of bugs — events are never replayed, so on a sequence gap the client
9
+ re-fetches a snapshot instead of the server keeping a durable per-client log.
10
+
11
+ Writing to the seat is one method with a queue mode, not several: the same
12
+ call reaches an idle seat (start a turn) or a busy one (steer into the running
13
+ turn, or queue, or just leave a note), and the bus decides which. Loopback is
14
+ a convenience for pairing, never a substitute for the seat's own admission.
15
+
16
+ Pure Python so the Hall's server and its tests validate frames identically;
17
+ the browser side reimplements the same shapes in TypeScript when it is built.
18
+ """
19
+
20
+ import json
21
+ from enum import Enum
22
+
23
+ PROTOCOL = 'agents-city-hall/1'
24
+
25
+
26
+ class Tipo(str, Enum):
27
+ REQ = 'req' # client -> bus: a method call
28
+ RES = 'res' # bus -> client: the paired reply
29
+ EVENT = 'event' # bus -> client: an unsolicited push, carrying seq
30
+
31
+ def __str__(self):
32
+ return self.value
33
+
34
+
35
+ class Modo(str, Enum):
36
+ """How a write to the seat meets the seat's current turn."""
37
+ INICIA = 'start' # idle seat: begin a new turn
38
+ DIRIGE = 'steer' # busy seat: inject into the running turn
39
+ ENCOLA = 'queue' # busy seat: run after the current turn
40
+ NOTA = 'note' # write to the transcript, do not wake the seat
41
+
42
+ def __str__(self):
43
+ return self.value
44
+
45
+
46
+ #: Closed method set. A method outside it is refused at the edge, never routed.
47
+ METODOS = frozenset({
48
+ 'hall.hello', # handshake: negotiate protocol + declare read scope
49
+ 'cities.snapshot', # subscribe-and-snapshot in one round trip
50
+ 'seat.write', # write to the seat (carries a Modo)
51
+ 'seat.history', # re-fetch a seat transcript after a gap
52
+ 'pairing.pending', # list opaque pending road pairings
53
+ 'pairing.approve', # approve one by opaque id
54
+ })
55
+
56
+ #: Structured error codes the client branches on (never prose-matching).
57
+ ERRORES = frozenset({
58
+ 'unknown_method', 'bad_frame', 'unauthorized', 'seat_busy',
59
+ 'not_addressed', 'rate_limited', 'gap_detected',
60
+ })
61
+
62
+ MAX_TEXTO = 64_000
63
+
64
+
65
+ def error(codigo, mensaje, reintentable=False):
66
+ if codigo not in ERRORES:
67
+ raise ValueError(f'unknown error code: {codigo}')
68
+ return {'code': codigo, 'message': mensaje, 'retryable': bool(reintentable)}
69
+
70
+
71
+ def valida_req(frame):
72
+ """Validate an inbound request frame. Returns (ok, problema|None).
73
+
74
+ Fail-closed: an unknown method, a missing id, a non-object params, or a
75
+ steer/queue write without text is refused here rather than deeper in.
76
+ """
77
+ if not isinstance(frame, dict):
78
+ return False, error('bad_frame', 'frame must be an object')
79
+ if frame.get('type') != Tipo.REQ.value:
80
+ return False, error('bad_frame', 'not a req frame')
81
+ if not isinstance(frame.get('id'), str) or not frame['id']:
82
+ return False, error('bad_frame', 'req needs a string id')
83
+ metodo = frame.get('method')
84
+ if metodo not in METODOS:
85
+ return False, error('unknown_method', f'no such method: {metodo}')
86
+ params = frame.get('params', {})
87
+ if not isinstance(params, dict):
88
+ return False, error('bad_frame', 'params must be an object')
89
+ if metodo == 'seat.write':
90
+ modo = params.get('mode', Modo.INICIA.value)
91
+ if modo not in {m.value for m in Modo}:
92
+ return False, error('bad_frame', f'unknown write mode: {modo}')
93
+ texto = params.get('text', '')
94
+ # text must be an actual string — a dict/list/number silently stringified
95
+ # would be admitted and forwarded as 'None' or "{'x': 1}".
96
+ if not isinstance(texto, str):
97
+ return False, error('bad_frame', 'text must be a string')
98
+ if modo != Modo.NOTA.value and not texto.strip():
99
+ return False, error('bad_frame', 'a seat write needs non-empty text')
100
+ if len(texto) > MAX_TEXTO:
101
+ return False, error('bad_frame', f'text exceeds {MAX_TEXTO} characters')
102
+ return True, None
103
+
104
+
105
+ def respuesta(id_, payload=None, err=None):
106
+ """Build a res frame paired to a req id. Exactly one of payload/err."""
107
+ if (payload is None) == (err is None):
108
+ raise ValueError('a response carries exactly one of payload or error')
109
+ frame = {'type': Tipo.RES.value, 'id': id_, 'ok': err is None}
110
+ if err is None:
111
+ frame['payload'] = payload
112
+ else:
113
+ frame['error'] = err
114
+ return frame
115
+
116
+
117
+ class Secuencia:
118
+ """A per-connection monotonic sequence stamped on every event.
119
+
120
+ The client watches for gaps: a jump means frames were dropped (a slow
121
+ consumer is served by dropping, not by unbounded buffering), and the client
122
+ recovers by re-fetching a snapshot — the server keeps no durable replay log.
123
+ """
124
+
125
+ def __init__(self):
126
+ self._n = 0
127
+
128
+ def evento(self, nombre, payload):
129
+ self._n += 1
130
+ return {'type': Tipo.EVENT.value, 'event': nombre, 'seq': self._n, 'payload': payload}
131
+
132
+ @staticmethod
133
+ def hay_hueco(ultimo_visto, seq):
134
+ """True when `seq` is not exactly one past `ultimo_visto` — a gap."""
135
+ return seq != ultimo_visto + 1
136
+
137
+
138
+ def codifica(frame):
139
+ return json.dumps(frame, separators=(',', ':'))
140
+
141
+
142
+ def descodifica(crudo):
143
+ """Parse one wire frame, or (None, problema) on malformed input."""
144
+ try:
145
+ return json.loads(crudo), None
146
+ except (json.JSONDecodeError, TypeError):
147
+ return None, error('bad_frame', 'frame is not valid json')
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """Mirror Claude's visible prompts and final answers onto the city WebSocket."""
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+
9
+ import runtime_log
10
+ import runtime_processes
11
+
12
+
13
+ def text(value, limit=4000):
14
+ return str(value or '').strip()[:limit]
15
+
16
+
17
+ def source_id(event, payload, content):
18
+ session = text(payload.get('session_id'), 100) or 'session'
19
+ transcript = text(payload.get('transcript_path'), 1000)
20
+ try:
21
+ position = os.path.getsize(transcript) if transcript else 0
22
+ except OSError:
23
+ position = 0
24
+ digest = hashlib.sha256(content.encode('utf-8')).hexdigest()[:20]
25
+ return f'claude:{session}:{event}:{position}:{digest}'
26
+
27
+
28
+ def marker_path(data, actor):
29
+ safe = ''.join(c if c.isalnum() or c in '_-' else '-' for c in actor)[:80] or 'seat'
30
+ return os.path.join(runtime_processes.ruta(data), 'claude-threads', safe + '.json')
31
+
32
+
33
+ def marker(data, actor):
34
+ try:
35
+ with open(marker_path(data, actor), encoding='utf-8') as stream:
36
+ value = json.load(stream)
37
+ return text(value.get('thread'), 160) or None
38
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
39
+ return None
40
+
41
+
42
+ def clear_marker(data, actor):
43
+ try:
44
+ os.unlink(marker_path(data, actor))
45
+ except OSError:
46
+ pass
47
+
48
+
49
+ def internal_city_prompt(content):
50
+ prompt = content.lstrip()
51
+ return (prompt.startswith('[Agents City authenticated local bus]')
52
+ or (prompt.startswith('<channel')
53
+ and 'source="plugin:city:city-bus"' in prompt[:500]))
54
+
55
+
56
+ def activity(event, payload, data='', actor='seat'):
57
+ session = text(payload.get('session_id'), 160) or None
58
+ if event == 'UserPromptSubmit':
59
+ content = text(payload.get('prompt'))
60
+ if not content:
61
+ return None
62
+ if internal_city_prompt(content):
63
+ # The committee state machine already emits a concise assignment.
64
+ # Mirroring the transport wrapper produces the unreadable <channel>
65
+ # wall that the Hall must never expose.
66
+ return None
67
+ clear_marker(data, actor)
68
+ return session, {
69
+ 'sourceId': source_id(event, payload, content),
70
+ 'kind': 'conversation.user', 'phase': 'asked', 'tone': 'question',
71
+ 'title': f'{actor} asked', 'summary': content,
72
+ }
73
+ if event == 'Stop':
74
+ content = text(payload.get('last_assistant_message'))
75
+ if not content:
76
+ return None
77
+ return marker(data, actor) or session, {
78
+ 'sourceId': source_id(event, payload, content),
79
+ 'kind': 'conversation.agent', 'phase': 'answered', 'tone': 'evidence',
80
+ 'title': f'{actor} answered', 'summary': content,
81
+ }
82
+ if event in ('SessionStart', 'SessionEnd'):
83
+ phase = 'started' if event == 'SessionStart' else 'ended'
84
+ return session, {
85
+ 'sourceId': source_id(event, payload, phase),
86
+ 'kind': f'runtime.session.{phase}', 'phase': phase, 'tone': 'system',
87
+ 'title': f'{actor} session {phase}',
88
+ 'summary': f'Claude session {phase}.',
89
+ }
90
+ return None
91
+
92
+
93
+ def main():
94
+ event = sys.argv[1] if len(sys.argv) > 1 else ''
95
+ data = os.environ.get('AGENTS_CITY_DATA', '')
96
+ actor = text(os.environ.get('CITY_BUS_ACTOR'), 80) or 'seat'
97
+ try:
98
+ payload = json.load(sys.stdin)
99
+ except (ValueError, OSError):
100
+ payload = {}
101
+ # The native Claude stream gateway has the exact bus-envelope/thread
102
+ # correlation and publishes the visible answer itself. Mirroring the same
103
+ # turn from lifecycle hooks would show it twice in the Hall. Token/growth
104
+ # hooks remain enabled; only this activity mirror becomes a no-op.
105
+ if os.environ.get('CITY_CLAUDE_STREAM_GATEWAY') == '1':
106
+ print('{}')
107
+ return
108
+ chosen = activity(event, payload, data, actor)
109
+ if chosen and data:
110
+ thread, body = chosen
111
+ client = os.path.realpath(
112
+ os.path.join(os.path.dirname(__file__), '..', 'channel', 'client.js')
113
+ )
114
+ command = ['node', client, 'activity', 'publish']
115
+ if thread:
116
+ command.append(thread)
117
+ command += ['--input', '-']
118
+ try:
119
+ result = subprocess.run(
120
+ command, input=json.dumps(body), capture_output=True, text=True,
121
+ timeout=6, env=dict(os.environ, CITY_BUS_ACTOR=actor))
122
+ if result.returncode:
123
+ runtime_log.append(data, f'claude-hook:{actor}', 'activity.publish.failed',
124
+ actor=actor, outcome='failed', message=result.stderr)
125
+ else:
126
+ runtime_log.append(data, f'claude-hook:{actor}', 'activity.published',
127
+ actor=actor, outcome='ok', message=event)
128
+ except Exception as error:
129
+ runtime_log.append(data, f'claude-hook:{actor}', 'activity.publish.failed',
130
+ actor=actor, outcome='failed', message=error)
131
+ if event in ('Stop', 'SessionEnd') and data:
132
+ clear_marker(data, actor)
133
+ # Claude hooks accept an empty JSON decision. Mirroring is deliberately
134
+ # fail-open so observability can never block the user's model session.
135
+ print('{}')
136
+
137
+
138
+ if __name__ == '__main__':
139
+ main()
@@ -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()