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,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A 40-agent Claude/Codex city survives fan-out and hub replacement."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
RAIZ = os.path.dirname(AQUI)
|
|
10
|
+
sys.path.insert(0, AQUI)
|
|
11
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
print('\n 40-agent mixed Claude/Codex stress')
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
[sys.executable, os.path.join(RAIZ, 'benchmarks', 'stress', 'run.py'),
|
|
18
|
+
'--agents', '40', '--rounds', '2', '--json'],
|
|
19
|
+
capture_output=True, text=True, timeout=90)
|
|
20
|
+
try:
|
|
21
|
+
report = json.loads(result.stdout)
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
report = {}
|
|
24
|
+
afirma('· the offline 40-agent stress run passes',
|
|
25
|
+
result.returncode == 0 and report.get('status') == 'pass',
|
|
26
|
+
result.stderr.strip() or result.stdout[-1000:])
|
|
27
|
+
comprueba('· the city mixes 20 Claude and 20 Codex actors',
|
|
28
|
+
report.get('mix'),
|
|
29
|
+
{'claude-stream-json': 20, 'codex-app-server-ws': 20})
|
|
30
|
+
comprueba('· both rounds reach all 40 native runtimes',
|
|
31
|
+
[row.get('accepted') for row in report.get('rounds', [])], [40, 40])
|
|
32
|
+
comprueba('· each round keeps exactly one shared WebSocket hub',
|
|
33
|
+
[row.get('hubProcesses') for row in report.get('rounds', [])], [1, 1])
|
|
34
|
+
afirma('· local native acceptance stays inside the stress budget',
|
|
35
|
+
len(report.get('rounds', [])) == 2
|
|
36
|
+
and all(row.get('p95NativeAcceptMs', 99999) < 5_000
|
|
37
|
+
and row.get('maxNativeAcceptMs', 99999) < 10_000
|
|
38
|
+
for row in report['rounds']),
|
|
39
|
+
str(report.get('rounds')))
|
|
40
|
+
comprueba('· 80 acceptances are split across the native transports',
|
|
41
|
+
report.get('transportAcceptances'),
|
|
42
|
+
{'claude-stream-json': 40, 'codex-app-server-ws': 40})
|
|
43
|
+
afirma('· the browser WebSocket represents all 40 actors and survives hub replacement',
|
|
44
|
+
report.get('liveFeed', {}).get('transport')
|
|
45
|
+
== 'authenticated-read-only-websocket'
|
|
46
|
+
and report.get('liveFeed', {}).get('participantsPerRound') == [40, 40]
|
|
47
|
+
and report.get('liveFeed', {}).get('openEvents') == 2
|
|
48
|
+
and report.get('liveFeed', {}).get('survivedHubRestart') is True,
|
|
49
|
+
str(report.get('liveFeed')))
|
|
50
|
+
comprueba('· the live browser feed duplicates no committee event',
|
|
51
|
+
report.get('liveFeed', {}).get('duplicates'), 0)
|
|
52
|
+
comprueba('· stress recovery produces no duplicate delivery',
|
|
53
|
+
report.get('duplicateDeliveries'), 0)
|
|
54
|
+
afirma('· stress never uses terminal injection or leaves queued work',
|
|
55
|
+
report.get('terminalInjection') is False
|
|
56
|
+
and report.get('remainingOutboxes') == 0)
|
|
57
|
+
comprueba('· stress cleanup leaves no runtime or hub process',
|
|
58
|
+
report.get('cleanup'),
|
|
59
|
+
{'remainingHubProcesses': [], 'remainingRuntimeProcesses': 0})
|
|
60
|
+
return resumen('stress')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
sys.exit(main())
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the terminal widgets through a fake window, and check what they return.
|
|
3
|
+
|
|
4
|
+
Why this exists, precisely: the scripted harness in ui.py sets `TTY = False`, so
|
|
5
|
+
every widget answers from AGENTS_CITY_ANSWERS and returns *before* entering its
|
|
6
|
+
curses loop. That covers the wizard's logic and leaves the four widgets never
|
|
7
|
+
executed — and one of them shipped broken for exactly that reason. `elige` did
|
|
8
|
+
`marcadas |= {...}` inside its nested `run()`, which made `marcadas` local to it,
|
|
9
|
+
so the very first frame died on `UnboundLocalError` and the roles screen was
|
|
10
|
+
unreachable in any real terminal.
|
|
11
|
+
|
|
12
|
+
So: no pty, no terminal, no hands. Replace `curses.wrapper` with something that
|
|
13
|
+
calls `run()` against a window that renders nothing and returns a scripted list
|
|
14
|
+
of keystrokes. Every branch of the key handling runs for real.
|
|
15
|
+
|
|
16
|
+
./bin/test-widgets.py
|
|
17
|
+
"""
|
|
18
|
+
import curses
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
+
sys.path.insert(0, os.path.join(os.path.dirname(AQUI), 'plugin', 'scripts'))
|
|
24
|
+
import ui # noqa: E402
|
|
25
|
+
|
|
26
|
+
FALLOS = []
|
|
27
|
+
TECLAS = []
|
|
28
|
+
COMPROBADAS = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Ventana:
|
|
32
|
+
"""Wide enough that nothing wraps, and every drawing call is a no-op. What we
|
|
33
|
+
are testing is what the widget decides, not what it paints."""
|
|
34
|
+
def getmaxyx(self):
|
|
35
|
+
return (40, 110)
|
|
36
|
+
|
|
37
|
+
def getch(self):
|
|
38
|
+
if not TECLAS:
|
|
39
|
+
raise AssertionError('the widget asked for a key after the script ran out '
|
|
40
|
+
'— it is not returning when it should')
|
|
41
|
+
return TECLAS.pop(0)
|
|
42
|
+
|
|
43
|
+
def __getattr__(self, _):
|
|
44
|
+
return lambda *a, **k: None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def preparado():
|
|
48
|
+
"""curses, stubbed down to the parts the widgets touch."""
|
|
49
|
+
ui.TTY = True
|
|
50
|
+
curses.wrapper = lambda fn, *a, **k: fn(Ventana(), *a, **k)
|
|
51
|
+
curses.curs_set = lambda *_: None
|
|
52
|
+
curses.color_pair = lambda _: 0
|
|
53
|
+
ui._pairs = lambda: None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def comprueba(nombre, teclas, llama, espera):
|
|
57
|
+
global TECLAS, COMPROBADAS
|
|
58
|
+
TECLAS = list(teclas)
|
|
59
|
+
COMPROBADAS += 1
|
|
60
|
+
try:
|
|
61
|
+
salida = llama()
|
|
62
|
+
except Exception as e: # noqa: BLE001
|
|
63
|
+
FALLOS.append(f'{nombre}: raised {type(e).__name__}: {e}')
|
|
64
|
+
return
|
|
65
|
+
if salida != espera:
|
|
66
|
+
FALLOS.append(f'{nombre}: got {salida!r}, expected {espera!r}')
|
|
67
|
+
return
|
|
68
|
+
if TECLAS:
|
|
69
|
+
FALLOS.append(f'{nombre}: returned with {len(TECLAS)} keystrokes unread')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
ESPACIO, ENTRA, ESC = ord(' '), 13, 27
|
|
73
|
+
ABAJO, ARRIBA = curses.KEY_DOWN, curses.KEY_UP
|
|
74
|
+
OPS = [('a', 'Alpha', 'first'), ('b', 'Beta', 'second'), ('c', 'Gamma', 'third')]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
preparado()
|
|
79
|
+
|
|
80
|
+
# ── elige: the multi-select, and the one that was broken ────────────────
|
|
81
|
+
comprueba('elige · enter with the preselection',
|
|
82
|
+
[ENTRA], lambda: ui.elige('R', OPS, marcadas=['a']), ['a'])
|
|
83
|
+
comprueba('elige · a takes all', # was UnboundLocalError
|
|
84
|
+
[ord('a'), ENTRA], lambda: ui.elige('R', OPS), ['a', 'b', 'c'])
|
|
85
|
+
comprueba('elige · n drops all', # was UnboundLocalError
|
|
86
|
+
[ord('a'), ord('n'), ENTRA], lambda: ui.elige('R', OPS), [])
|
|
87
|
+
comprueba('elige · space toggles on',
|
|
88
|
+
[ESPACIO, ENTRA], lambda: ui.elige('R', OPS), ['a'])
|
|
89
|
+
comprueba('elige · space toggles off again',
|
|
90
|
+
[ESPACIO, ENTRA], lambda: ui.elige('R', OPS, marcadas=['a']), [])
|
|
91
|
+
comprueba('elige · moves down before picking',
|
|
92
|
+
[ABAJO, ESPACIO, ENTRA], lambda: ui.elige('R', OPS), ['b'])
|
|
93
|
+
comprueba('elige · will not stop below the minimum',
|
|
94
|
+
[ENTRA, ESPACIO, ENTRA], lambda: ui.elige('R', OPS, minimo=1), ['a'])
|
|
95
|
+
comprueba('elige · quitting returns nothing',
|
|
96
|
+
[ESC], lambda: ui.elige('R', OPS), None)
|
|
97
|
+
comprueba('elige · filter narrows what a and space reach',
|
|
98
|
+
[ord('/'), ord('B'), ENTRA, ord('a'), ENTRA],
|
|
99
|
+
lambda: ui.elige('R', OPS), ['b'])
|
|
100
|
+
comprueba('elige · n only drops what the filter shows',
|
|
101
|
+
[ord('a'), ord('/'), ord('B'), ENTRA, ord('n'), ENTRA],
|
|
102
|
+
lambda: ui.elige('R', OPS), ['a', 'c'])
|
|
103
|
+
comprueba('elige · an empty list still returns',
|
|
104
|
+
[ENTRA], lambda: ui.elige('R', []), [])
|
|
105
|
+
|
|
106
|
+
# ── una: the single choice ─────────────────────────────────────────────
|
|
107
|
+
comprueba('una · enter takes the first',
|
|
108
|
+
[ENTRA], lambda: ui.una('K', OPS), 'a')
|
|
109
|
+
comprueba('una · down then enter',
|
|
110
|
+
[ABAJO, ENTRA], lambda: ui.una('K', OPS), 'b')
|
|
111
|
+
comprueba('una · up from the top wraps to the bottom',
|
|
112
|
+
[ARRIBA, ENTRA], lambda: ui.una('K', OPS), 'c')
|
|
113
|
+
comprueba('una · down past the bottom wraps to the top',
|
|
114
|
+
[ABAJO, ABAJO, ABAJO, ENTRA], lambda: ui.una('K', OPS), 'a')
|
|
115
|
+
comprueba('una · an empty list returns nothing instead of dividing by zero',
|
|
116
|
+
[], lambda: ui.una('K', []), None)
|
|
117
|
+
comprueba('una · quitting returns nothing',
|
|
118
|
+
[ESC], lambda: ui.una('K', OPS), None)
|
|
119
|
+
|
|
120
|
+
# ── pantalla: the page of text ─────────────────────────────────────────
|
|
121
|
+
comprueba('pantalla · enter continues',
|
|
122
|
+
[ENTRA], lambda: ui.pantalla('T', 'body\n\nsecond paragraph'), True)
|
|
123
|
+
comprueba('pantalla · q stops',
|
|
124
|
+
[ord('q')], lambda: ui.pantalla('T', 'body'), False)
|
|
125
|
+
|
|
126
|
+
# `pide` reads with input(), not curses, so it has nothing to stub. Its wrapping
|
|
127
|
+
# helper is pure and worth one check: paragraphs survive as blank lines.
|
|
128
|
+
lineas = ui.texto('one two\n\nthree four', 40)
|
|
129
|
+
if '' not in lineas:
|
|
130
|
+
FALLOS.append('texto: paragraph break was lost')
|
|
131
|
+
|
|
132
|
+
if FALLOS:
|
|
133
|
+
print(f'\n {len(FALLOS)} widget failure(s)\n')
|
|
134
|
+
for f in FALLOS:
|
|
135
|
+
print(f' ✗ {f}')
|
|
136
|
+
print()
|
|
137
|
+
return 1
|
|
138
|
+
print(f'\n widgets ok — {COMPROBADAS} checks across elige, una, pantalla and texto\n')
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == '__main__':
|
|
143
|
+
sys.exit(main())
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Agent-first workspaces: legacy repos still work, new agents mount folders.
|
|
3
|
+
|
|
4
|
+
Under a throwaway data dir. The load-bearing cases: a bare `repos:` card
|
|
5
|
+
normalises to one agent per repo (unchanged behaviour), a new `agents:` card
|
|
6
|
+
drives workspaces with mounts, mounts are validated symlinks that resolve to
|
|
7
|
+
their real target for the cage, and re-mounting a name repoints atomically.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
|
|
15
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
RAIZ = os.path.dirname(AQUI)
|
|
17
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
18
|
+
sys.path.insert(0, AQUI)
|
|
19
|
+
|
|
20
|
+
import workspace as ws # noqa: E402
|
|
21
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
22
|
+
|
|
23
|
+
LEGACY = """---
|
|
24
|
+
user: ada
|
|
25
|
+
agent: ada/seat
|
|
26
|
+
repos: [nova, store-service]
|
|
27
|
+
role.nova: po
|
|
28
|
+
role.store-service: security
|
|
29
|
+
runs.store-service: codex
|
|
30
|
+
---
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
NUEVO = """---
|
|
34
|
+
user: ada
|
|
35
|
+
agent: ada/seat
|
|
36
|
+
agents: [writer, chair]
|
|
37
|
+
kind.writer: knowledge
|
|
38
|
+
role.writer: blank
|
|
39
|
+
mounts.writer: [~/docs/handbook]
|
|
40
|
+
kind.chair: coordinator
|
|
41
|
+
runs.chair: claude
|
|
42
|
+
---
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def legacy_subsume():
|
|
47
|
+
base = tempfile.mkdtemp(prefix="agents-city-ws-")
|
|
48
|
+
try:
|
|
49
|
+
ags = ws.agentes(LEGACY, base)
|
|
50
|
+
comprueba("a bare repos card yields one agent per repo", len(ags), 2)
|
|
51
|
+
a = {x.slug: x for x in ags}
|
|
52
|
+
afirma("each legacy agent is marked legacy", all(x.legacy for x in ags))
|
|
53
|
+
comprueba("its kind defaults to code", a["nova"].clase, "code")
|
|
54
|
+
comprueba("its role carries over", a["nova"].rol, "po")
|
|
55
|
+
comprueba("its runtime carries over", a["store-service"].runtime, "codex")
|
|
56
|
+
comprueba("a legacy agent's single mount is its repo name",
|
|
57
|
+
a["nova"].mounts, ["nova"])
|
|
58
|
+
# With a legacy resolver, the workspace becomes the resolved repo path.
|
|
59
|
+
ags2 = ws.agentes(LEGACY, base, resolver_legacy=lambda r: f"/repos/{r}")
|
|
60
|
+
comprueba("the resolver sets the workspace to the repo path",
|
|
61
|
+
{x.slug: x.workspace for x in ags2}["nova"], "/repos/nova")
|
|
62
|
+
finally:
|
|
63
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def nuevo_modelo():
|
|
67
|
+
base = tempfile.mkdtemp(prefix="agents-city-ws-")
|
|
68
|
+
try:
|
|
69
|
+
ags = {x.slug: x for x in ws.agentes(NUEVO, base)}
|
|
70
|
+
comprueba("an agents card yields the declared agents", len(ags), 2)
|
|
71
|
+
afirma("new agents are not legacy", not any(x.legacy for x in ags.values()))
|
|
72
|
+
comprueba("a knowledge agent keeps its kind", ags["writer"].clase, "knowledge")
|
|
73
|
+
comprueba("a coordinator keeps its kind", ags["chair"].clase, "coordinator")
|
|
74
|
+
comprueba("its workspace is under the city's agents dir",
|
|
75
|
+
ags["writer"].workspace, ws.workspace_de(base, "writer"))
|
|
76
|
+
comprueba("declared mounts are read from the card",
|
|
77
|
+
ags["writer"].mounts, ["~/docs/handbook"])
|
|
78
|
+
afirma("como_dict is JSON-shaped",
|
|
79
|
+
set(ags["writer"].como_dict()) >= {"name", "kind", "workspace", "mounts"})
|
|
80
|
+
finally:
|
|
81
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def montajes():
|
|
85
|
+
base = tempfile.mkdtemp(prefix="agents-city-ws-")
|
|
86
|
+
try:
|
|
87
|
+
real = os.path.join(base, "real-docs")
|
|
88
|
+
os.makedirs(real)
|
|
89
|
+
with open(os.path.join(real, "note.md"), "w") as f:
|
|
90
|
+
f.write("x")
|
|
91
|
+
enlace = ws.monta(base, "writer", real)
|
|
92
|
+
afirma("mounting creates a symlink in the workspace", os.path.islink(enlace))
|
|
93
|
+
comprueba("the workspace mounts dir now lists it",
|
|
94
|
+
[e for e, _ in ws.mounts_en_disco(base, "writer")],
|
|
95
|
+
["real-docs"])
|
|
96
|
+
comprueba("the mount resolves to its real target",
|
|
97
|
+
ws.mounts_en_disco(base, "writer")[0][1], os.path.realpath(real))
|
|
98
|
+
|
|
99
|
+
# mount_targets follows the symlink to the real path — what the cage seals.
|
|
100
|
+
agente = ws.agentes(NUEVO, base)[0] # 'writer'
|
|
101
|
+
targets = ws.mount_targets(agente, base)
|
|
102
|
+
afirma("mount_targets returns the resolved real path, not the link",
|
|
103
|
+
os.path.realpath(real) in targets)
|
|
104
|
+
|
|
105
|
+
# Re-mounting the same name repoints atomically, does not error.
|
|
106
|
+
otro = os.path.join(base, "other-docs")
|
|
107
|
+
os.makedirs(otro)
|
|
108
|
+
ws.monta(base, "writer", otro, nombre="real-docs")
|
|
109
|
+
comprueba("re-mounting a name repoints it",
|
|
110
|
+
ws.mounts_en_disco(base, "writer")[0][1], os.path.realpath(otro))
|
|
111
|
+
|
|
112
|
+
afirma("mounting a nonexistent path is refused",
|
|
113
|
+
_rechaza(lambda: ws.monta(base, "writer", os.path.join(base, "ghost"))))
|
|
114
|
+
afirma("unmount removes the link", ws.desmonta(base, "writer", "real-docs"))
|
|
115
|
+
afirma("unmount of nothing returns False", not ws.desmonta(base, "writer", "nope"))
|
|
116
|
+
finally:
|
|
117
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def colisiones_de_basename():
|
|
121
|
+
# Two sources sharing a basename must NOT collapse into one mount.
|
|
122
|
+
base = tempfile.mkdtemp(prefix="agents-city-ws-")
|
|
123
|
+
try:
|
|
124
|
+
a = os.path.join(base, "work", "api")
|
|
125
|
+
b = os.path.join(base, "other", "api")
|
|
126
|
+
os.makedirs(a)
|
|
127
|
+
os.makedirs(b)
|
|
128
|
+
ws.monta(base, "dev", a)
|
|
129
|
+
ws.monta(base, "dev", b)
|
|
130
|
+
destinos = sorted(t for _, t in ws.mounts_en_disco(base, "dev"))
|
|
131
|
+
comprueba("both same-basename mounts survive (no silent overwrite)",
|
|
132
|
+
len(destinos), 2)
|
|
133
|
+
afirma("both real targets are present",
|
|
134
|
+
os.path.realpath(a) in destinos and os.path.realpath(b) in destinos)
|
|
135
|
+
# An uppercase/underscore name is slugged the same way at mount and unmount.
|
|
136
|
+
docs = os.path.join(base, "My_Docs")
|
|
137
|
+
os.makedirs(docs)
|
|
138
|
+
ws.monta(base, "dev", docs, nombre="My_Docs")
|
|
139
|
+
afirma("a mount is removable by the name it was created with",
|
|
140
|
+
ws.desmonta(base, "dev", "My_Docs"))
|
|
141
|
+
# A colon in the resolved path is refused (it would break the pipeline).
|
|
142
|
+
raro = os.path.join(base, "a:b")
|
|
143
|
+
os.makedirs(raro)
|
|
144
|
+
afirma("a colon-containing path is refused", _rechaza(lambda: ws.monta(base, "dev", raro)))
|
|
145
|
+
finally:
|
|
146
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def slugs_colisionados_rechazados():
|
|
150
|
+
base = tempfile.mkdtemp(prefix="agents-city-ws-")
|
|
151
|
+
try:
|
|
152
|
+
chocan = "---\nuser: x\nagents: [store-service, store_service]\n---\n"
|
|
153
|
+
afirma("two agents that slug to one identity are refused",
|
|
154
|
+
_rechaza(lambda: ws.agentes(chocan, base)))
|
|
155
|
+
finally:
|
|
156
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sync_solo_materializados():
|
|
160
|
+
base = tempfile.mkdtemp(prefix="agents-city-ws-")
|
|
161
|
+
try:
|
|
162
|
+
real = os.path.join(base, "real")
|
|
163
|
+
os.makedirs(real)
|
|
164
|
+
card_txt = ("---\nuser: x\nagents: [w]\nkind.w: knowledge\n"
|
|
165
|
+
f"mounts.w: [{real}, {os.path.join(base, 'ghost')}]\n---\n")
|
|
166
|
+
agente = ws.agentes(card_txt, base)[0]
|
|
167
|
+
targets = ws.sincroniza(agente, base)
|
|
168
|
+
comprueba("sync returns only what materialised, not the missing source",
|
|
169
|
+
targets, [os.path.realpath(real)])
|
|
170
|
+
finally:
|
|
171
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _rechaza(fn):
|
|
175
|
+
try:
|
|
176
|
+
fn()
|
|
177
|
+
return False
|
|
178
|
+
except (OSError, ValueError):
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
legacy_subsume()
|
|
183
|
+
nuevo_modelo()
|
|
184
|
+
montajes()
|
|
185
|
+
colisiones_de_basename()
|
|
186
|
+
slugs_colisionados_rechazados()
|
|
187
|
+
sync_solo_materializados()
|
|
188
|
+
sys.exit(resumen("workspace"))
|
package/bin/testlib.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The two assertions and the summary every suite here uses.
|
|
3
|
+
|
|
4
|
+
Four suites carried an identical copy of `comprueba`, `afirma`, the two counters and
|
|
5
|
+
the closing summary. The suites exist to hunt down copies that drift; their own
|
|
6
|
+
helpers should not be the next example.
|
|
7
|
+
|
|
8
|
+
Each suite runs as its own process, so module state is per-suite by construction.
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
FALLOS, HECHAS = [], 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def comprueba(nombre, real, esperado):
|
|
19
|
+
"""Equality, with both sides printed when it is not."""
|
|
20
|
+
global HECHAS
|
|
21
|
+
HECHAS += 1
|
|
22
|
+
if real != esperado:
|
|
23
|
+
FALLOS.append(f'{nombre}\n got {real!r}\n expected {esperado!r}')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def afirma(nombre, condicion, detalle=''):
|
|
27
|
+
"""A plain condition, with whatever context the caller thought to keep."""
|
|
28
|
+
global HECHAS
|
|
29
|
+
HECHAS += 1
|
|
30
|
+
if not condicion:
|
|
31
|
+
FALLOS.append(f'{nombre}{chr(10) + " " + detalle if detalle else ""}')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def detiene_proceso(pid, segundos=2):
|
|
35
|
+
"""Stop one exact test-owned PID and prove it did not become an orphan."""
|
|
36
|
+
if not pid:
|
|
37
|
+
return True
|
|
38
|
+
try:
|
|
39
|
+
os.kill(int(pid), signal.SIGTERM)
|
|
40
|
+
except ProcessLookupError:
|
|
41
|
+
return True
|
|
42
|
+
limite = time.monotonic() + segundos
|
|
43
|
+
while time.monotonic() < limite:
|
|
44
|
+
try:
|
|
45
|
+
os.kill(int(pid), 0)
|
|
46
|
+
except ProcessLookupError:
|
|
47
|
+
return True
|
|
48
|
+
time.sleep(.05)
|
|
49
|
+
try:
|
|
50
|
+
os.kill(int(pid), signal.SIGKILL)
|
|
51
|
+
except ProcessLookupError:
|
|
52
|
+
return True
|
|
53
|
+
time.sleep(.1)
|
|
54
|
+
try:
|
|
55
|
+
os.kill(int(pid), 0)
|
|
56
|
+
return False
|
|
57
|
+
except ProcessLookupError:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def hubs_de_ciudad(datos):
|
|
62
|
+
"""Return only local-hub PIDs whose exact --data argument is test-owned."""
|
|
63
|
+
target = os.path.realpath(datos)
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
['ps', '-axo', 'pid=,command='], capture_output=True, text=True)
|
|
66
|
+
pids = []
|
|
67
|
+
marker = 'local-hub.js --data '
|
|
68
|
+
for line in result.stdout.splitlines():
|
|
69
|
+
if marker not in line:
|
|
70
|
+
continue
|
|
71
|
+
raw_pid, command = line.strip().split(None, 1)
|
|
72
|
+
supplied = command.split(marker, 1)[1].strip()
|
|
73
|
+
if os.path.realpath(supplied) == target:
|
|
74
|
+
pids.append(int(raw_pid))
|
|
75
|
+
return sorted(set(pids))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detiene_hubs_de_ciudad(datos):
|
|
79
|
+
"""Stop every exact test-owned hub, including a hidden startup-race loser."""
|
|
80
|
+
return all(detiene_proceso(pid) for pid in hubs_de_ciudad(datos)) \
|
|
81
|
+
and not hubs_de_ciudad(datos)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resumen(etiqueta):
|
|
85
|
+
"""Print the verdict and return the exit code. `N checks` is the exact phrase
|
|
86
|
+
bin/test greps for, so it lives in one place."""
|
|
87
|
+
print()
|
|
88
|
+
if FALLOS:
|
|
89
|
+
print(f' {len(FALLOS)} of {HECHAS} failed\n')
|
|
90
|
+
for f in FALLOS:
|
|
91
|
+
print(f' ✗ {f}')
|
|
92
|
+
print()
|
|
93
|
+
return 1
|
|
94
|
+
print(f' {etiqueta} ok — {HECHAS} checks\n')
|
|
95
|
+
return 0
|
package/bin/tokens.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report this machine's token spend to the city.
|
|
3
|
+
|
|
4
|
+
A one-line wrapper. The reporter itself ships inside the plugin, because that is
|
|
5
|
+
what gets installed on everybody's machine — keeping a second copy here would
|
|
6
|
+
mean two implementations of the same arithmetic, and one of them would rot.
|
|
7
|
+
|
|
8
|
+
./bin/tokens.py --url https://your-city.workers.dev --push
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
os.execv(sys.executable, [sys.executable,
|
|
13
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
14
|
+
'..', 'plugin', 'scripts', 'tokens.py'), *sys.argv[1:]])
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Deploy the hub. Creates the token KV the first time and writes its id into
|
|
3
|
+
# wrangler.toml.
|
|
4
|
+
#
|
|
5
|
+
# ./scripts/deploy.sh
|
|
6
|
+
#
|
|
7
|
+
# You have to be logged into the right Cloudflare account:
|
|
8
|
+
# npx --yes wrangler@4 login (interactive, opens the browser)
|
|
9
|
+
# o exportar CLOUDFLARE_API_TOKEN con un token que tenga
|
|
10
|
+
# Workers Scripts:Edit · Workers KV Storage:Edit · Account Settings:Read
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
cd "$(dirname "$0")/../worker"
|
|
15
|
+
|
|
16
|
+
# A malformed CF_API_TOKEN in the environment wins over everything else in
|
|
17
|
+
# wrangler, including a perfectly good OAuth session — so it is taken out of the
|
|
18
|
+
# way for this one invocation.
|
|
19
|
+
unset CF_API_TOKEN
|
|
20
|
+
|
|
21
|
+
# The runner: bun if it is here, npx otherwise — npx ships with npm, so it is
|
|
22
|
+
# the one almost everybody already has.
|
|
23
|
+
if command -v bunx >/dev/null 2>&1; then CORRE=(bunx)
|
|
24
|
+
elif command -v bun >/dev/null 2>&1; then CORRE=(bun x)
|
|
25
|
+
else CORRE=(npx --yes); fi
|
|
26
|
+
W() { "${CORRE[@]}" wrangler@4 "$@"; }
|
|
27
|
+
|
|
28
|
+
echo "→ account:"
|
|
29
|
+
W whoami 2>&1 | grep -E "logged in|Account Name|│" | head -5 || true
|
|
30
|
+
echo
|
|
31
|
+
|
|
32
|
+
if grep -qE 'id = "(PENDIENTE_DE_CREAR|<YOUR_KV_NAMESPACE_ID>)"' wrangler.toml; then
|
|
33
|
+
echo "→ creating the token KV..."
|
|
34
|
+
OUT="$(W kv namespace create TOKENS 2>&1)"
|
|
35
|
+
echo "$OUT" | tail -5
|
|
36
|
+
ID="$(printf '%s' "$OUT" | grep -oE '"?id"?[ =:]+"?[a-f0-9]{32}' | grep -oE '[a-f0-9]{32}' | head -1)"
|
|
37
|
+
if [ -z "$ID" ]; then
|
|
38
|
+
echo "I could not find the KV id in wrangler's output." >&2
|
|
39
|
+
echo "Put it into worker/wrangler.toml by hand and run this again." >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
sed -i.bak -E "s/id = \"(PENDIENTE_DE_CREAR|<YOUR_KV_NAMESPACE_ID>)\"/id = \"$ID\"/" wrangler.toml && rm -f wrangler.toml.bak
|
|
43
|
+
echo " KV created: $ID"
|
|
44
|
+
echo
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "→ deploying..."
|
|
48
|
+
W deploy
|
|
49
|
+
|
|
50
|
+
cat <<'EOF'
|
|
51
|
+
|
|
52
|
+
Done. Now one token per owner prefix (it can serve several cities):
|
|
53
|
+
|
|
54
|
+
./scripts/mint-token.sh <user>
|
|
55
|
+
|
|
56
|
+
And check the hub answers:
|
|
57
|
+
|
|
58
|
+
curl https://city-bus.<your-subdomain>.workers.dev/health
|
|
59
|
+
|
|
60
|
+
EOF
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mint a token for one city owner prefix and register it in KV.
|
|
3
|
+
#
|
|
4
|
+
# ./scripts/mint-token.sh alice
|
|
5
|
+
# ./scripts/mint-token.sh alice --local (against wrangler dev)
|
|
6
|
+
# ./scripts/mint-token.sh alice --to bob,carol (may only write to those)
|
|
7
|
+
#
|
|
8
|
+
# It prints the token ONCE: KV keeps only its SHA-256, so a lost token is a
|
|
9
|
+
# reissued token. Hand it over privately — never through the repo.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
USER_NAME="${1:-}"
|
|
14
|
+
[ -z "$USER_NAME" ] && { echo "usage: $0 <user> [--local] [--to a,b]" >&2; exit 1; }
|
|
15
|
+
shift
|
|
16
|
+
|
|
17
|
+
if ! [[ "$USER_NAME" =~ ^[a-z0-9][a-z0-9_-]{0,31}$ ]]; then
|
|
18
|
+
echo "invalid user: lowercase, digits, dashes; no slashes" >&2; exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
REMOTE="--remote"
|
|
22
|
+
CAN_SEND_TO=""
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--local) REMOTE="--local"; shift ;;
|
|
26
|
+
--to) CAN_SEND_TO="${2:-}"; shift 2 ;;
|
|
27
|
+
*) echo "unknown option: $1" >&2; exit 1 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
# The runner: npx ships with npm, so it is the one almost everybody has.
|
|
32
|
+
if command -v bunx >/dev/null 2>&1; then CORRE=(bunx)
|
|
33
|
+
elif command -v bun >/dev/null 2>&1; then CORRE=(bun x)
|
|
34
|
+
elif command -v npx >/dev/null 2>&1; then CORRE=(npx --yes)
|
|
35
|
+
else echo "I need npx (comes with Node) or bun." >&2; exit 1; fi
|
|
36
|
+
|
|
37
|
+
cd "$(dirname "$0")/../worker"
|
|
38
|
+
|
|
39
|
+
TOKEN="rb_$(openssl rand -hex 24)"
|
|
40
|
+
HASH="$(printf '%s' "$TOKEN" | shasum -a 256 | cut -d' ' -f1)"
|
|
41
|
+
|
|
42
|
+
if [ -n "$CAN_SEND_TO" ]; then
|
|
43
|
+
LIST="$(printf '%s' "$CAN_SEND_TO" | awk -F, '{for(i=1;i<=NF;i++) printf "%s\"%s\"", (i>1?",":""), $i}')"
|
|
44
|
+
VALUE="{\"user\":\"$USER_NAME\",\"can_send_to\":[$LIST]}"
|
|
45
|
+
else
|
|
46
|
+
VALUE="{\"user\":\"$USER_NAME\"}"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
"${CORRE[@]}" wrangler@4 kv key put $REMOTE --binding TOKENS "tok:$HASH" "$VALUE" >/dev/null
|
|
50
|
+
|
|
51
|
+
cat <<EOF
|
|
52
|
+
|
|
53
|
+
Token for "$USER_NAME" (shown this once only):
|
|
54
|
+
|
|
55
|
+
$TOKEN
|
|
56
|
+
|
|
57
|
+
They should keep it in the plugin's sensitive configuration or in
|
|
58
|
+
~/.claude/channels/city-bus/.env :
|
|
59
|
+
|
|
60
|
+
CITY_BUS_URL=https://city-bus.<your-subdomain>.workers.dev
|
|
61
|
+
CITY_BUS_TOKEN=$TOKEN
|
|
62
|
+
|
|
63
|
+
Each seat derives its own $USER_NAME/<city> address at runtime.
|
|
64
|
+
|
|
65
|
+
To revoke it:
|
|
66
|
+
npx --yes wrangler@4 kv key delete $REMOTE --binding TOKENS "tok:$HASH"
|
|
67
|
+
|
|
68
|
+
EOF
|