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.
- package/.claude-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2226 -0
- package/README.md +2196 -0
- package/benchmarks/committee/README.md +39 -0
- package/benchmarks/committee/metrics.py +51 -0
- package/benchmarks/committee/run.py +64 -0
- package/benchmarks/committee/traces/01-single.json +8 -0
- package/benchmarks/committee/traces/02-mesh.json +16 -0
- package/benchmarks/committee/traces/03-chair.json +15 -0
- package/benchmarks/latency/README.md +35 -0
- package/benchmarks/latency/fake-claude-cli.mjs +119 -0
- package/benchmarks/latency/fake-native-server.mjs +340 -0
- package/benchmarks/latency/fake-tui.py +91 -0
- package/benchmarks/latency/live.py +380 -0
- package/benchmarks/stress/README.md +20 -0
- package/benchmarks/stress/run.py +487 -0
- package/bin/agents +4 -0
- package/bin/agents-city.js +119 -0
- package/bin/benchmark +17 -0
- package/bin/bus +6 -0
- package/bin/cities +3 -0
- package/bin/city +86 -0
- package/bin/committee +6 -0
- package/bin/demo +200 -0
- package/bin/doctor +3 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +498 -0
- package/bin/logs +3 -0
- package/bin/navegador.mjs +243 -0
- package/bin/report.py +14 -0
- package/bin/reset +3 -0
- package/bin/road +3 -0
- package/bin/seat +10 -0
- package/bin/serve.py +1579 -0
- package/bin/setup.py +753 -0
- package/bin/shortcut +3 -0
- package/bin/skills +3 -0
- package/bin/test +133 -0
- package/bin/test-actualiza.py +130 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-avatar.py +58 -0
- package/bin/test-benchmark.py +40 -0
- package/bin/test-broker.py +334 -0
- package/bin/test-cage.py +374 -0
- package/bin/test-card.py +402 -0
- package/bin/test-channel.py +379 -0
- package/bin/test-cities.py +453 -0
- package/bin/test-claude-runtime.py +252 -0
- package/bin/test-committee.py +340 -0
- package/bin/test-contracts.py +551 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +139 -0
- package/bin/test-domains.py +151 -0
- package/bin/test-evidencia.py +50 -0
- package/bin/test-exit.py +128 -0
- package/bin/test-hall-protocol.py +93 -0
- package/bin/test-launch.py +169 -0
- package/bin/test-live-feed.py +508 -0
- package/bin/test-navegador.py +140 -0
- package/bin/test-pairing.py +99 -0
- package/bin/test-parcels.py +283 -0
- package/bin/test-runtime-failures.py +252 -0
- package/bin/test-runtime-ui.py +488 -0
- package/bin/test-runtime.py +268 -0
- package/bin/test-rutas.py +80 -0
- package/bin/test-seat.py +1598 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1251 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +251 -0
- package/bin/testlib.py +118 -0
- package/bin/tokens.py +14 -0
- package/bin/update +3 -0
- package/bus/scripts/deploy.sh +60 -0
- package/bus/scripts/mint-token.sh +68 -0
- package/bus/scripts/setup-dev.sh +79 -0
- package/bus/scripts/test-channel.ts +8 -0
- package/bus/scripts/test-hub.ts +266 -0
- package/bus/scripts/test-local.sh +40 -0
- package/bus/scripts/test-queue.ts +132 -0
- package/bus/worker/package-lock.json +1971 -0
- package/bus/worker/package.json +17 -0
- package/bus/worker/src/index.ts +485 -0
- package/bus/worker/tsconfig.json +13 -0
- package/bus/worker/wrangler.toml +32 -0
- package/city/oven/README.md +59 -0
- package/city/oven/collect.py +43 -0
- package/city/oven/oven.html +4 -0
- package/city/scripts/history.py +120 -0
- package/city/scripts/seed.py +442 -0
- package/city/web/assets/modelos/License.txt +28 -0
- package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/assets/modelos/building-a.glb +0 -0
- package/city/web/assets/modelos/building-b.glb +0 -0
- package/city/web/assets/modelos/building-c.glb +0 -0
- package/city/web/assets/modelos/building-d.glb +0 -0
- package/city/web/assets/modelos/building-e.glb +0 -0
- package/city/web/assets/modelos/building-f.glb +0 -0
- package/city/web/assets/modelos/building-g.glb +0 -0
- package/city/web/assets/modelos/building-h.glb +0 -0
- package/city/web/assets/sprites/building-a.png +0 -0
- package/city/web/assets/sprites/building-b.png +0 -0
- package/city/web/assets/sprites/building-c.png +0 -0
- package/city/web/assets/sprites/building-d.png +0 -0
- package/city/web/assets/sprites/building-e.png +0 -0
- package/city/web/assets/sprites/building-f.png +0 -0
- package/city/web/assets/sprites/building-g.png +0 -0
- package/city/web/assets/sprites/building-h.png +0 -0
- package/city/web/assets/sprites/building-i.png +0 -0
- package/city/web/assets/sprites/building-j.png +0 -0
- package/city/web/assets/sprites/building-k.png +0 -0
- package/city/web/assets/sprites/building-l.png +0 -0
- package/city/web/assets/sprites/building-m.png +0 -0
- package/city/web/assets/sprites/building-n.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
- package/city/web/assets/sprites/catalogo.json +47 -0
- package/city/web/assets/sprites/detail-awning-wide.png +0 -0
- package/city/web/assets/sprites/detail-awning.png +0 -0
- package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
- package/city/web/assets/sprites/detail-overhang.png +0 -0
- package/city/web/assets/sprites/detail-parasol-a.png +0 -0
- package/city/web/assets/sprites/detail-parasol-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-c.png +0 -0
- package/city/web/assets/sprites/low-detail-building-d.png +0 -0
- package/city/web/assets/sprites/low-detail-building-e.png +0 -0
- package/city/web/assets/sprites/low-detail-building-f.png +0 -0
- package/city/web/assets/sprites/low-detail-building-g.png +0 -0
- package/city/web/assets/sprites/low-detail-building-h.png +0 -0
- package/city/web/assets/sprites/low-detail-building-i.png +0 -0
- package/city/web/assets/sprites/low-detail-building-j.png +0 -0
- package/city/web/assets/sprites/low-detail-building-k.png +0 -0
- package/city/web/assets/sprites/low-detail-building-l.png +0 -0
- package/city/web/assets/sprites/low-detail-building-m.png +0 -0
- package/city/web/assets/sprites/low-detail-building-n.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/assets/sprites/medidas.json +248 -0
- package/city/web/dist/city.js +939 -0
- package/city/web/dist/index.html +294 -0
- package/city/web/dist/modelos/License.txt +28 -0
- package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/dist/modelos/building-a.glb +0 -0
- package/city/web/dist/modelos/building-b.glb +0 -0
- package/city/web/dist/modelos/building-c.glb +0 -0
- package/city/web/dist/modelos/building-d.glb +0 -0
- package/city/web/dist/modelos/building-e.glb +0 -0
- package/city/web/dist/modelos/building-f.glb +0 -0
- package/city/web/dist/modelos/building-g.glb +0 -0
- package/city/web/dist/modelos/building-h.glb +0 -0
- package/city/web/dist/sprites/building-a.png +0 -0
- package/city/web/dist/sprites/building-b.png +0 -0
- package/city/web/dist/sprites/building-c.png +0 -0
- package/city/web/dist/sprites/building-d.png +0 -0
- package/city/web/dist/sprites/building-e.png +0 -0
- package/city/web/dist/sprites/building-f.png +0 -0
- package/city/web/dist/sprites/building-g.png +0 -0
- package/city/web/dist/sprites/building-h.png +0 -0
- package/city/web/dist/sprites/building-i.png +0 -0
- package/city/web/dist/sprites/building-j.png +0 -0
- package/city/web/dist/sprites/building-k.png +0 -0
- package/city/web/dist/sprites/building-l.png +0 -0
- package/city/web/dist/sprites/building-m.png +0 -0
- package/city/web/dist/sprites/building-n.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
- package/city/web/dist/sprites/catalogo.json +47 -0
- package/city/web/dist/sprites/detail-awning-wide.png +0 -0
- package/city/web/dist/sprites/detail-awning.png +0 -0
- package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
- package/city/web/dist/sprites/detail-overhang.png +0 -0
- package/city/web/dist/sprites/detail-parasol-a.png +0 -0
- package/city/web/dist/sprites/detail-parasol-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-c.png +0 -0
- package/city/web/dist/sprites/low-detail-building-d.png +0 -0
- package/city/web/dist/sprites/low-detail-building-e.png +0 -0
- package/city/web/dist/sprites/low-detail-building-f.png +0 -0
- package/city/web/dist/sprites/low-detail-building-g.png +0 -0
- package/city/web/dist/sprites/low-detail-building-h.png +0 -0
- package/city/web/dist/sprites/low-detail-building-i.png +0 -0
- package/city/web/dist/sprites/low-detail-building-j.png +0 -0
- package/city/web/dist/sprites/low-detail-building-k.png +0 -0
- package/city/web/dist/sprites/low-detail-building-l.png +0 -0
- package/city/web/dist/sprites/low-detail-building-m.png +0 -0
- package/city/web/dist/sprites/low-detail-building-n.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/dist/sprites/medidas.json +248 -0
- package/city/web/dist-hall/hall.js +1363 -0
- package/city/web/index.html +294 -0
- package/city/web/package.json +18 -0
- package/city/web/sella.py +21 -0
- package/city/web/src/activity-actors.ts +47 -0
- package/city/web/src/activity.ts +172 -0
- package/city/web/src/ayuntamiento.ts +580 -0
- package/city/web/src/draw.ts +640 -0
- package/city/web/src/game-speech.ts +79 -0
- package/city/web/src/hall.ts +1959 -0
- package/city/web/src/main.ts +2252 -0
- package/city/web/src/oven.ts +124 -0
- package/city/web/src/people.ts +244 -0
- package/city/web/src/presencia.ts +193 -0
- package/city/web/src/puertas.ts +180 -0
- package/city/web/src/repo-roles.ts +59 -0
- package/city/worker/package.json +16 -0
- package/city/worker/schema.sql +117 -0
- package/city/worker/src/index.ts +702 -0
- package/city/worker/src/square.ts +218 -0
- package/city/worker/wrangler.toml +58 -0
- package/demo/ada.md +50 -0
- package/demo/bruno.md +62 -0
- package/demo/camila.md +56 -0
- package/demo/city/ada.md +40 -0
- package/demo/city/city.yml +8 -0
- package/demo/city/parcels.yml +15 -0
- package/demo/city/roads.json +4 -0
- package/demo/city/units.yml +10 -0
- package/demo/clinica/city.yml +8 -0
- package/demo/clinica/parcels.yml +15 -0
- package/demo/clinica/roads.json +4 -0
- package/demo/clinica/units.yml +9 -0
- package/demo/clinica/vera.md +46 -0
- package/demo/dante.md +54 -0
- package/demo/despacho/city.yml +8 -0
- package/demo/despacho/marta.md +46 -0
- package/demo/despacho/parcels.yml +15 -0
- package/demo/despacho/roads.json +4 -0
- package/demo/despacho/units.yml +9 -0
- package/demo/elsa.md +50 -0
- package/demo/farid.md +52 -0
- package/demo/greta.md +50 -0
- package/demo/hugo.md +49 -0
- package/demo/iris.md +49 -0
- package/demo/jonas.md +51 -0
- package/demo/kira.md +51 -0
- package/demo/luca.md +49 -0
- package/demo/parcels.yml +92 -0
- package/demo/seed.py +20 -0
- package/demo/show.py +161 -0
- package/demo/stories.py +824 -0
- package/demo/units.yml +33 -0
- package/docs/agents-first.md +106 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +265 -0
- package/docs/self-host.md +151 -0
- package/docs/testing.md +205 -0
- package/package.json +73 -0
- package/plugin/.claude-plugin/plugin.json +62 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/channel/activity-cli.ts +16 -0
- package/plugin/channel/adapter-prompts.ts +113 -0
- package/plugin/channel/adapter.js +4292 -0
- package/plugin/channel/adapter.ts +133 -0
- package/plugin/channel/bus.js +20188 -0
- package/plugin/channel/bus.ts +131 -0
- package/plugin/channel/city-config.ts +125 -0
- package/plugin/channel/claude-channel.ts +58 -0
- package/plugin/channel/cli-args.ts +75 -0
- package/plugin/channel/client.js +4200 -0
- package/plugin/channel/client.ts +38 -0
- package/plugin/channel/committee/activity.ts +256 -0
- package/plugin/channel/committee/collection.ts +154 -0
- package/plugin/channel/committee/decision.ts +175 -0
- package/plugin/channel/committee/floor.ts +135 -0
- package/plugin/channel/committee/guards.ts +30 -0
- package/plugin/channel/committee/history.ts +38 -0
- package/plugin/channel/committee/render.ts +102 -0
- package/plugin/channel/committee/service.ts +72 -0
- package/plugin/channel/committee/storage.ts +92 -0
- package/plugin/channel/committee/types.ts +163 -0
- package/plugin/channel/committee/view.ts +63 -0
- package/plugin/channel/committee-cli.ts +109 -0
- package/plugin/channel/delivery-metrics.ts +61 -0
- package/plugin/channel/delivery-queue.ts +118 -0
- package/plugin/channel/hub/activity-controller.ts +72 -0
- package/plugin/channel/hub/activity-feed.ts +169 -0
- package/plugin/channel/hub/committee-controller.ts +60 -0
- package/plugin/channel/hub/connections.ts +48 -0
- package/plugin/channel/hub/diagnostics.ts +67 -0
- package/plugin/channel/hub/envelope-validity.ts +109 -0
- package/plugin/channel/hub/envelopes.ts +66 -0
- package/plugin/channel/hub/lifecycle.ts +91 -0
- package/plugin/channel/hub/local-roads.ts +89 -0
- package/plugin/channel/hub/remote-roads.ts +165 -0
- package/plugin/channel/hub/road-controller.ts +99 -0
- package/plugin/channel/hub-client.ts +149 -0
- package/plugin/channel/local-hub.js +5995 -0
- package/plugin/channel/local-hub.ts +349 -0
- package/plugin/channel/map-reporter.ts +48 -0
- package/plugin/channel/package-lock.json +1731 -0
- package/plugin/channel/package.json +19 -0
- package/plugin/channel/protocol.ts +98 -0
- package/plugin/channel/road-cli.ts +18 -0
- package/plugin/channel/run.sh +15 -0
- package/plugin/channel/runtime/claude.ts +458 -0
- package/plugin/channel/runtime/codex-config.ts +77 -0
- package/plugin/channel/runtime/codex.ts +669 -0
- package/plugin/channel/runtime/command.ts +62 -0
- package/plugin/channel/runtime/factory.ts +15 -0
- package/plugin/channel/runtime/json-rpc.ts +154 -0
- package/plugin/channel/runtime/kimi.ts +277 -0
- package/plugin/channel/runtime/opencode.ts +248 -0
- package/plugin/channel/runtime/process.ts +84 -0
- package/plugin/channel/runtime/types.ts +45 -0
- package/plugin/channel/runtime-files.ts +73 -0
- package/plugin/channel/runtime-gateway.js +6396 -0
- package/plugin/channel/runtime-gateway.ts +358 -0
- package/plugin/channel/runtime-metrics.ts +52 -0
- package/plugin/channel/runtime-subscription.ts +180 -0
- package/plugin/channel/terminal-delivery.ts +131 -0
- package/plugin/channel/untrusted.ts +60 -0
- package/plugin/commands/committee.md +31 -0
- package/plugin/commands/exit.md +32 -0
- package/plugin/commands/goals.md +20 -0
- package/plugin/commands/join.md +18 -0
- package/plugin/commands/notice.md +20 -0
- package/plugin/commands/propose.md +19 -0
- package/plugin/commands/round.md +17 -0
- package/plugin/commands/session.md +22 -0
- package/plugin/commands/settings.md +25 -0
- package/plugin/commands/setup.md +21 -0
- package/plugin/commands/team.md +23 -0
- package/plugin/domains/custom.md +27 -0
- package/plugin/domains/finance.md +31 -0
- package/plugin/domains/healthcare.md +34 -0
- package/plugin/domains/legal.md +31 -0
- package/plugin/domains/marketing.md +33 -0
- package/plugin/domains/operations.md +31 -0
- package/plugin/domains/research.md +32 -0
- package/plugin/domains/sales.md +30 -0
- package/plugin/domains/software.md +37 -0
- package/plugin/hooks/activity.sh +6 -0
- package/plugin/hooks/digging.sh +68 -0
- package/plugin/hooks/growth.sh +42 -0
- package/plugin/hooks/hooks.json +106 -0
- package/plugin/hooks/notice-on-pr.sh +35 -0
- package/plugin/hooks/notice-on-stop.sh +80 -0
- package/plugin/hooks/notice-pending.sh +58 -0
- package/plugin/hooks/solo-en-ciudad.sh +23 -0
- package/plugin/hooks/tokens.sh +47 -0
- package/plugin/roles/examples/account-executive.md +25 -0
- package/plugin/roles/examples/ai-manager.md +34 -0
- package/plugin/roles/examples/associate.md +24 -0
- package/plugin/roles/examples/brand-lead.md +20 -0
- package/plugin/roles/examples/cfo.md +20 -0
- package/plugin/roles/examples/city-lead.md +25 -0
- package/plugin/roles/examples/clinical-director.md +26 -0
- package/plugin/roles/examples/clinical-ops.md +25 -0
- package/plugin/roles/examples/clinician.md +25 -0
- package/plugin/roles/examples/compliance.md +27 -0
- package/plugin/roles/examples/content.md +24 -0
- package/plugin/roles/examples/controller.md +27 -0
- package/plugin/roles/examples/cpto.md +41 -0
- package/plugin/roles/examples/customer-success.md +25 -0
- package/plugin/roles/examples/data-engineer.md +37 -0
- package/plugin/roles/examples/data.md +60 -0
- package/plugin/roles/examples/dev.md +37 -0
- package/plugin/roles/examples/devops.md +39 -0
- package/plugin/roles/examples/enablement.md +24 -0
- package/plugin/roles/examples/ethics.md +24 -0
- package/plugin/roles/examples/fin-analytics.md +27 -0
- package/plugin/roles/examples/health-compliance.md +25 -0
- package/plugin/roles/examples/health-data.md +26 -0
- package/plugin/roles/examples/knowledge.md +25 -0
- package/plugin/roles/examples/lifecycle.md +26 -0
- package/plugin/roles/examples/llm-engineer.md +34 -0
- package/plugin/roles/examples/managing-partner.md +20 -0
- package/plugin/roles/examples/methods.md +23 -0
- package/plugin/roles/examples/operations-lead.md +23 -0
- package/plugin/roles/examples/ops.md +24 -0
- package/plugin/roles/examples/patient-safety.md +26 -0
- package/plugin/roles/examples/performance.md +27 -0
- package/plugin/roles/examples/po.md +36 -0
- package/plugin/roles/examples/process-owner.md +23 -0
- package/plugin/roles/examples/product-design.md +39 -0
- package/plugin/roles/examples/program-manager.md +24 -0
- package/plugin/roles/examples/quality.md +25 -0
- package/plugin/roles/examples/research-director.md +24 -0
- package/plugin/roles/examples/research-ops.md +25 -0
- package/plugin/roles/examples/researcher.md +25 -0
- package/plugin/roles/examples/revenue-lead.md +25 -0
- package/plugin/roles/examples/revops.md +26 -0
- package/plugin/roles/examples/seo.md +27 -0
- package/plugin/roles/examples/specialist.md +24 -0
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +492 -0
- package/plugin/scripts/capabilities.py +210 -0
- package/plugin/scripts/card.py +322 -0
- package/plugin/scripts/cities.py +520 -0
- package/plugin/scripts/city-env.sh +90 -0
- package/plugin/scripts/city-runtime.sh +87 -0
- package/plugin/scripts/city-session.sh +552 -0
- package/plugin/scripts/city_env.py +67 -0
- package/plugin/scripts/crecimiento.py +140 -0
- package/plugin/scripts/deliberations.py +48 -0
- package/plugin/scripts/doctor.py +294 -0
- package/plugin/scripts/domains.py +201 -0
- package/plugin/scripts/evidencia.py +64 -0
- package/plugin/scripts/find-repos.sh +111 -0
- package/plugin/scripts/gh.py +159 -0
- package/plugin/scripts/hall_protocol.py +147 -0
- package/plugin/scripts/hook_activity.py +139 -0
- package/plugin/scripts/launch.py +94 -0
- package/plugin/scripts/logs.py +98 -0
- package/plugin/scripts/pairing.py +185 -0
- package/plugin/scripts/parcels.py +132 -0
- package/plugin/scripts/read-card.py +55 -0
- package/plugin/scripts/report.py +201 -0
- package/plugin/scripts/reset.py +180 -0
- package/plugin/scripts/roads.py +230 -0
- package/plugin/scripts/roles.py +274 -0
- package/plugin/scripts/runtime_log.py +69 -0
- package/plugin/scripts/runtime_processes.py +132 -0
- package/plugin/scripts/rutas.py +83 -0
- package/plugin/scripts/seat.py +1696 -0
- package/plugin/scripts/tokens.py +194 -0
- package/plugin/scripts/trust-repos.py +59 -0
- package/plugin/scripts/ui.py +280 -0
- package/plugin/scripts/units.py +80 -0
- package/plugin/scripts/workspace.py +561 -0
- package/plugin/skills/city/SKILL.md +191 -0
- package/templates/blank.md +39 -0
- package/templates/finance.md +44 -0
- package/templates/legal.md +45 -0
- package/templates/marketing.md +48 -0
- package/templates/product.md +45 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Knowing there is a new version, and getting it.
|
|
3
|
+
|
|
4
|
+
A tool that cannot tell you it is out of date leaves every owner running
|
|
5
|
+
whatever they installed the day they found it — including the day a security
|
|
6
|
+
fix shipped. So this asks npm, and it asks rarely: once a day at most, cached
|
|
7
|
+
under the runtime dir, and never on a plain command. The check runs where the
|
|
8
|
+
person deliberately opened something (`doctor`, `update`, the Hall), not behind
|
|
9
|
+
their back on every `agents-city cities`.
|
|
10
|
+
|
|
11
|
+
It is a GET to the public registry and nothing else: no identifiers, no
|
|
12
|
+
counters, nothing about this machine leaves it. `CITY_UPDATE_CHECK=0` turns it
|
|
13
|
+
off completely, and the answer is cached so a plane, a firewall or a dead
|
|
14
|
+
registry degrade to silence rather than to an error.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.request
|
|
24
|
+
|
|
25
|
+
GUIONES = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
sys.path.insert(0, GUIONES)
|
|
27
|
+
import cities # noqa: E402
|
|
28
|
+
|
|
29
|
+
PAQUETE = 'agents-city'
|
|
30
|
+
REGISTRO = f'https://registry.npmjs.org/{PAQUETE}/latest'
|
|
31
|
+
#: A day. The point is to notice a release, not to poll a registry.
|
|
32
|
+
VIDA = 24 * 3600
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def version_instalada():
|
|
36
|
+
"""What is running right now, read from the package this file ships in."""
|
|
37
|
+
raiz = os.path.dirname(os.path.dirname(GUIONES))
|
|
38
|
+
try:
|
|
39
|
+
with open(os.path.join(raiz, 'package.json'), encoding='utf-8') as f:
|
|
40
|
+
return str(json.load(f).get('version') or '')
|
|
41
|
+
except (OSError, ValueError):
|
|
42
|
+
return ''
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cache():
|
|
46
|
+
return os.path.join(cities.raiz(), '.runtime', 'version.json')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _lee_cache():
|
|
50
|
+
try:
|
|
51
|
+
with open(_cache(), encoding='utf-8') as f:
|
|
52
|
+
return json.load(f)
|
|
53
|
+
except (OSError, ValueError):
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _guarda_cache(ultima):
|
|
58
|
+
ruta = _cache()
|
|
59
|
+
try:
|
|
60
|
+
os.makedirs(os.path.dirname(ruta), mode=0o700, exist_ok=True)
|
|
61
|
+
cities.escribe_atomico(ruta, json.dumps({'latest': ultima, 'when': time.time()}))
|
|
62
|
+
except OSError:
|
|
63
|
+
pass # not being able to remember is not a reason to fail a command
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def consulta_registro(timeout=8):
|
|
67
|
+
"""The published version, or '' when the network says nothing useful.
|
|
68
|
+
|
|
69
|
+
Eight seconds because the first call of the day pays DNS and a TLS
|
|
70
|
+
handshake from cold; a tighter budget turns "you are up to date" into a
|
|
71
|
+
lie told by a stopwatch.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
peticion = urllib.request.Request(
|
|
75
|
+
REGISTRO, headers={'Accept': 'application/vnd.npm.install-v1+json'}
|
|
76
|
+
)
|
|
77
|
+
with urllib.request.urlopen(peticion, timeout=timeout) as r:
|
|
78
|
+
return str(json.load(r).get('version') or '')
|
|
79
|
+
except (urllib.error.URLError, OSError, ValueError, TimeoutError):
|
|
80
|
+
return ''
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _partes(v):
|
|
84
|
+
"""A version as comparable pieces. A prerelease sorts BELOW its release."""
|
|
85
|
+
base, _, pre = str(v).partition('-')
|
|
86
|
+
numeros = []
|
|
87
|
+
for trozo in base.split('.'):
|
|
88
|
+
try:
|
|
89
|
+
numeros.append(int(trozo))
|
|
90
|
+
except ValueError:
|
|
91
|
+
numeros.append(0)
|
|
92
|
+
while len(numeros) < 3:
|
|
93
|
+
numeros.append(0)
|
|
94
|
+
# No prerelease outranks one: 1.0.0 > 1.0.0-beta.1, as semver says.
|
|
95
|
+
return (numeros[0], numeros[1], numeros[2], 1 if not pre else 0, pre)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def es_mas_nueva(candidata, actual):
|
|
99
|
+
return bool(candidata) and bool(actual) and _partes(candidata) > _partes(actual)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def comprueba(forzar=False, vida=VIDA):
|
|
103
|
+
"""(installed, published, there-is-an-update). Cached, quiet, opt-out-able.
|
|
104
|
+
|
|
105
|
+
Returns the published version as '' when the check is switched off or the
|
|
106
|
+
registry could not be reached — the caller then says nothing, which is the
|
|
107
|
+
right thing to say when you do not know.
|
|
108
|
+
"""
|
|
109
|
+
instalada = version_instalada()
|
|
110
|
+
if os.environ.get('CITY_UPDATE_CHECK', '1') == '0':
|
|
111
|
+
return instalada, '', False
|
|
112
|
+
guardado = _lee_cache()
|
|
113
|
+
fresca = (time.time() - float(guardado.get('when') or 0)) < vida
|
|
114
|
+
if fresca and not forzar:
|
|
115
|
+
ultima = str(guardado.get('latest') or '')
|
|
116
|
+
else:
|
|
117
|
+
ultima = consulta_registro()
|
|
118
|
+
if ultima:
|
|
119
|
+
_guarda_cache(ultima)
|
|
120
|
+
else:
|
|
121
|
+
ultima = str(guardado.get('latest') or '')
|
|
122
|
+
return instalada, ultima, es_mas_nueva(ultima, instalada)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def aviso():
|
|
126
|
+
"""One line for a person who did not ask, or '' when there is nothing to say."""
|
|
127
|
+
instalada, ultima, hay = comprueba()
|
|
128
|
+
if not hay:
|
|
129
|
+
return ''
|
|
130
|
+
return (f' A newer Agents City is out: {instalada} → {ultima}. '
|
|
131
|
+
f'Update with: agents-city update')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def como_se_instalo():
|
|
135
|
+
"""'npm' when this copy lives inside a global npm install, else 'clone'.
|
|
136
|
+
|
|
137
|
+
It decides what `update` may do: pulling somebody's git checkout from under
|
|
138
|
+
them is not an update, it is a surprise.
|
|
139
|
+
"""
|
|
140
|
+
raiz = os.path.dirname(os.path.dirname(GUIONES))
|
|
141
|
+
if os.path.isdir(os.path.join(raiz, '.git')):
|
|
142
|
+
return 'clone'
|
|
143
|
+
return 'npm' if f'node_modules{os.sep}{PAQUETE}' in raiz else 'clone'
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def actualiza(canal=''):
|
|
147
|
+
"""Install the newest published version over this one. Returns an exit code."""
|
|
148
|
+
if como_se_instalo() == 'clone':
|
|
149
|
+
print(' This is a git checkout, not an npm install.\n'
|
|
150
|
+
' Update it the way you got it:\n'
|
|
151
|
+
' git pull && npm pack && npm install -g ./agents-city-*.tgz')
|
|
152
|
+
return 1
|
|
153
|
+
destino = f'{PAQUETE}@{canal}' if canal else PAQUETE
|
|
154
|
+
print(f' Installing {destino} over {version_instalada()}…')
|
|
155
|
+
hecho = subprocess.run(['npm', 'install', '-g', destino])
|
|
156
|
+
if hecho.returncode != 0:
|
|
157
|
+
print(' npm could not install it. Nothing was changed.', file=sys.stderr)
|
|
158
|
+
return hecho.returncode
|
|
159
|
+
print(' Done. Open cities keep the code they already loaded;\n'
|
|
160
|
+
' `agents-city exit <city>` and then `agents-city seat` picks the new one up.')
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _uso():
|
|
165
|
+
print(
|
|
166
|
+
' usage: agents-city update [--check] [--tag beta]\n\n'
|
|
167
|
+
' Install the newest published version, or just ask whether there is one.\n\n'
|
|
168
|
+
' --check say what is installed and what is published, and stop\n'
|
|
169
|
+
' --tag NAME follow a dist-tag (for example: beta)\n\n'
|
|
170
|
+
' The check is one GET to the public npm registry, cached for a day.\n'
|
|
171
|
+
' Nothing about this machine is sent. CITY_UPDATE_CHECK=0 disables it.'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main():
|
|
176
|
+
args = sys.argv[1:]
|
|
177
|
+
if any(a in ('-h', '--help', 'help') for a in args):
|
|
178
|
+
_uso()
|
|
179
|
+
return 0
|
|
180
|
+
if '--check' in args:
|
|
181
|
+
instalada, ultima, hay = comprueba(forzar=True)
|
|
182
|
+
if not ultima:
|
|
183
|
+
print(f' Installed: {instalada}. The registry could not be reached '
|
|
184
|
+
'(or the check is off).')
|
|
185
|
+
return 0
|
|
186
|
+
print(f' Installed: {instalada}\n Published: {ultima}')
|
|
187
|
+
print(' There is a newer version — agents-city update' if hay
|
|
188
|
+
else ' You are up to date.')
|
|
189
|
+
return 0
|
|
190
|
+
canal = ''
|
|
191
|
+
if '--tag' in args:
|
|
192
|
+
i = args.index('--tag')
|
|
193
|
+
canal = args[i + 1] if len(args) > i + 1 else ''
|
|
194
|
+
return actualiza(canal)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == '__main__':
|
|
198
|
+
sys.exit(main())
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Admission as an ordered gate graph, not a boolean.
|
|
3
|
+
|
|
4
|
+
Whether a seat accepts an inbound road message is a decision with a *reason*,
|
|
5
|
+
and the reason must be inspectable without leaking who is on the allowlist. So
|
|
6
|
+
every admission returns the ordered list of gates it evaluated, which gate was
|
|
7
|
+
decisive, a stable machine-readable reason code, and — for each gate — a
|
|
8
|
+
redacted diagnostic: counts and opaque ids, never a raw sender or allowlist
|
|
9
|
+
entry.
|
|
10
|
+
|
|
11
|
+
The mechanism (an ordered reducer that stops at the first blocking gate) is
|
|
12
|
+
kept separate from the policy (`decide_entrada`), so new gates or new callers
|
|
13
|
+
compose without rewriting either. Pure and transport-free: the tests drive it
|
|
14
|
+
directly, and the same function can decide a local road, a remote road, or a
|
|
15
|
+
future chat channel.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Admision(str, Enum):
|
|
24
|
+
DESPACHA = 'dispatch' # deliver to the seat
|
|
25
|
+
OBSERVA = 'observe' # record as context, do not act
|
|
26
|
+
OMITE = 'skip' # not addressed to us; ignore quietly
|
|
27
|
+
DESCARTA = 'drop' # malformed or refused; drop
|
|
28
|
+
EMPAREJA = 'pairing-required' # unknown sender, a pairing code is owed
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
return self.value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#: Closed set of reason codes. A code outside this set is a bug, so the
|
|
35
|
+
#: constructor asserts membership rather than letting prose leak in.
|
|
36
|
+
RAZONES = frozenset({
|
|
37
|
+
'road_allowed',
|
|
38
|
+
'road_missing',
|
|
39
|
+
'road_incident',
|
|
40
|
+
'sender_not_paired',
|
|
41
|
+
'sender_paired',
|
|
42
|
+
'payload_malformed',
|
|
43
|
+
'payload_empty',
|
|
44
|
+
'not_addressed_here',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
#: Which reason a gate emits maps to how the seat should treat the message.
|
|
48
|
+
_ADMISION_POR_RAZON = {
|
|
49
|
+
'road_allowed': Admision.DESPACHA,
|
|
50
|
+
'sender_paired': Admision.DESPACHA,
|
|
51
|
+
'road_missing': Admision.OMITE,
|
|
52
|
+
'not_addressed_here': Admision.OMITE,
|
|
53
|
+
'road_incident': Admision.DESCARTA,
|
|
54
|
+
'payload_malformed': Admision.DESCARTA,
|
|
55
|
+
'payload_empty': Admision.DESCARTA,
|
|
56
|
+
'sender_not_paired': Admision.EMPAREJA,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def id_opaco(valor):
|
|
61
|
+
"""A stable, non-reversible short id for one allowlist entry or sender."""
|
|
62
|
+
return 'x' + hashlib.sha256(str(valor).encode()).hexdigest()[:12]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def redacta(entradas):
|
|
66
|
+
"""A diagnostic for an allowlist that reveals shape, never content."""
|
|
67
|
+
entradas = list(entradas)
|
|
68
|
+
return {'n': len(entradas), 'ids': [id_opaco(e) for e in entradas]}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Puerta:
|
|
73
|
+
id: str
|
|
74
|
+
fase: str
|
|
75
|
+
permitido: bool
|
|
76
|
+
razon: str
|
|
77
|
+
diagnostico: dict = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
def __post_init__(self):
|
|
80
|
+
# A real raise, not assert: validation must survive `python -O`.
|
|
81
|
+
if self.razon not in RAZONES:
|
|
82
|
+
raise ValueError(f'unknown reason code: {self.razon}')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class Decision:
|
|
87
|
+
admision: Admision
|
|
88
|
+
razon: str
|
|
89
|
+
puerta_decisiva: str
|
|
90
|
+
grafo: tuple
|
|
91
|
+
|
|
92
|
+
def permitido(self):
|
|
93
|
+
# Authorization lives here and ONLY here — never inferred from an
|
|
94
|
+
# evidence field, so a blocked decision can never read as authorised.
|
|
95
|
+
return self.admision is Admision.DESPACHA
|
|
96
|
+
|
|
97
|
+
def como_dict(self):
|
|
98
|
+
return {
|
|
99
|
+
'admission': str(self.admision),
|
|
100
|
+
'reasonCode': self.razon,
|
|
101
|
+
'decisiveGate': self.puerta_decisiva,
|
|
102
|
+
'gates': [
|
|
103
|
+
{'id': g.id, 'phase': g.fase, 'allowed': g.permitido,
|
|
104
|
+
'reasonCode': g.razon, 'diagnostic': g.diagnostico}
|
|
105
|
+
for g in self.grafo
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resuelve(grafo):
|
|
111
|
+
"""Reduce an ordered gate list to a decision: first blocker wins, else last.
|
|
112
|
+
|
|
113
|
+
The decision carries no evidence state on purpose: allow/deny is `permitido()`
|
|
114
|
+
(derived from the admission), and an audit-provenance vocabulary belongs to
|
|
115
|
+
the layer that records the action, not to this pure routing decision. That
|
|
116
|
+
keeps authorization impossible to read off a decorative field.
|
|
117
|
+
"""
|
|
118
|
+
decisiva = next((g for g in grafo if not g.permitido), grafo[-1])
|
|
119
|
+
admision = _ADMISION_POR_RAZON[decisiva.razon]
|
|
120
|
+
return Decision(admision, decisiva.razon, decisiva.id, tuple(grafo))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def decide_entrada(*, road_existe, road_incidente, direccion_coincide,
|
|
124
|
+
emparejado, allowlist, payload_valido, texto_no_vacio,
|
|
125
|
+
dirigido_aqui=True):
|
|
126
|
+
"""Agents City's road-inbound policy, phase by phase.
|
|
127
|
+
|
|
128
|
+
Order matters and is the policy: address → road → sender → payload. A road
|
|
129
|
+
that resolves but has no matching sender is an explicit block, never a
|
|
130
|
+
fallthrough; an unknown sender under pairing yields EMPAREJA so the seat can
|
|
131
|
+
mint a code rather than silently dropping a first contact.
|
|
132
|
+
"""
|
|
133
|
+
grafo = []
|
|
134
|
+
diag = redacta(allowlist)
|
|
135
|
+
|
|
136
|
+
if not dirigido_aqui:
|
|
137
|
+
grafo.append(Puerta('address', 'route', False, 'not_addressed_here'))
|
|
138
|
+
return _resuelve(grafo)
|
|
139
|
+
grafo.append(Puerta('address', 'route', True, 'road_allowed'))
|
|
140
|
+
|
|
141
|
+
if not road_existe:
|
|
142
|
+
grafo.append(Puerta('road', 'route', False, 'road_missing', diag))
|
|
143
|
+
return _resuelve(grafo)
|
|
144
|
+
if road_incidente:
|
|
145
|
+
grafo.append(Puerta('road', 'route', False, 'road_incident', diag))
|
|
146
|
+
return _resuelve(grafo)
|
|
147
|
+
grafo.append(Puerta('road', 'route', True, 'road_allowed', diag))
|
|
148
|
+
|
|
149
|
+
if not direccion_coincide:
|
|
150
|
+
if emparejado:
|
|
151
|
+
grafo.append(Puerta('sender', 'sender', True, 'sender_paired', diag))
|
|
152
|
+
else:
|
|
153
|
+
grafo.append(Puerta('sender', 'sender', False, 'sender_not_paired', diag))
|
|
154
|
+
return _resuelve(grafo)
|
|
155
|
+
else:
|
|
156
|
+
grafo.append(Puerta('sender', 'sender', True, 'road_allowed', diag))
|
|
157
|
+
|
|
158
|
+
if not payload_valido:
|
|
159
|
+
grafo.append(Puerta('payload', 'event', False, 'payload_malformed'))
|
|
160
|
+
return _resuelve(grafo)
|
|
161
|
+
if not texto_no_vacio:
|
|
162
|
+
grafo.append(Puerta('payload', 'event', False, 'payload_empty'))
|
|
163
|
+
return _resuelve(grafo)
|
|
164
|
+
grafo.append(Puerta('payload', 'event', True, 'road_allowed'))
|
|
165
|
+
return _resuelve(grafo)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Close the whole day: the tmux sessions, every agent inside them, the hall,
|
|
3
|
+
the map. The seat's missing other half.
|
|
4
|
+
|
|
5
|
+
agents-city exit close everything, every city
|
|
6
|
+
agents-city exit home close ONE city — its session, its agents, its
|
|
7
|
+
map — and leave the others working
|
|
8
|
+
agents-city exit --dry-run say what would close, close nothing
|
|
9
|
+
|
|
10
|
+
`seat` builds a session with an agent per window and nothing ever tore it down:
|
|
11
|
+
detach, open another city, run the hall — and a machine fills with processes
|
|
12
|
+
nobody remembers starting. One session was found nineteen windows deep while
|
|
13
|
+
this was being written.
|
|
14
|
+
|
|
15
|
+
Precision matters more than reach here. This closes ONLY what the city started,
|
|
16
|
+
identified by marks the city itself leaves, and never by broad process-name
|
|
17
|
+
sweeps that would take down some other project's dev server:
|
|
18
|
+
|
|
19
|
+
* tmux sessions whose name is one this product minted — always user-city —
|
|
20
|
+
across every city this user owns. Your own
|
|
21
|
+
unrelated tmux sessions are not touched.
|
|
22
|
+
* the town hall, matched by its own entry point.
|
|
23
|
+
* the map and the demo, found by who holds the city's local database open
|
|
24
|
+
(`~/.agents-city/state`) — not by killing every wrangler on the machine.
|
|
25
|
+
|
|
26
|
+
Killing a session SIGHUPs its panes, which is how every Claude inside exits.
|
|
27
|
+
"""
|
|
28
|
+
import argparse
|
|
29
|
+
import os
|
|
30
|
+
import signal
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
36
|
+
sys.path.insert(0, AQUI)
|
|
37
|
+
import cities # noqa: E402
|
|
38
|
+
import runtime_processes # noqa: E402
|
|
39
|
+
|
|
40
|
+
ESTADO = os.path.join(cities.raiz(), 'state')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _corre(args):
|
|
44
|
+
try:
|
|
45
|
+
r = subprocess.run(args, capture_output=True, text=True, timeout=30)
|
|
46
|
+
return r.stdout
|
|
47
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
48
|
+
return ''
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sesiones_de_la_ciudad(solo=''):
|
|
52
|
+
"""Every live tmux session this product would have minted, and only those.
|
|
53
|
+
|
|
54
|
+
Candidates come from city metadata: for each city this machine knows, the
|
|
55
|
+
owner-city session minted by cities.py. A session called anything else is
|
|
56
|
+
somebody's own work and stays alive. With `solo` set to one
|
|
57
|
+
city's folder, only that city's sessions qualify — closing the client's day
|
|
58
|
+
must not end the company's.
|
|
59
|
+
"""
|
|
60
|
+
vivas = {l.strip() for l in _corre(['tmux', 'ls', '-F', '#{session_name}']).splitlines()
|
|
61
|
+
if l.strip()}
|
|
62
|
+
if not vivas:
|
|
63
|
+
return []
|
|
64
|
+
donde = ([{'ruta': solo}] if solo else cities.lista())
|
|
65
|
+
candidatas = set()
|
|
66
|
+
for ciudad in donde:
|
|
67
|
+
usuario = cities.lee_clave(ciudad['ruta'], 'owner')
|
|
68
|
+
if usuario:
|
|
69
|
+
candidatas.add(cities.sesion(usuario, ciudad['ruta']))
|
|
70
|
+
return sorted(vivas & candidatas)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def servidores():
|
|
74
|
+
"""The Hall and an in-flight setup launcher, by their own entry points —
|
|
75
|
+
never a pattern loose enough to match somebody else's Python."""
|
|
76
|
+
pids = []
|
|
77
|
+
for patron in ('bin/hall', 'bin/setup.py'):
|
|
78
|
+
salida = _corre(['pgrep', '-f', patron])
|
|
79
|
+
pids += [int(p) for p in salida.split() if p.strip().isdigit()]
|
|
80
|
+
return sorted(set(pids) - {os.getpid(), os.getppid()})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def mapas(solo=''):
|
|
84
|
+
"""Whoever holds the city's local database open: wrangler, workerd, the demo.
|
|
85
|
+
lsof on our own state directory is exact — it cannot name another project's
|
|
86
|
+
dev server, because no other project has these files open. With `solo`, only
|
|
87
|
+
that city's slice of the state; the demo counts as everything-only."""
|
|
88
|
+
donde = os.path.join(ESTADO, cities.slug(solo)) if solo else ESTADO
|
|
89
|
+
if not os.path.isdir(donde):
|
|
90
|
+
return []
|
|
91
|
+
salida = _corre(['lsof', '-t', '+D', donde])
|
|
92
|
+
return sorted({int(p) for p in salida.split() if p.strip().isdigit()}
|
|
93
|
+
- {os.getpid(), os.getppid()})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def mata(pids):
|
|
97
|
+
"""TERM first, KILL for whatever ignores it. Returns how many actually died."""
|
|
98
|
+
if not pids:
|
|
99
|
+
return 0
|
|
100
|
+
for pid in pids:
|
|
101
|
+
try:
|
|
102
|
+
os.kill(pid, signal.SIGTERM)
|
|
103
|
+
except (ProcessLookupError, PermissionError):
|
|
104
|
+
pass
|
|
105
|
+
time.sleep(1.2)
|
|
106
|
+
muertos = 0
|
|
107
|
+
for pid in pids:
|
|
108
|
+
try:
|
|
109
|
+
os.kill(pid, 0)
|
|
110
|
+
os.kill(pid, signal.SIGKILL)
|
|
111
|
+
except ProcessLookupError:
|
|
112
|
+
muertos += 1
|
|
113
|
+
except PermissionError:
|
|
114
|
+
pass
|
|
115
|
+
return muertos
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _argumentos():
|
|
119
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
120
|
+
ap.add_argument('ciudad', nargs='?', default='',
|
|
121
|
+
help='close only this city (name or path); empty closes all')
|
|
122
|
+
ap.add_argument('--dry-run', action='store_true')
|
|
123
|
+
ap.add_argument('-h', '--help', action='store_true')
|
|
124
|
+
return ap.parse_args()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _objetivo(nombre):
|
|
128
|
+
if not nombre:
|
|
129
|
+
return ''
|
|
130
|
+
solo = cities.resuelve(nombre)
|
|
131
|
+
if solo:
|
|
132
|
+
print(f' Only the city at {solo} — the rest keep working.')
|
|
133
|
+
return solo
|
|
134
|
+
conocidas = ', '.join(c['nombre'] for c in cities.lista()) or 'none'
|
|
135
|
+
raise ValueError(f'No city called {nombre!r}. Known here: {conocidas}.')
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _recursos(solo):
|
|
139
|
+
ciudades = [solo] if solo else [c['ruta'] for c in cities.lista()]
|
|
140
|
+
procesos = [(ciudad, proceso) for ciudad in ciudades
|
|
141
|
+
for proceso in runtime_processes.procesos(ciudad)]
|
|
142
|
+
return {
|
|
143
|
+
'sesiones': sesiones_de_la_ciudad(solo),
|
|
144
|
+
# The hall covers all cities, so a scoped exit leaves it alive.
|
|
145
|
+
'webs': [] if solo else servidores(),
|
|
146
|
+
'mapas': mapas(solo),
|
|
147
|
+
'ciudades': ciudades,
|
|
148
|
+
'bus': procesos,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _vacio(recursos):
|
|
153
|
+
return not any(recursos[k] for k in ('sesiones', 'webs', 'mapas', 'bus'))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _describe(recursos, dry_run):
|
|
157
|
+
verbo = 'Would close' if dry_run else 'Closing'
|
|
158
|
+
for sesion_ in recursos['sesiones']:
|
|
159
|
+
ventanas = _corre(['tmux', 'list-windows', '-t', sesion_]).count('\n')
|
|
160
|
+
print(f' {verbo}: tmux session `{sesion_}` — {ventanas} windows, '
|
|
161
|
+
f'every agent inside included')
|
|
162
|
+
webs = recursos['webs']
|
|
163
|
+
if webs:
|
|
164
|
+
print(f' {verbo}: the town hall server ({len(webs)} process'
|
|
165
|
+
f'{"es" if len(webs) > 1 else ""})')
|
|
166
|
+
dibujos = recursos['mapas']
|
|
167
|
+
if dibujos:
|
|
168
|
+
print(f' {verbo}: the map ({len(dibujos)} process'
|
|
169
|
+
f'{"es" if len(dibujos) > 1 else ""} holding the local database)')
|
|
170
|
+
for _, proceso in recursos['bus']:
|
|
171
|
+
print(f' {verbo}: {proceso["label"]} (pid {proceso["pid"]})')
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _cierra_sesiones(sesiones):
|
|
175
|
+
"""Close our current tmux session last so this command can finish."""
|
|
176
|
+
propia = os.environ.get('TMUX', '')
|
|
177
|
+
actual = _corre(['tmux', 'display-message', '-p', '#{session_name}']).strip()
|
|
178
|
+
ultimas = []
|
|
179
|
+
for sesion_ in sesiones:
|
|
180
|
+
if propia and actual == sesion_:
|
|
181
|
+
ultimas.append(sesion_)
|
|
182
|
+
else:
|
|
183
|
+
subprocess.run(['tmux', 'kill-session', '-t', sesion_], capture_output=True)
|
|
184
|
+
return ultimas
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _cierra(recursos):
|
|
188
|
+
# Sessions first: SIGHUP lets every runtime exit on its own terms.
|
|
189
|
+
ultimas = _cierra_sesiones(recursos['sesiones'])
|
|
190
|
+
mata(recursos['webs'])
|
|
191
|
+
mata(recursos['mapas'])
|
|
192
|
+
mata([proceso['pid'] for _, proceso in recursos['bus']])
|
|
193
|
+
# A gateway can notice the hub dying in the narrow interval before tmux's
|
|
194
|
+
# SIGHUP reaches it and recreate a hub. Re-scan exact --data argv after the
|
|
195
|
+
# session is gone; this also catches old hubs whose marker was overwritten.
|
|
196
|
+
for _ in range(3):
|
|
197
|
+
restantes = [proceso['pid'] for ciudad in recursos['ciudades']
|
|
198
|
+
for proceso in runtime_processes.procesos(ciudad)]
|
|
199
|
+
if not restantes:
|
|
200
|
+
break
|
|
201
|
+
mata(sorted(set(restantes)))
|
|
202
|
+
for ciudad in recursos['ciudades']:
|
|
203
|
+
runtime_processes.limpia_marcas(ciudad)
|
|
204
|
+
for sesion_ in ultimas:
|
|
205
|
+
subprocess.run(['tmux', 'kill-session', '-t', sesion_], capture_output=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
a = _argumentos()
|
|
210
|
+
if a.help:
|
|
211
|
+
print(__doc__)
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
solo = _objetivo(a.ciudad)
|
|
216
|
+
except ValueError as error:
|
|
217
|
+
print(f' {error}', file=sys.stderr)
|
|
218
|
+
return 1
|
|
219
|
+
recursos = _recursos(solo)
|
|
220
|
+
if _vacio(recursos):
|
|
221
|
+
print(' Nothing of the city is running. The machine is yours.')
|
|
222
|
+
return 0
|
|
223
|
+
_describe(recursos, a.dry_run)
|
|
224
|
+
if a.dry_run:
|
|
225
|
+
return 0
|
|
226
|
+
_cierra(recursos)
|
|
227
|
+
print(' Done. `agents-city seat` brings it all back.')
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == '__main__':
|
|
232
|
+
sys.exit(main())
|