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.
- package/.claude-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2021 -0
- package/README.md +1999 -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 +116 -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/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +426 -0
- package/bin/logs +3 -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 +1428 -0
- package/bin/setup.py +753 -0
- package/bin/skills +3 -0
- package/bin/test +131 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -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 +206 -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 +544 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +106 -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-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 +1386 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1057 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +188 -0
- package/bin/testlib.py +95 -0
- package/bin/tokens.py +14 -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 +264 -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 +1867 -0
- package/city/web/src/main.ts +2225 -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 +99 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +231 -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/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +252 -0
- package/plugin/scripts/capabilities.py +197 -0
- package/plugin/scripts/card.py +312 -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 +526 -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 +172 -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 +51 -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 +1349 -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 +364 -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,379 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Channel delivers the typed WebSocket bus without terminal injection."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import queue
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
RAIZ = os.path.dirname(AQUI)
|
|
15
|
+
sys.path.insert(0, AQUI)
|
|
16
|
+
from testlib import afirma, comprueba, detiene_proceso, resumen # noqa: E402
|
|
17
|
+
|
|
18
|
+
CHANNEL = os.path.join(RAIZ, 'plugin', 'channel', 'run.sh')
|
|
19
|
+
CLIENT = os.path.join(RAIZ, 'plugin', 'channel', 'client.js')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def espera(condicion, segundos=8):
|
|
23
|
+
limite = time.monotonic() + segundos
|
|
24
|
+
while time.monotonic() < limite:
|
|
25
|
+
if condicion():
|
|
26
|
+
return True
|
|
27
|
+
time.sleep(.05)
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Cliente:
|
|
32
|
+
def __init__(self, actor, datos, app, channel=True):
|
|
33
|
+
self.mensajes, self.err = [], []
|
|
34
|
+
self.cola = queue.Queue()
|
|
35
|
+
env = dict(os.environ, CITY_BUS_ACTOR=actor,
|
|
36
|
+
CITY_CLAUDE_CHANNEL='1' if channel else '0',
|
|
37
|
+
AGENTS_CITY_DATA=datos, AGENTS_CITY_HOME=app,
|
|
38
|
+
AGENTS_CITY_USER='alice')
|
|
39
|
+
for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
|
|
40
|
+
env.pop(key, None)
|
|
41
|
+
self.p = subprocess.Popen(
|
|
42
|
+
[CHANNEL], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
43
|
+
stderr=subprocess.PIPE, text=True, bufsize=1, env=env)
|
|
44
|
+
threading.Thread(target=self._lee, daemon=True).start()
|
|
45
|
+
threading.Thread(target=self._lee_err, daemon=True).start()
|
|
46
|
+
self.envia({
|
|
47
|
+
'jsonrpc': '2.0', 'id': 1, 'method': 'initialize',
|
|
48
|
+
'params': {'protocolVersion': '2024-11-05', 'capabilities': {},
|
|
49
|
+
'clientInfo': {'name': 'test', 'version': '1'}},
|
|
50
|
+
})
|
|
51
|
+
self.inicial = self.espera(lambda m: m.get('id') == 1)
|
|
52
|
+
self.envia({'jsonrpc': '2.0', 'method': 'notifications/initialized'})
|
|
53
|
+
|
|
54
|
+
def _lee(self):
|
|
55
|
+
for linea in self.p.stdout:
|
|
56
|
+
try:
|
|
57
|
+
mensaje = json.loads(linea)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
continue
|
|
60
|
+
self.mensajes.append(mensaje)
|
|
61
|
+
self.cola.put(mensaje)
|
|
62
|
+
|
|
63
|
+
def _lee_err(self):
|
|
64
|
+
for linea in self.p.stderr:
|
|
65
|
+
self.err.append(linea.rstrip())
|
|
66
|
+
|
|
67
|
+
def envia(self, mensaje):
|
|
68
|
+
self.p.stdin.write(json.dumps(mensaje) + '\n')
|
|
69
|
+
self.p.stdin.flush()
|
|
70
|
+
|
|
71
|
+
def espera(self, condicion, segundos=8):
|
|
72
|
+
limite = time.monotonic() + segundos
|
|
73
|
+
for mensaje in self.mensajes:
|
|
74
|
+
if condicion(mensaje):
|
|
75
|
+
return mensaje
|
|
76
|
+
while time.monotonic() < limite:
|
|
77
|
+
try:
|
|
78
|
+
mensaje = self.cola.get(timeout=max(.01, min(.2, limite - time.monotonic())))
|
|
79
|
+
except queue.Empty:
|
|
80
|
+
continue
|
|
81
|
+
if condicion(mensaje):
|
|
82
|
+
return mensaje
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
def herramientas(self, ident):
|
|
86
|
+
self.envia({'jsonrpc': '2.0', 'id': ident, 'method': 'tools/list'})
|
|
87
|
+
return self.espera(lambda m: m.get('id') == ident)
|
|
88
|
+
|
|
89
|
+
def herramienta(self, ident, nombre, argumentos=None):
|
|
90
|
+
self.envia({'jsonrpc': '2.0', 'id': ident, 'method': 'tools/call',
|
|
91
|
+
'params': {'name': nombre, 'arguments': argumentos or {}}})
|
|
92
|
+
return self.espera(lambda m: m.get('id') == ident)
|
|
93
|
+
|
|
94
|
+
def cierra(self):
|
|
95
|
+
if self.p.poll() is None:
|
|
96
|
+
self.p.terminate()
|
|
97
|
+
try:
|
|
98
|
+
self.p.wait(timeout=2)
|
|
99
|
+
except subprocess.TimeoutExpired:
|
|
100
|
+
self.p.kill()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def texto(respuesta):
|
|
104
|
+
try:
|
|
105
|
+
return respuesta['result']['content'][0]['text']
|
|
106
|
+
except (KeyError, IndexError, TypeError):
|
|
107
|
+
return ''
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def escribe_ciudad(ruta, ident, slug, repos, roads):
|
|
111
|
+
os.makedirs(ruta)
|
|
112
|
+
open(os.path.join(ruta, 'city.yml'), 'w', encoding='utf-8').write(
|
|
113
|
+
f'id: {ident}\nname: {slug.title()}\nslug: {slug}\nowner: alice\n')
|
|
114
|
+
open(os.path.join(ruta, 'alice.md'), 'w', encoding='utf-8').write(
|
|
115
|
+
f'---\nuser: alice\nagent: alice/ceo\nrepos: [{", ".join(repos)}]\n'
|
|
116
|
+
+ ''.join(f'role.{repo}: seo\n' for repo in repos)
|
|
117
|
+
+ '---\n')
|
|
118
|
+
open(os.path.join(ruta, 'roads.json'), 'w', encoding='utf-8').write(
|
|
119
|
+
json.dumps({'version': 1, 'roads': roads}) + '\n')
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def detiene_hubs(app):
|
|
123
|
+
pids = []
|
|
124
|
+
for carpeta, _, nombres in os.walk(os.path.join(app, '.runtime', 'bus')):
|
|
125
|
+
if 'endpoint.json' not in nombres:
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
endpoint = json.load(open(os.path.join(carpeta, 'endpoint.json'),
|
|
129
|
+
encoding='utf-8'))
|
|
130
|
+
pids.append(int(endpoint['pid']))
|
|
131
|
+
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
|
132
|
+
pass
|
|
133
|
+
results = [detiene_proceso(pid) for pid in pids]
|
|
134
|
+
return all(results)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
print('\n one typed bus, with native Claude Channel delivery')
|
|
139
|
+
if not shutil.which('node'):
|
|
140
|
+
afirma('· node is available for the shipped channel', False, 'node missing')
|
|
141
|
+
return resumen('channel')
|
|
142
|
+
|
|
143
|
+
base = tempfile.mkdtemp(prefix='agents-city-channel-')
|
|
144
|
+
app = os.path.join(base, 'app')
|
|
145
|
+
home = os.path.join(app, 'alice', 'home')
|
|
146
|
+
lab = os.path.join(app, 'alice', 'lab')
|
|
147
|
+
road_lab = {'id': 'city_lab', 'name': 'lab', 'owner': 'alice',
|
|
148
|
+
'address': 'alice/lab', 'local': True}
|
|
149
|
+
road_home = {'id': 'city_home', 'name': 'home', 'owner': 'alice',
|
|
150
|
+
'address': 'alice/home', 'local': True}
|
|
151
|
+
escribe_ciudad(home, 'city_home', 'home', ['api'], [road_lab])
|
|
152
|
+
escribe_ciudad(lab, 'city_lab', 'lab', [], [road_home])
|
|
153
|
+
|
|
154
|
+
standard = a = b = repo = None
|
|
155
|
+
try:
|
|
156
|
+
standard = Cliente('seat', home, app, channel=False)
|
|
157
|
+
afirma('· normal personal-account MCP does not advertise a custom Channel',
|
|
158
|
+
'experimental' not in standard.inicial.get('result', {})
|
|
159
|
+
.get('capabilities', {}), str(standard.inicial))
|
|
160
|
+
standard.cierra()
|
|
161
|
+
standard = None
|
|
162
|
+
a = Cliente('seat', home, app)
|
|
163
|
+
lista = a.herramientas(2)
|
|
164
|
+
nombres = sorted(t['name'] for t in lista.get('result', {}).get('tools', []))
|
|
165
|
+
afirma('· MCP advertises the native Claude Channel capability',
|
|
166
|
+
bool(a.inicial.get('result'))
|
|
167
|
+
and a.inicial.get('result', {}).get('capabilities', {})
|
|
168
|
+
.get('experimental', {}).get('claude/channel') == {},
|
|
169
|
+
'\n'.join(a.err[-3:]))
|
|
170
|
+
endpoint_path = os.path.join(
|
|
171
|
+
app, '.runtime', 'bus', 'city-home', 'endpoint.json')
|
|
172
|
+
hub_started = espera(lambda: os.path.exists(endpoint_path))
|
|
173
|
+
endpoint = json.load(open(endpoint_path, encoding='utf-8')) if hub_started else {}
|
|
174
|
+
executable = subprocess.run(
|
|
175
|
+
['ps', '-p', str(endpoint.get('pid', 0)), '-o', 'command='],
|
|
176
|
+
capture_output=True, text=True).stdout.strip() if endpoint else ''
|
|
177
|
+
afirma('· the production Channel launcher pins the WebSocket hub to Node',
|
|
178
|
+
hub_started and 'node' in executable.lower() and 'local-hub.js' in executable,
|
|
179
|
+
executable)
|
|
180
|
+
comprueba('· the authenticated seat exposes the road trio', nombres,
|
|
181
|
+
['bus_inbox', 'bus_roster', 'bus_send'])
|
|
182
|
+
|
|
183
|
+
repo = Cliente('api', home, app)
|
|
184
|
+
afirma(
|
|
185
|
+
'· the repo MCP context names its operating role without chair authority',
|
|
186
|
+
'operating role is seo' in repo.inicial.get('result', {}).get('instructions', ''),
|
|
187
|
+
str(repo.inicial),
|
|
188
|
+
)
|
|
189
|
+
repo_tools = repo.herramientas(20)
|
|
190
|
+
comprueba('· a repo runtime receives no road capability',
|
|
191
|
+
repo_tools.get('result', {}).get('tools', []), [])
|
|
192
|
+
|
|
193
|
+
opened = subprocess.run(
|
|
194
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
195
|
+
input=json.dumps({
|
|
196
|
+
'question': 'Check the API contract',
|
|
197
|
+
'desiredOutcome': 'One evidence-backed position',
|
|
198
|
+
'definitionOfDone': ['the repo submits its evidence'],
|
|
199
|
+
'participants': ['api'],
|
|
200
|
+
}), capture_output=True, text=True, env=dict(
|
|
201
|
+
os.environ, CITY_BUS_ACTOR='seat', AGENTS_CITY_DATA=home,
|
|
202
|
+
AGENTS_CITY_HOME=app, AGENTS_CITY_USER='alice'), timeout=12)
|
|
203
|
+
state = json.loads(opened.stdout) if opened.returncode == 0 else {}
|
|
204
|
+
thread = state.get('id', '')
|
|
205
|
+
notice = repo.espera(
|
|
206
|
+
lambda m: m.get('method') == 'notifications/claude/channel'
|
|
207
|
+
and m.get('params', {}).get('meta', {}).get('thread') == thread)
|
|
208
|
+
content = notice.get('params', {}).get('content', '')
|
|
209
|
+
afirma('· a repo assignment arrives as a native channel notification',
|
|
210
|
+
bool(thread) and thread in content
|
|
211
|
+
and 'agents-city committee respond' in content,
|
|
212
|
+
opened.stderr.strip() or str(notice))
|
|
213
|
+
meta = notice.get('params', {}).get('meta', {})
|
|
214
|
+
afirma('· channel metadata preserves the authenticated envelope identity',
|
|
215
|
+
meta.get('actor') == 'api'
|
|
216
|
+
and meta.get('envelope_id', '').startswith('msg_'),
|
|
217
|
+
str(notice))
|
|
218
|
+
outbox = os.path.join(app, '.runtime', 'bus', 'city-home', 'outbox', 'api')
|
|
219
|
+
for _ in range(50):
|
|
220
|
+
if not os.path.isdir(outbox) or not os.listdir(outbox):
|
|
221
|
+
break
|
|
222
|
+
time.sleep(.05)
|
|
223
|
+
afirma('· the durable outbox drains only after Channel accepts the event',
|
|
224
|
+
not os.path.isdir(outbox) or not os.listdir(outbox))
|
|
225
|
+
|
|
226
|
+
# Non-happy path: Claude stays open while its local WebSocket hub is
|
|
227
|
+
# forcibly stopped. The next command recreates the hub; the Channel
|
|
228
|
+
# reconnects, receives the durable assignment once, and ACKs it.
|
|
229
|
+
afirma('· the forced outage stops Claude\'s shared WebSocket hub',
|
|
230
|
+
detiene_proceso(int(endpoint['pid'])))
|
|
231
|
+
afirma('· the stale Claude hub endpoint disappears',
|
|
232
|
+
espera(lambda: not os.path.exists(endpoint_path)))
|
|
233
|
+
reopened = subprocess.run(
|
|
234
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
235
|
+
input=json.dumps({
|
|
236
|
+
'question': 'Prove Claude Channel recovery after a bus outage.',
|
|
237
|
+
'desiredOutcome': 'One native Channel delivery after reconnect.',
|
|
238
|
+
'definitionOfDone': ['the api actor receives the retained assignment'],
|
|
239
|
+
'participants': ['api'],
|
|
240
|
+
}), capture_output=True, text=True, env=dict(
|
|
241
|
+
os.environ, CITY_BUS_ACTOR='seat', AGENTS_CITY_DATA=home,
|
|
242
|
+
AGENTS_CITY_HOME=app, AGENTS_CITY_USER='alice'), timeout=12)
|
|
243
|
+
recovery = json.loads(reopened.stdout) if reopened.returncode == 0 else {}
|
|
244
|
+
recovery_thread = recovery.get('id', '')
|
|
245
|
+
recovered_notice = repo.espera(
|
|
246
|
+
lambda m: m.get('method') == 'notifications/claude/channel'
|
|
247
|
+
and m.get('params', {}).get('meta', {}).get('thread') == recovery_thread,
|
|
248
|
+
segundos=12)
|
|
249
|
+
afirma('· Claude Channel reconnects and receives the retained assignment',
|
|
250
|
+
bool(recovery_thread) and bool(recovered_notice),
|
|
251
|
+
reopened.stderr.strip() or str(recovered_notice))
|
|
252
|
+
afirma('· Claude recovery ACKs its durable outbox',
|
|
253
|
+
espera(lambda: not os.path.isdir(outbox) or not os.listdir(outbox)))
|
|
254
|
+
time.sleep(.35)
|
|
255
|
+
recovery_notices = [
|
|
256
|
+
m for m in repo.mensajes
|
|
257
|
+
if m.get('method') == 'notifications/claude/channel'
|
|
258
|
+
and m.get('params', {}).get('meta', {}).get('thread') == recovery_thread
|
|
259
|
+
]
|
|
260
|
+
comprueba('· Claude recovery injects the assignment exactly once',
|
|
261
|
+
len(recovery_notices), 1)
|
|
262
|
+
afirma('· Claude outage recovery never creates a terminal adapter',
|
|
263
|
+
not os.path.exists(os.path.join(
|
|
264
|
+
app, '.runtime', 'bus', 'city-home', 'adapters')))
|
|
265
|
+
|
|
266
|
+
# Non-happy path: an actor can be offline while a committee both opens
|
|
267
|
+
# and reaches a terminal state. Reconnecting must drain those obsolete
|
|
268
|
+
# durable envelopes without waking the model with work that no longer
|
|
269
|
+
# exists (the exact failure that made a closed committee restart).
|
|
270
|
+
repo.cierra()
|
|
271
|
+
repo = None
|
|
272
|
+
stale_marker = 'STALE_CANCELLED_ASSIGNMENT_MUST_NOT_RUN'
|
|
273
|
+
stale_opened = subprocess.run(
|
|
274
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
275
|
+
input=json.dumps({
|
|
276
|
+
'question': stale_marker,
|
|
277
|
+
'desiredOutcome': 'Nothing reaches an agent after cancellation.',
|
|
278
|
+
'definitionOfDone': ['the offline outbox is discarded on reconnect'],
|
|
279
|
+
'participants': ['api'],
|
|
280
|
+
}), capture_output=True, text=True, env=dict(
|
|
281
|
+
os.environ, CITY_BUS_ACTOR='seat', AGENTS_CITY_DATA=home,
|
|
282
|
+
AGENTS_CITY_HOME=app, AGENTS_CITY_USER='alice'), timeout=12)
|
|
283
|
+
stale_state = json.loads(stale_opened.stdout) if stale_opened.returncode == 0 else {}
|
|
284
|
+
stale_thread = stale_state.get('id', '')
|
|
285
|
+
cancelled = subprocess.run(
|
|
286
|
+
['node', CLIENT, 'committee', 'cancel', stale_thread,
|
|
287
|
+
'--reason', 'test cancellation while participant is offline'],
|
|
288
|
+
capture_output=True, text=True, env=dict(
|
|
289
|
+
os.environ, CITY_BUS_ACTOR='seat', AGENTS_CITY_DATA=home,
|
|
290
|
+
AGENTS_CITY_HOME=app, AGENTS_CITY_USER='alice'), timeout=12)
|
|
291
|
+
afirma('· non-happy: terminal work remains durably queued while the agent is offline',
|
|
292
|
+
bool(stale_thread) and cancelled.returncode == 0
|
|
293
|
+
and os.path.isdir(outbox) and len(os.listdir(outbox)) == 2,
|
|
294
|
+
cancelled.stderr or str(os.listdir(outbox) if os.path.isdir(outbox) else []))
|
|
295
|
+
repo = Cliente('api', home, app)
|
|
296
|
+
afirma('· non-happy: reconnect discards every obsolete terminal delivery',
|
|
297
|
+
espera(lambda: not os.path.isdir(outbox) or not os.listdir(outbox)),
|
|
298
|
+
str(os.listdir(outbox) if os.path.isdir(outbox) else []))
|
|
299
|
+
time.sleep(.25)
|
|
300
|
+
stale_notices = [
|
|
301
|
+
m for m in repo.mensajes
|
|
302
|
+
if m.get('method') == 'notifications/claude/channel'
|
|
303
|
+
and m.get('params', {}).get('meta', {}).get('thread') == stale_thread
|
|
304
|
+
]
|
|
305
|
+
comprueba('· non-happy: a cancelled committee never wakes the provider after reconnect',
|
|
306
|
+
stale_notices, [])
|
|
307
|
+
diagnostics_path = os.path.join(
|
|
308
|
+
app, '.runtime', 'bus', 'city-home', 'diagnostics.jsonl')
|
|
309
|
+
diagnostics = open(diagnostics_path, encoding='utf-8').read()
|
|
310
|
+
afirma('· stale drops are durable and contain no committee prompt',
|
|
311
|
+
diagnostics.count('"event":"delivery.stale.dropped"') >= 2
|
|
312
|
+
and stale_thread in diagnostics
|
|
313
|
+
and stale_marker not in diagnostics,
|
|
314
|
+
diagnostics[-1200:])
|
|
315
|
+
|
|
316
|
+
fuera = a.herramienta(3, 'bus_send',
|
|
317
|
+
{'to': 'alice/ghost', 'text': 'not on a road'})
|
|
318
|
+
afirma('· a city cannot send outside its explicit roads',
|
|
319
|
+
fuera.get('result', {}).get('isError') is True
|
|
320
|
+
and 'no road' in texto(fuera), texto(fuera))
|
|
321
|
+
|
|
322
|
+
enviado = a.herramienta(4, 'bus_send',
|
|
323
|
+
{'to': 'alice/lab', 'text': 'hello from home'})
|
|
324
|
+
afirma('· an offline city is durably queued by the same hub',
|
|
325
|
+
not enviado.get('result', {}).get('isError')
|
|
326
|
+
and 'queued on the local bus' in texto(enviado), texto(enviado))
|
|
327
|
+
queued_dir = os.path.join(app, '.runtime', 'bus', 'city-lab', 'road-queue')
|
|
328
|
+
queued_file = os.path.join(queued_dir, os.listdir(queued_dir)[0])
|
|
329
|
+
comprueba('· queue directory and envelope are private',
|
|
330
|
+
(os.stat(queued_dir).st_mode & 0o777,
|
|
331
|
+
os.stat(queued_file).st_mode & 0o777), (0o700, 0o600))
|
|
332
|
+
envelope = json.load(open(queued_file, encoding='utf-8'))
|
|
333
|
+
afirma('· the queue carries the shared typed envelope',
|
|
334
|
+
envelope.get('protocol') == 'agents-city-bus/2'
|
|
335
|
+
and envelope.get('scope') == 'road'
|
|
336
|
+
and envelope.get('from', {}).get('actor') == 'seat'
|
|
337
|
+
and envelope.get('to', {}).get('actor') == 'seat')
|
|
338
|
+
grande = a.herramienta(8, 'bus_send',
|
|
339
|
+
{'to': 'alice/lab', 'text': 'x' * 64_001})
|
|
340
|
+
afirma('· local roads enforce the relay size boundary',
|
|
341
|
+
grande.get('result', {}).get('isError') is True
|
|
342
|
+
and 'too large' in texto(grande), texto(grande))
|
|
343
|
+
|
|
344
|
+
b = Cliente('seat', lab, app)
|
|
345
|
+
inbox = b.herramienta(5, 'bus_inbox')
|
|
346
|
+
afirma('· starting the destination drains the durable road queue',
|
|
347
|
+
'hello from home' in texto(inbox)
|
|
348
|
+
and 'agents-city-bus/2' in texto(inbox), texto(inbox))
|
|
349
|
+
vacio = b.herramienta(6, 'bus_inbox')
|
|
350
|
+
afirma('· reading the road inbox clears it',
|
|
351
|
+
'nothing new' in texto(vacio).lower(), texto(vacio))
|
|
352
|
+
roster = b.herramienta(7, 'bus_roster')
|
|
353
|
+
afirma('· roster is road-scoped and sees the other local hub online',
|
|
354
|
+
'alice/home' in texto(roster)
|
|
355
|
+
and 'alice/ghost' not in texto(roster)
|
|
356
|
+
and '"online": true' in texto(roster), texto(roster))
|
|
357
|
+
road_notices = [m for m in b.mensajes
|
|
358
|
+
if m.get('method') == 'notifications/claude/channel']
|
|
359
|
+
afirma('· a queued road event reaches the seat through Channel exactly once',
|
|
360
|
+
len(road_notices) == 1
|
|
361
|
+
and 'hello from home' in road_notices[0].get('params', {}).get('content', ''),
|
|
362
|
+
str(road_notices))
|
|
363
|
+
before = len(road_notices)
|
|
364
|
+
b.herramienta(9, 'bus_roster')
|
|
365
|
+
time.sleep(.15)
|
|
366
|
+
after = len([m for m in b.mensajes
|
|
367
|
+
if m.get('method') == 'notifications/claude/channel'])
|
|
368
|
+
afirma('· opening MCP status never duplicates a native prompt', before == after)
|
|
369
|
+
finally:
|
|
370
|
+
for cliente in (standard, a, b, repo):
|
|
371
|
+
if cliente:
|
|
372
|
+
cliente.cierra()
|
|
373
|
+
afirma('· Channel cleanup leaves no orphan city hub', detiene_hubs(app))
|
|
374
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
375
|
+
return resumen('channel')
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__ == '__main__':
|
|
379
|
+
sys.exit(main())
|