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,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Polymorphic growth: a house grows by what its agent actually does.
|
|
3
|
+
|
|
4
|
+
Under a throwaway data dir with a fixed clock (CITY_GROWTH_NOW). The point of
|
|
5
|
+
the suite: a knowledge agent with zero git grows from its documents, a
|
|
6
|
+
coordinator from its decisions, and a code agent uses an injected PR counter —
|
|
7
|
+
so nobody is stuck at an empty lot for lacking a repo.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
|
|
16
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
RAIZ = os.path.dirname(AQUI)
|
|
18
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
19
|
+
sys.path.insert(0, AQUI)
|
|
20
|
+
|
|
21
|
+
import crecimiento as cr # noqa: E402
|
|
22
|
+
import workspace as ws # noqa: E402
|
|
23
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@contextlib.contextmanager
|
|
27
|
+
def _reloj_fijo(epoch="1000000000"):
|
|
28
|
+
"""Pin CITY_GROWTH_NOW, restoring any prior value rather than clobbering it."""
|
|
29
|
+
previo = os.environ.get("CITY_GROWTH_NOW")
|
|
30
|
+
os.environ["CITY_GROWTH_NOW"] = epoch
|
|
31
|
+
try:
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
if previo is None:
|
|
35
|
+
os.environ.pop("CITY_GROWTH_NOW", None)
|
|
36
|
+
else:
|
|
37
|
+
os.environ["CITY_GROWTH_NOW"] = previo
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _agente(nombre, clase, data, mounts=()):
|
|
41
|
+
return ws.Agente(nombre, nombre, "blank", "claude", clase,
|
|
42
|
+
ws.workspace_de(data, nombre), list(mounts), legacy=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def knowledge_crece_sin_git():
|
|
46
|
+
base = tempfile.mkdtemp(prefix="agents-city-grow-")
|
|
47
|
+
try:
|
|
48
|
+
with _reloj_fijo():
|
|
49
|
+
docs = os.path.join(base, "handbook")
|
|
50
|
+
os.makedirs(docs)
|
|
51
|
+
for n in ("a.md", "b.txt", "c.pdf", "ignore.py"):
|
|
52
|
+
with open(os.path.join(docs, n), "w") as f:
|
|
53
|
+
f.write("x")
|
|
54
|
+
os.utime(os.path.join(docs, "a.md"), (1000000000 - 100, 1000000000 - 100)) # recent
|
|
55
|
+
os.utime(os.path.join(docs, "b.txt"), (1, 1)) # old
|
|
56
|
+
ws.crea_workspace(base, "writer")
|
|
57
|
+
ws.monta(base, "writer", docs)
|
|
58
|
+
a = _agente("writer", "knowledge", base)
|
|
59
|
+
r = cr.crece(a, base)
|
|
60
|
+
comprueba("floors count the documents, not the code files", r["floors"], 3)
|
|
61
|
+
comprueba("the signal names documents", r["signal"], "documents")
|
|
62
|
+
afirma("recent edits count as activity", r["activity30"] >= 1)
|
|
63
|
+
finally:
|
|
64
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def knowledge_suma_workspace_y_mounts():
|
|
68
|
+
# Mounting a folder must ADD to the house, never replace the workspace:
|
|
69
|
+
# the `or`-fallback once made an agent's own documents vanish the moment
|
|
70
|
+
# it mounted anything — an empty mount shrank the house to zero.
|
|
71
|
+
base = tempfile.mkdtemp(prefix="agents-city-grow-")
|
|
72
|
+
try:
|
|
73
|
+
with _reloj_fijo():
|
|
74
|
+
ws.crea_workspace(base, "writer")
|
|
75
|
+
taller = ws.workspace_de(base, "writer")
|
|
76
|
+
for n in ("propio.md", "notas.txt"):
|
|
77
|
+
with open(os.path.join(taller, n), "w") as f:
|
|
78
|
+
f.write("x")
|
|
79
|
+
a = _agente("writer", "knowledge", base)
|
|
80
|
+
antes = cr.crece(a, base)["floors"]
|
|
81
|
+
vacio = os.path.join(base, "vacio")
|
|
82
|
+
os.makedirs(vacio)
|
|
83
|
+
ws.monta(base, "writer", vacio)
|
|
84
|
+
r = cr.crece(a, base)
|
|
85
|
+
comprueba("an empty mount never shrinks the house", r["floors"], antes)
|
|
86
|
+
lleno = os.path.join(base, "docs")
|
|
87
|
+
os.makedirs(lleno)
|
|
88
|
+
with open(os.path.join(lleno, "extra.md"), "w") as f:
|
|
89
|
+
f.write("x")
|
|
90
|
+
ws.monta(base, "writer", lleno)
|
|
91
|
+
comprueba("a mounted document adds to the workspace's own",
|
|
92
|
+
cr.crece(a, base)["floors"], antes + 1)
|
|
93
|
+
finally:
|
|
94
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _pon_deliberacion(base, ident, updated_at):
|
|
98
|
+
# The real store shape: deliberations/<id>/state.json, schema-tagged.
|
|
99
|
+
import json
|
|
100
|
+
carpeta = os.path.join(base, "deliberations", ident)
|
|
101
|
+
os.makedirs(carpeta)
|
|
102
|
+
estado = {"schema": "agents-city/deliberation@1", "status": "decided",
|
|
103
|
+
"brief": {"question": "q", "participants": []},
|
|
104
|
+
"decisions": [{"outcome": "go"}], "updatedAt": updated_at}
|
|
105
|
+
with open(os.path.join(carpeta, "state.json"), "w") as f:
|
|
106
|
+
json.dump(estado, f)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def coordinator_crece_por_decisiones():
|
|
110
|
+
# The id must match deliberations.ID_RE: delib_<ts>_<hex>.
|
|
111
|
+
base = tempfile.mkdtemp(prefix="agents-city-grow-")
|
|
112
|
+
try:
|
|
113
|
+
with _reloj_fijo():
|
|
114
|
+
_pon_deliberacion(base, "delib_20260101010101_aaaaaaaa", "2001-09-09T01:00:00+00:00")
|
|
115
|
+
_pon_deliberacion(base, "delib_20260101010102_bbbbbbbb", "2001-09-09T01:00:00+00:00")
|
|
116
|
+
a = _agente("chair", "coordinator", base)
|
|
117
|
+
r = cr.crece(a, base)
|
|
118
|
+
comprueba("real deliberation directories become floors (not always 0)",
|
|
119
|
+
r["floors"], 2)
|
|
120
|
+
comprueba("the signal names decisions", r["signal"], "decisions")
|
|
121
|
+
finally:
|
|
122
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def code_usa_contador_inyectado():
|
|
126
|
+
base = tempfile.mkdtemp(prefix="agents-city-grow-")
|
|
127
|
+
try:
|
|
128
|
+
a = _agente("nova", "code", base)
|
|
129
|
+
comprueba("without a git counter a code house is empty, not crashed",
|
|
130
|
+
cr.crece(a, base)["floors"], 0)
|
|
131
|
+
r = cr.crece(a, base, contador_git=lambda ag, d: (7, 3, 5))
|
|
132
|
+
comprueba("with the injected counter, floors are merged PRs", r["floors"], 7)
|
|
133
|
+
comprueba("bricks are commits", r["bricks"], 3)
|
|
134
|
+
comprueba("the signal names pull requests", r["signal"], "pull-requests")
|
|
135
|
+
finally:
|
|
136
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def forma_uniforme():
|
|
140
|
+
base = tempfile.mkdtemp(prefix="agents-city-grow-")
|
|
141
|
+
try:
|
|
142
|
+
for clase in ("code", "knowledge", "coordinator"):
|
|
143
|
+
r = cr.crece(_agente("x", clase, base), base)
|
|
144
|
+
afirma(f"{clase} returns the uniform shape",
|
|
145
|
+
set(r) == {"floors", "bricks", "activity30", "signal"})
|
|
146
|
+
finally:
|
|
147
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def fecha_naive_es_utc():
|
|
151
|
+
# A naive timestamp must be read as UTC, not local, or recency drifts by the
|
|
152
|
+
# machine's offset. epoch 1e9 == 2001-09-09T01:46:40Z; a naive stamp one
|
|
153
|
+
# minute earlier is recent regardless of the runner's timezone.
|
|
154
|
+
comprueba("a naive ISO timestamp is parsed as UTC",
|
|
155
|
+
round(cr._epoch("2001-09-09T01:45:40")), 999999940)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
knowledge_crece_sin_git()
|
|
159
|
+
knowledge_suma_workspace_y_mounts()
|
|
160
|
+
coordinator_crece_por_decisiones()
|
|
161
|
+
code_usa_contador_inyectado()
|
|
162
|
+
forma_uniforme()
|
|
163
|
+
fecha_naive_es_utc()
|
|
164
|
+
sys.exit(resumen("crecimiento"))
|
package/bin/test-demo.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The account-free product demo, over the same real local WebSocket bus.
|
|
3
|
+
|
|
4
|
+
The happy path runs Aurora's complete committee against an ephemeral city and
|
|
5
|
+
asserts the durable spectator stream. Non-happy checks prove the script refuses
|
|
6
|
+
an invalid roster, a missing Hall spectator and impossible timing without ever
|
|
7
|
+
opening a browser, starting tmux or contacting a model provider.
|
|
8
|
+
"""
|
|
9
|
+
import glob
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import signal
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
RAIZ = os.path.dirname(AQUI)
|
|
22
|
+
SHOW = os.path.join(RAIZ, "demo", "show.py")
|
|
23
|
+
FIXTURE = os.path.join(RAIZ, "demo", "city")
|
|
24
|
+
sys.path.insert(0, AQUI)
|
|
25
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def city(valid=True):
|
|
29
|
+
base = tempfile.mkdtemp(prefix="agents-city-demo-test-")
|
|
30
|
+
data = os.path.join(base, "city")
|
|
31
|
+
app = os.path.join(base, "app")
|
|
32
|
+
shutil.copytree(FIXTURE, data)
|
|
33
|
+
os.makedirs(app)
|
|
34
|
+
if not valid:
|
|
35
|
+
card_path = os.path.join(data, "ada.md")
|
|
36
|
+
card_text = open(card_path, encoding="utf-8").read()
|
|
37
|
+
card_text = card_text.replace(
|
|
38
|
+
"repos: [nova, store-service, telemetry-collector, engine-core, launcher]",
|
|
39
|
+
"repos: [nova, store-service]",
|
|
40
|
+
)
|
|
41
|
+
open(card_path, "w", encoding="utf-8").write(card_text)
|
|
42
|
+
env = dict(
|
|
43
|
+
os.environ,
|
|
44
|
+
AGENTS_CITY_HOME=app,
|
|
45
|
+
AGENTS_CITY_DATA=data,
|
|
46
|
+
AGENTS_CITY_USER="ada",
|
|
47
|
+
CITY_ADDRESS="ada/aurora-games",
|
|
48
|
+
)
|
|
49
|
+
for key in ("CITY_BUS_URL", "CITY_BUS_TOKEN", "CITY_DIR"):
|
|
50
|
+
env.pop(key, None)
|
|
51
|
+
return base, data, app, env
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run(env, *args):
|
|
55
|
+
return subprocess.run(
|
|
56
|
+
[sys.executable, SHOW, *args],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
env=env,
|
|
60
|
+
timeout=25,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def activities(app):
|
|
65
|
+
paths = glob.glob(os.path.join(app, ".runtime", "bus", "*", "activity.jsonl"))
|
|
66
|
+
if not paths:
|
|
67
|
+
return []
|
|
68
|
+
return [json.loads(line) for line in open(paths[0], encoding="utf-8") if line.strip()]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def stop_hubs(app):
|
|
72
|
+
for path in glob.glob(os.path.join(app, ".runtime", "bus", "*", "endpoint.json")):
|
|
73
|
+
try:
|
|
74
|
+
pid = int(json.load(open(path, encoding="utf-8"))["pid"])
|
|
75
|
+
os.kill(pid, signal.SIGTERM)
|
|
76
|
+
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
|
|
77
|
+
pass
|
|
78
|
+
time.sleep(0.1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ordered(actual, expected):
|
|
82
|
+
cursor = 0
|
|
83
|
+
for item in actual:
|
|
84
|
+
if cursor < len(expected) and item == expected[cursor]:
|
|
85
|
+
cursor += 1
|
|
86
|
+
return cursor == len(expected)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
global FIXTURE
|
|
91
|
+
print("\n full Aurora demo on the real local bus")
|
|
92
|
+
roots = []
|
|
93
|
+
try:
|
|
94
|
+
base, data, app, env = city()
|
|
95
|
+
roots.append((base, app))
|
|
96
|
+
result = run(env, "--no-wait", "--step", "0")
|
|
97
|
+
events = activities(app)
|
|
98
|
+
kinds = [event.get("kind") for event in events]
|
|
99
|
+
afirma(
|
|
100
|
+
"· happy: the guided demo completes without a provider account",
|
|
101
|
+
result.returncode == 0 and "Guided committee complete" in result.stdout,
|
|
102
|
+
result.stderr,
|
|
103
|
+
)
|
|
104
|
+
comprueba("· it produces the complete visible conversation", len(events), 22)
|
|
105
|
+
afirma(
|
|
106
|
+
"· every item crossed the authenticated activity protocol",
|
|
107
|
+
all(event.get("protocol") == "agents-city-activity/1" for event in events),
|
|
108
|
+
)
|
|
109
|
+
afirma(
|
|
110
|
+
"· the first blind round reveals three repo agents only after its barrier",
|
|
111
|
+
kinds.count("committee.position.submitted") == 3
|
|
112
|
+
and kinds.count("committee.position.revealed") == 3
|
|
113
|
+
and kinds.index("committee.positions.revealed")
|
|
114
|
+
< kinds.index("committee.position.revealed"),
|
|
115
|
+
str(kinds),
|
|
116
|
+
)
|
|
117
|
+
afirma(
|
|
118
|
+
"· the game speakers are the chair and real repo actors, never fake people",
|
|
119
|
+
{event["actor"] for event in events}
|
|
120
|
+
== {"seat", "nova", "store-service", "telemetry-collector"},
|
|
121
|
+
str(sorted({event["actor"] for event in events})),
|
|
122
|
+
)
|
|
123
|
+
afirma(
|
|
124
|
+
"· two specialists request permission, receive it and speak to the committee",
|
|
125
|
+
kinds.count("committee.floor.requested") == 2
|
|
126
|
+
and kinds.count("committee.floor.granted") == 2
|
|
127
|
+
and kinds.count("committee.floor.spoke") == 2
|
|
128
|
+
and all(
|
|
129
|
+
event.get("target") == "committee"
|
|
130
|
+
for event in events
|
|
131
|
+
if event.get("kind") == "committee.floor.spoke"
|
|
132
|
+
),
|
|
133
|
+
str(kinds),
|
|
134
|
+
)
|
|
135
|
+
afirma(
|
|
136
|
+
"· decision, independent verification and closure stay ordered",
|
|
137
|
+
ordered(
|
|
138
|
+
kinds,
|
|
139
|
+
[
|
|
140
|
+
"committee.opened",
|
|
141
|
+
"committee.positions.revealed",
|
|
142
|
+
"committee.synthesis.published",
|
|
143
|
+
"committee.floor.spoke",
|
|
144
|
+
"committee.decision.recorded",
|
|
145
|
+
"committee.verification.pass",
|
|
146
|
+
"committee.closed",
|
|
147
|
+
],
|
|
148
|
+
),
|
|
149
|
+
str(kinds),
|
|
150
|
+
)
|
|
151
|
+
seqs = [event["seq"] for event in events]
|
|
152
|
+
afirma("· its WebSocket ledger is monotonic and duplicate-free", seqs == list(range(1, 23)))
|
|
153
|
+
# The story deliberately walks the failure lane: a verification that
|
|
154
|
+
# fails, a replan, and only then a verified close.
|
|
155
|
+
afirma(
|
|
156
|
+
"· the first plan fails verification and the chair replans before closing",
|
|
157
|
+
kinds.count("committee.verification.fail") == 1
|
|
158
|
+
and kinds.count("committee.replanned") == 1
|
|
159
|
+
and kinds.index("committee.verification.fail")
|
|
160
|
+
< kinds.index("committee.replanned")
|
|
161
|
+
< kinds.index("committee.verification.pass"),
|
|
162
|
+
str(kinds),
|
|
163
|
+
)
|
|
164
|
+
acts = glob.glob(os.path.join(data, "deliberations", "*", "ACT.md"))
|
|
165
|
+
afirma(
|
|
166
|
+
"· the same demo leaves a human-readable verified act",
|
|
167
|
+
len(acts) == 1
|
|
168
|
+
and "Partidas restauradas" in open(acts[0], encoding="utf-8").read()
|
|
169
|
+
and "## Verification" in open(acts[0], encoding="utf-8").read(),
|
|
170
|
+
str(acts),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
card = open(os.path.join(FIXTURE, "ada.md"), encoding="utf-8").read()
|
|
174
|
+
afirma(
|
|
175
|
+
"· Aurora demonstrates role-per-repo and mixed runtime metadata",
|
|
176
|
+
all(
|
|
177
|
+
token in card
|
|
178
|
+
for token in (
|
|
179
|
+
"role.nova: po",
|
|
180
|
+
"role.store-service: security",
|
|
181
|
+
"runs.nova: claude",
|
|
182
|
+
"runs.store-service: codex",
|
|
183
|
+
"runs.telemetry-collector: opencode",
|
|
184
|
+
"runs.engine-core: kimi",
|
|
185
|
+
)
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Every domain's story plays on its own city, with its own cast. This
|
|
190
|
+
# is also the regression net for agents-first on the bus: the clinic
|
|
191
|
+
# and the firm declare `agents:` (knowledge/coordinator), not `repos:`,
|
|
192
|
+
# so these two runs fail loudly if the roster ever regresses to
|
|
193
|
+
# repos-only.
|
|
194
|
+
fixture_original = FIXTURE
|
|
195
|
+
for fixture, historia, reparto in (
|
|
196
|
+
("clinica", "medicina", {"seat", "urgencias", "laboratorio", "farmacia"}),
|
|
197
|
+
("despacho", "legal", {"seat", "contratos", "litigios", "archivo"}),
|
|
198
|
+
):
|
|
199
|
+
FIXTURE = os.path.join(RAIZ, "demo", fixture)
|
|
200
|
+
dom_base, _, dom_app, dom_env = city()
|
|
201
|
+
roots.append((dom_base, dom_app))
|
|
202
|
+
dom_result = run(dom_env, "--no-wait", "--step", "0", "--story", historia)
|
|
203
|
+
dom_events = activities(dom_app)
|
|
204
|
+
dom_kinds = [event.get("kind") for event in dom_events]
|
|
205
|
+
afirma(
|
|
206
|
+
f"· the {historia} story plays on its own city with agents-first actors",
|
|
207
|
+
dom_result.returncode == 0
|
|
208
|
+
and len(dom_events) == 22
|
|
209
|
+
and {event["actor"] for event in dom_events} == reparto
|
|
210
|
+
and dom_kinds.count("committee.verification.fail") == 1
|
|
211
|
+
and dom_kinds.count("committee.replanned") == 1,
|
|
212
|
+
dom_result.stderr + str(sorted({event["actor"] for event in dom_events})),
|
|
213
|
+
)
|
|
214
|
+
FIXTURE = fixture_original
|
|
215
|
+
|
|
216
|
+
bad_base, _, bad_app, bad_env = city(valid=False)
|
|
217
|
+
roots.append((bad_base, bad_app))
|
|
218
|
+
rejected = run(bad_env, "--no-wait", "--step", "0")
|
|
219
|
+
rejected_events = activities(bad_app)
|
|
220
|
+
afirma(
|
|
221
|
+
"· non-happy: a story cannot impersonate an agent absent from the roster",
|
|
222
|
+
rejected.returncode != 0
|
|
223
|
+
and "not a repo support agent" in rejected.stderr
|
|
224
|
+
and [event.get("kind") for event in rejected_events]
|
|
225
|
+
== ["committee.command.rejected"],
|
|
226
|
+
rejected.stderr + json.dumps(rejected_events),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
wait_base, _, wait_app, wait_env = city()
|
|
230
|
+
roots.append((wait_base, wait_app))
|
|
231
|
+
no_hall = run(wait_env, "--wait-timeout", "0", "--step", "0")
|
|
232
|
+
afirma(
|
|
233
|
+
"· non-happy: autoplay refuses to run before a Hall spectator connects",
|
|
234
|
+
no_hall.returncode != 0
|
|
235
|
+
and "Hall did not connect" in no_hall.stderr
|
|
236
|
+
and not activities(wait_app),
|
|
237
|
+
no_hall.stderr,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
timing = run(env, "--step", "-1")
|
|
241
|
+
afirma(
|
|
242
|
+
"· non-happy: an impossible negative turn delay is rejected at the CLI",
|
|
243
|
+
timing.returncode != 0 and "zero or greater" in timing.stderr,
|
|
244
|
+
timing.stderr,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
invented = run(env, "--no-wait", "--step", "0", "--story", "cocina")
|
|
248
|
+
afirma(
|
|
249
|
+
"· non-happy: a story that does not exist is rejected before touching the bus",
|
|
250
|
+
invented.returncode != 0 and "invalid choice" in invented.stderr,
|
|
251
|
+
invented.stderr,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Two demos may seed at once (one per domain): the seeder must never
|
|
255
|
+
# write a fixed path two runs would overwrite mid-flight.
|
|
256
|
+
semilla = open(os.path.join(RAIZ, "city", "scripts", "seed.py"), encoding="utf-8").read()
|
|
257
|
+
afirma(
|
|
258
|
+
"· concurrent demos cannot clobber each other's seed SQL",
|
|
259
|
+
"mkstemp" in semilla and "/tmp/agents-city-seed.sql" not in semilla,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
help_result = subprocess.run(
|
|
263
|
+
[os.path.join(RAIZ, "bin", "demo"), "--help"],
|
|
264
|
+
capture_output=True,
|
|
265
|
+
text=True,
|
|
266
|
+
timeout=5,
|
|
267
|
+
)
|
|
268
|
+
afirma(
|
|
269
|
+
"· the public demo explains Hall, WebSockets, bubbles and account-free use",
|
|
270
|
+
help_result.returncode == 0
|
|
271
|
+
and all(
|
|
272
|
+
token in help_result.stdout
|
|
273
|
+
for token in ("Town Hall", "WebSocket", "speech bubbles", "No Claude/Codex account")
|
|
274
|
+
),
|
|
275
|
+
help_result.stdout + help_result.stderr,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
launcher = open(os.path.join(RAIZ, "bin", "demo"), encoding="utf-8").read()
|
|
279
|
+
worker = open(
|
|
280
|
+
os.path.join(RAIZ, "city", "worker", "src", "index.ts"), encoding="utf-8"
|
|
281
|
+
).read()
|
|
282
|
+
hall = open(os.path.join(RAIZ, "bin", "serve.py"), encoding="utf-8").read()
|
|
283
|
+
afirma(
|
|
284
|
+
"· the disposable launcher owns and cleans only its temporary runtime",
|
|
285
|
+
"mktemp -d" in launcher
|
|
286
|
+
and "trap cleanup EXIT" in launcher
|
|
287
|
+
and 'AGENTS_CITY_HOME=$DEMO_APP' in launcher
|
|
288
|
+
and 'if [ "$ABRIR" -eq 1 ]; then' in launcher
|
|
289
|
+
and "HALL_ARGS" not in launcher
|
|
290
|
+
and "~/.agents-city/state/demo" not in launcher,
|
|
291
|
+
)
|
|
292
|
+
afirma(
|
|
293
|
+
"· Hall discovers the matching city map instead of the first open map port",
|
|
294
|
+
"/api/identity" in worker
|
|
295
|
+
and "CITY_ID?: string" in worker
|
|
296
|
+
and "identidad == esperada" in hall
|
|
297
|
+
and "range(8787, 8797)" in hall,
|
|
298
|
+
)
|
|
299
|
+
finally:
|
|
300
|
+
for base, app in roots:
|
|
301
|
+
stop_hubs(app)
|
|
302
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
303
|
+
|
|
304
|
+
return resumen("demo")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
sys.exit(main())
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""doctor: an old config is detected, explained, backed up, rewritten — once.
|
|
3
|
+
|
|
4
|
+
Runs against a temp file. The load-bearing cases: migrations are idempotent
|
|
5
|
+
(re-running changes nothing), a dry-run never writes, a real fix leaves a
|
|
6
|
+
backup, and several migrations converge in one pass.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
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 doctor # noqa: E402
|
|
21
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def diagnostico():
|
|
25
|
+
viejo = {"busUrl": "https://x", "gateway": {}}
|
|
26
|
+
pendientes = [m.id for m in doctor.diagnostica(viejo)]
|
|
27
|
+
afirma("the renamed key is detected", "bus-url-renamed-to-roads-url" in pendientes)
|
|
28
|
+
afirma("the missing bind default is detected",
|
|
29
|
+
"gateway-bind-defaults-to-loopback" in pendientes)
|
|
30
|
+
nuevo, _ = doctor.cura(viejo)
|
|
31
|
+
afirma("busUrl becomes roadsUrl", "roadsUrl" in nuevo and "busUrl" not in nuevo)
|
|
32
|
+
comprueba("the value is carried over", nuevo["roadsUrl"], "https://x")
|
|
33
|
+
comprueba("the gateway bind is pinned to loopback", nuevo["gateway"]["bind"], "loopback")
|
|
34
|
+
afirma("the cage default is recorded on", nuevo["security"]["cage"] is True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def configs_malformadas():
|
|
38
|
+
# doctor must not crash on the malformed config it exists to repair.
|
|
39
|
+
for malo in ({"security": None}, {"security": "on"}, {"security": ["x"]}, {"security": 3}):
|
|
40
|
+
try:
|
|
41
|
+
nuevo, _ = doctor.cura(malo)
|
|
42
|
+
afirma(f"a non-dict security ({malo['security']!r}) does not crash cura", True)
|
|
43
|
+
afirma("and such a config is left untouched (no bogus migration)",
|
|
44
|
+
nuevo == malo)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
afirma(f"a non-dict security ({malo['security']!r}) does not crash cura", False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def idempotencia():
|
|
50
|
+
canonico = {"roadsUrl": "https://x", "gateway": {"bind": "loopback"},
|
|
51
|
+
"security": {"cage": True}}
|
|
52
|
+
comprueba("a canonical config needs no migration", doctor.diagnostica(canonico), [])
|
|
53
|
+
_, aplicadas = doctor.cura(canonico)
|
|
54
|
+
comprueba("curing a canonical config changes nothing", aplicadas, [])
|
|
55
|
+
curado, _ = doctor.cura({"busUrl": "u"})
|
|
56
|
+
_, otra_vez = doctor.cura(curado)
|
|
57
|
+
comprueba("a second pass is a no-op (idempotent)", otra_vez, [])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fichero():
|
|
61
|
+
base = tempfile.mkdtemp(prefix="agents-city-doctor-")
|
|
62
|
+
try:
|
|
63
|
+
ruta = os.path.join(base, "openclaw.json")
|
|
64
|
+
with open(ruta, "w") as f:
|
|
65
|
+
json.dump({"busUrl": "https://x", "gateway": {}}, f)
|
|
66
|
+
seco = doctor.cura_fichero(ruta, "T1", dry_run=True)
|
|
67
|
+
afirma("a dry-run reports migrations", seco["migrations"])
|
|
68
|
+
afirma("a dry-run writes nothing", not seco["wrote"] and seco["backup"] is None)
|
|
69
|
+
antes = open(ruta).read()
|
|
70
|
+
afirma("the file is untouched after dry-run", json.loads(antes).get("busUrl") == "https://x")
|
|
71
|
+
|
|
72
|
+
rep = doctor.cura_fichero(ruta, "T1", dry_run=False)
|
|
73
|
+
afirma("a real fix wrote the file", rep["wrote"])
|
|
74
|
+
afirma("a backup was left", os.path.exists(rep["backup"]))
|
|
75
|
+
despues = json.load(open(ruta))
|
|
76
|
+
afirma("the rewritten file is canonical",
|
|
77
|
+
"roadsUrl" in despues and despues["gateway"]["bind"] == "loopback")
|
|
78
|
+
afirma("the backup preserved the original shape",
|
|
79
|
+
json.load(open(rep["backup"])).get("busUrl") == "https://x")
|
|
80
|
+
|
|
81
|
+
rep2 = doctor.cura_fichero(ruta, "T2", dry_run=False)
|
|
82
|
+
afirma("a canonical file needs no second rewrite", not rep2["wrote"])
|
|
83
|
+
finally:
|
|
84
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def preserva_permisos():
|
|
88
|
+
base = tempfile.mkdtemp(prefix="agents-city-doctor-")
|
|
89
|
+
try:
|
|
90
|
+
ruta = os.path.join(base, "openclaw.json")
|
|
91
|
+
with open(ruta, "w") as f:
|
|
92
|
+
json.dump({"busUrl": "https://x", "gateway": {}}, f)
|
|
93
|
+
os.chmod(ruta, 0o600)
|
|
94
|
+
doctor.cura_fichero(ruta, "T1", dry_run=False)
|
|
95
|
+
modo = oct(os.stat(ruta).st_mode & 0o777)
|
|
96
|
+
comprueba("a 0600 config stays 0600 after a rewrite (never widened)", modo, "0o600")
|
|
97
|
+
finally:
|
|
98
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
diagnostico()
|
|
102
|
+
configs_malformadas()
|
|
103
|
+
idempotencia()
|
|
104
|
+
fichero()
|
|
105
|
+
preserva_permisos()
|
|
106
|
+
sys.exit(resumen("doctor"))
|