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,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Domain-first onboarding and transparent built-in knowledge packs."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
RAIZ = os.path.dirname(AQUI)
|
|
11
|
+
sys.path.insert(0, AQUI)
|
|
12
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
13
|
+
|
|
14
|
+
import domains # noqa: E402
|
|
15
|
+
import roles # noqa: E402
|
|
16
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def catalogo():
|
|
20
|
+
print(" domain catalogue")
|
|
21
|
+
packs = domains.catalogo()
|
|
22
|
+
ids = [p["id"] for p in packs]
|
|
23
|
+
comprueba("· domain comes before role, with software first", ids[0], "software")
|
|
24
|
+
for requerido in (
|
|
25
|
+
"software",
|
|
26
|
+
"healthcare",
|
|
27
|
+
"legal",
|
|
28
|
+
"finance",
|
|
29
|
+
"marketing",
|
|
30
|
+
"sales",
|
|
31
|
+
"research",
|
|
32
|
+
"operations",
|
|
33
|
+
"custom",
|
|
34
|
+
):
|
|
35
|
+
afirma(f"· {requerido} is a built-in work domain", requerido in ids, str(ids))
|
|
36
|
+
comprueba("· old product cities resolve to software", domains.canonico("product"), "software")
|
|
37
|
+
comprueba("· old blank cities resolve to custom", domains.canonico("blank"), "custom")
|
|
38
|
+
|
|
39
|
+
software = domains.obtiene("software")
|
|
40
|
+
comprueba(
|
|
41
|
+
"· software adds data engineering and blank to its eight established roles",
|
|
42
|
+
len(software["roles"]),
|
|
43
|
+
10,
|
|
44
|
+
)
|
|
45
|
+
afirma(
|
|
46
|
+
"· data engineering is distinct from analytics leadership",
|
|
47
|
+
{"data-engineer", "data"} <= {r for r, _ in software["roles"]},
|
|
48
|
+
repr(software["roles"]),
|
|
49
|
+
)
|
|
50
|
+
healthcare = domains.obtiene("healthcare")
|
|
51
|
+
afirma(
|
|
52
|
+
"· medicine has clinical roles rather than the software list",
|
|
53
|
+
healthcare["roles"][0][0] == "clinical-director"
|
|
54
|
+
and not any(r == "dev" for r, _ in healthcare["roles"]),
|
|
55
|
+
repr(healthcare["roles"]),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
conocidos = {r for r, _, _, _ in roles.CATALOGO}
|
|
59
|
+
for pack in packs:
|
|
60
|
+
afirma(
|
|
61
|
+
f"· {pack['id']} has a substantive knowledge pack",
|
|
62
|
+
"# " in pack["text"] and len(pack["text"]) > 500,
|
|
63
|
+
f"{len(pack['text'])} chars",
|
|
64
|
+
)
|
|
65
|
+
afirma(
|
|
66
|
+
f"· {pack['id']} offers the explicit blank role",
|
|
67
|
+
any(rol == "blank" for rol, _ in pack["roles"]),
|
|
68
|
+
repr(pack["roles"]),
|
|
69
|
+
)
|
|
70
|
+
for rol, _ in pack["roles"]:
|
|
71
|
+
afirma(f"· {pack['id']}/{rol} has catalogue metadata", rol in conocidos, rol)
|
|
72
|
+
ruta = os.path.join(RAIZ, "plugin", "roles", "examples", f"{rol}.md")
|
|
73
|
+
texto = open(ruta, encoding="utf-8").read() if os.path.isfile(ruta) else ""
|
|
74
|
+
if rol == "blank":
|
|
75
|
+
afirma(
|
|
76
|
+
f"· {pack['id']}/blank deliberately ships no knowledge pack",
|
|
77
|
+
not os.path.exists(ruta),
|
|
78
|
+
ruta,
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
afirma(
|
|
82
|
+
f"· {pack['id']}/{rol} has transparent role knowledge",
|
|
83
|
+
"## Domain" in texto and "## What you ask others" in texto,
|
|
84
|
+
ruta,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ciudad():
|
|
89
|
+
print(" city-owned knowledge")
|
|
90
|
+
datos = tempfile.mkdtemp()
|
|
91
|
+
try:
|
|
92
|
+
city_yml = os.path.join(datos, "city.yml")
|
|
93
|
+
open(city_yml, "w").write("id: city_keep\nowner: ana\nkind: product\n")
|
|
94
|
+
comprueba("· a legacy city reads its work domain", domains.de_ciudad(datos), "software")
|
|
95
|
+
domains.selecciona(datos, "healthcare")
|
|
96
|
+
texto = open(city_yml).read()
|
|
97
|
+
afirma(
|
|
98
|
+
"· selecting a domain preserves stable identity metadata",
|
|
99
|
+
"id: city_keep" in texto and "owner: ana" in texto,
|
|
100
|
+
texto,
|
|
101
|
+
)
|
|
102
|
+
afirma("· and writes the explicit canonical field", "domain: healthcare" in texto, texto)
|
|
103
|
+
|
|
104
|
+
hechos = domains.materializa(datos, "healthcare", "clinical-director")
|
|
105
|
+
for relativo in ("domains/healthcare.md", "roles/clinical-director.md", "AGENTS.md"):
|
|
106
|
+
afirma(
|
|
107
|
+
f"· materialises {relativo} inside the city",
|
|
108
|
+
relativo in hechos and os.path.isfile(os.path.join(datos, relativo)),
|
|
109
|
+
repr(hechos),
|
|
110
|
+
)
|
|
111
|
+
instrucciones = open(os.path.join(datos, "AGENTS.md")).read()
|
|
112
|
+
afirma(
|
|
113
|
+
"· runtime instructions keep repo skills outside the pack",
|
|
114
|
+
"Skills remain installed in the referenced repos" in instrucciones,
|
|
115
|
+
instrucciones[:200],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
rol = os.path.join(datos, "roles", "clinical-director.md")
|
|
119
|
+
open(rol, "w").write("my local clinical policy\n")
|
|
120
|
+
comprueba(
|
|
121
|
+
"· selecting the same pack never overwrites city edits",
|
|
122
|
+
domains.materializa(datos, "healthcare", "clinical-director"),
|
|
123
|
+
[],
|
|
124
|
+
)
|
|
125
|
+
comprueba(
|
|
126
|
+
"· the edited role stays exactly as written",
|
|
127
|
+
open(rol).read(),
|
|
128
|
+
"my local clinical policy\n",
|
|
129
|
+
)
|
|
130
|
+
comprueba(
|
|
131
|
+
"· selecting blank adds no hidden role knowledge",
|
|
132
|
+
domains.materializa(datos, "healthcare", "blank"),
|
|
133
|
+
[],
|
|
134
|
+
)
|
|
135
|
+
afirma(
|
|
136
|
+
"· and roles/blank.md truly does not exist",
|
|
137
|
+
not os.path.exists(os.path.join(datos, "roles", "blank.md")),
|
|
138
|
+
)
|
|
139
|
+
finally:
|
|
140
|
+
shutil.rmtree(datos)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
print()
|
|
145
|
+
catalogo()
|
|
146
|
+
ciudad()
|
|
147
|
+
return resumen("domains")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
sys.exit(main())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The evidence vocabulary: what authorises, and what never does.
|
|
3
|
+
|
|
4
|
+
Pure and tiny, but load-bearing: the whole point is that `unknown` and
|
|
5
|
+
`ambiguous` can never be read as consent, so those invariants get a test each.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
RAIZ = os.path.dirname(AQUI)
|
|
13
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
14
|
+
sys.path.insert(0, AQUI)
|
|
15
|
+
|
|
16
|
+
import evidencia as ev # noqa: E402
|
|
17
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def autorizacion():
|
|
21
|
+
afirma("only enforced authorises", ev.autoriza(ev.Evidencia.IMPUESTO))
|
|
22
|
+
afirma("attribution-only does NOT authorise (knowing who != permitted)",
|
|
23
|
+
not ev.autoriza(ev.Evidencia.ATRIBUIDO))
|
|
24
|
+
for estado in (ev.Evidencia.DESCONOCIDO, ev.Evidencia.AMBIGUO,
|
|
25
|
+
ev.Evidencia.NO_SOPORTADO, ev.Evidencia.NO_IMPUESTO):
|
|
26
|
+
afirma(f"{estado} never authorises", not ev.autoriza(estado))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalizacion():
|
|
30
|
+
comprueba("a known wire value round-trips",
|
|
31
|
+
ev.normaliza("enforced"), ev.Evidencia.IMPUESTO)
|
|
32
|
+
comprueba("garbage fails safe to unknown, not to a permissive state",
|
|
33
|
+
ev.normaliza("whatever-the-producer-sent"), ev.Evidencia.DESCONOCIDO)
|
|
34
|
+
afirma("and that fail-safe default never authorises",
|
|
35
|
+
not ev.autoriza(ev.normaliza("garbage")))
|
|
36
|
+
# autoriza must fail CLOSED on a raw unknown string, not raise.
|
|
37
|
+
afirma("autoriza on an unknown wire string returns False, never raises",
|
|
38
|
+
ev.autoriza("not-a-real-state") is False)
|
|
39
|
+
comprueba("an Evidencia passes through unchanged",
|
|
40
|
+
ev.normaliza(ev.Evidencia.AMBIGUO), ev.Evidencia.AMBIGUO)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def serializacion():
|
|
44
|
+
comprueba("str() gives the stable wire value", str(ev.Evidencia.IMPUESTO), "enforced")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
autorizacion()
|
|
48
|
+
normalizacion()
|
|
49
|
+
serializacion()
|
|
50
|
+
sys.exit(resumen("evidencia"))
|
package/bin/test-exit.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The exit: closes what the city started, and nothing else.
|
|
3
|
+
|
|
4
|
+
./bin/test-exit.py
|
|
5
|
+
|
|
6
|
+
The dangerous half of a teardown command is not what it closes — it is what it
|
|
7
|
+
might close by accident: your own tmux session from before lunch, some other
|
|
8
|
+
project's dev server, the very Claude conversation asking for the shutdown. So
|
|
9
|
+
most of this suite asserts survival.
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
RAIZ = os.path.dirname(AQUI)
|
|
20
|
+
sys.path.insert(0, os.path.join(RAIZ, 'plugin', 'scripts'))
|
|
21
|
+
sys.path.insert(0, AQUI)
|
|
22
|
+
from testlib import comprueba, afirma, resumen # noqa: E402
|
|
23
|
+
import apaga # noqa: E402
|
|
24
|
+
import cities # noqa: E402
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def tmux(*args):
|
|
28
|
+
return subprocess.run(['tmux', *args], capture_output=True, text=True).stdout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def vivas():
|
|
32
|
+
return {l.strip() for l in tmux('ls', '-F', '#{session_name}').splitlines() if l.strip()}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
print()
|
|
37
|
+
if not shutil.which('tmux'):
|
|
38
|
+
print(' (tmux not here — skipped)\n exit ok — 0 checks\n')
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
casa = tempfile.mkdtemp()
|
|
42
|
+
# Two fake cities with one card each, and one session per city — plus an
|
|
43
|
+
# unrelated session that must survive everything.
|
|
44
|
+
ciudades = {}
|
|
45
|
+
for nombre in ('zzacme', 'zzbeta'):
|
|
46
|
+
d = os.path.join(casa, f'{nombre}-data')
|
|
47
|
+
os.makedirs(d)
|
|
48
|
+
open(os.path.join(d, 'city.yml'), 'w').write(
|
|
49
|
+
f'kind: product\nname: {nombre}\nslug: {nombre}\nowner: zzfulano\n')
|
|
50
|
+
open(os.path.join(d, 'units.yml'), 'w').write('units:\n')
|
|
51
|
+
open(os.path.join(d, 'zzfulano.md'), 'w').write(
|
|
52
|
+
'---\nuser: zzfulano\nrole: dev\nagent: zzfulano/dev\nrepos: []\n'
|
|
53
|
+
'goals_defined: false\n---\n')
|
|
54
|
+
ciudades[nombre] = d
|
|
55
|
+
sesA = cities.sesion('zzfulano', ciudades['zzacme'])
|
|
56
|
+
sesB = cities.sesion('zzfulano', ciudades['zzbeta'])
|
|
57
|
+
comprueba('· the two cities mint different sessions', sesA != sesB, True)
|
|
58
|
+
|
|
59
|
+
for s in (sesA, sesB, 'zzajena'):
|
|
60
|
+
subprocess.run(['tmux', 'new-session', '-d', '-s', s], capture_output=True)
|
|
61
|
+
# A fake map: a process holding one city's state slice open.
|
|
62
|
+
estadoA = os.path.join(apaga.ESTADO, cities.slug(ciudades['zzacme']))
|
|
63
|
+
os.makedirs(estadoA, exist_ok=True)
|
|
64
|
+
rehen = subprocess.Popen(
|
|
65
|
+
[sys.executable, '-c',
|
|
66
|
+
f'f = open({os.path.join(estadoA, "db")!r}, "w"); import time; time.sleep(300)'])
|
|
67
|
+
fake_hub = os.path.join(casa, 'local-hub.js')
|
|
68
|
+
open(fake_hub, 'w').write('setInterval(() => {}, 1000);\n')
|
|
69
|
+
orphan_a = subprocess.Popen(['node', fake_hub, '--data', ciudades['zzacme']])
|
|
70
|
+
orphan_b = subprocess.Popen(['node', fake_hub, '--data', ciudades['zzbeta']])
|
|
71
|
+
time.sleep(0.8)
|
|
72
|
+
|
|
73
|
+
print(' what a selective exit closes')
|
|
74
|
+
comprueba('· its session', apaga.sesiones_de_la_ciudad(ciudades['zzacme']), [sesA])
|
|
75
|
+
comprueba('· its map-holder', apaga.mapas(ciudades['zzacme']), [rehen.pid])
|
|
76
|
+
afirma('· an unmarked orphan hub is found only by its exact city argv',
|
|
77
|
+
any(row['pid'] == orphan_a.pid
|
|
78
|
+
for row in apaga.runtime_processes.procesos(ciudades['zzacme']))
|
|
79
|
+
and all(row['pid'] != orphan_b.pid
|
|
80
|
+
for row in apaga.runtime_processes.procesos(ciudades['zzacme'])))
|
|
81
|
+
|
|
82
|
+
r = subprocess.run([os.path.join(AQUI, 'exit'), ciudades['zzacme'], '--dry-run'],
|
|
83
|
+
capture_output=True, text=True)
|
|
84
|
+
afirma('· dry-run says, and closes nothing',
|
|
85
|
+
sesA in r.stdout and sesA in vivas() and rehen.poll() is None,
|
|
86
|
+
r.stdout[:200])
|
|
87
|
+
|
|
88
|
+
r = subprocess.run([os.path.join(AQUI, 'exit'), ciudades['zzacme']],
|
|
89
|
+
capture_output=True, text=True)
|
|
90
|
+
time.sleep(0.5)
|
|
91
|
+
quedan = vivas()
|
|
92
|
+
afirma('· closing one city ends its session', sesA not in quedan, str(quedan))
|
|
93
|
+
afirma("· and kills that city's map-holder", rehen.poll() is not None)
|
|
94
|
+
afirma("· and kills that city's unmarked orphan hub", orphan_a.poll() is not None)
|
|
95
|
+
|
|
96
|
+
print(' what must survive it')
|
|
97
|
+
afirma("· the OTHER city's session", sesB in quedan)
|
|
98
|
+
afirma("· the OTHER city's exact hub", orphan_b.poll() is None)
|
|
99
|
+
afirma('· an unrelated tmux session', 'zzajena' in quedan)
|
|
100
|
+
afirma('· and this very test process', True)
|
|
101
|
+
|
|
102
|
+
print(' the edges')
|
|
103
|
+
r = subprocess.run([os.path.join(AQUI, 'exit'), 'no-such-city-xyz'],
|
|
104
|
+
capture_output=True, text=True)
|
|
105
|
+
afirma('· an unknown city refuses and lists the known ones',
|
|
106
|
+
r.returncode == 1 and 'Known here' in r.stderr, r.stderr[:150])
|
|
107
|
+
# Nothing running for that city any more: honest message, exit 0.
|
|
108
|
+
r = subprocess.run([os.path.join(AQUI, 'exit'), ciudades['zzacme']],
|
|
109
|
+
capture_output=True, text=True)
|
|
110
|
+
afirma('· closing an already-closed city says so and succeeds',
|
|
111
|
+
r.returncode == 0 and 'Nothing of the city is running' in r.stdout,
|
|
112
|
+
r.stdout[:150])
|
|
113
|
+
|
|
114
|
+
for s in (sesB, 'zzajena'):
|
|
115
|
+
subprocess.run(['tmux', 'kill-session', '-t', s], capture_output=True)
|
|
116
|
+
if rehen.poll() is None:
|
|
117
|
+
rehen.kill()
|
|
118
|
+
if orphan_a.poll() is None:
|
|
119
|
+
orphan_a.kill()
|
|
120
|
+
if orphan_b.poll() is None:
|
|
121
|
+
orphan_b.kill()
|
|
122
|
+
shutil.rmtree(casa)
|
|
123
|
+
shutil.rmtree(estadoA, ignore_errors=True)
|
|
124
|
+
return resumen('exit')
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__':
|
|
128
|
+
sys.exit(main())
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The Hall protocol contract: frames, sequence gaps, the closed method set.
|
|
3
|
+
|
|
4
|
+
Pure. The load-bearing cases: an unknown method and a malformed write are
|
|
5
|
+
refused at validation; a response carries exactly one of payload/error; the
|
|
6
|
+
per-connection sequence is monotonic and a gap is detectable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
RAIZ = os.path.dirname(AQUI)
|
|
14
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
15
|
+
sys.path.insert(0, AQUI)
|
|
16
|
+
|
|
17
|
+
import hall_protocol as hp # noqa: E402
|
|
18
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def req_valido():
|
|
22
|
+
ok, _ = hp.valida_req({"type": "req", "id": "r1", "method": "cities.snapshot", "params": {}})
|
|
23
|
+
afirma("a well-formed snapshot request validates", ok)
|
|
24
|
+
ok, _ = hp.valida_req(
|
|
25
|
+
{"type": "req", "id": "r2", "method": "seat.write",
|
|
26
|
+
"params": {"mode": "steer", "text": "status?"}})
|
|
27
|
+
afirma("a steer write with text validates", ok)
|
|
28
|
+
ok, _ = hp.valida_req(
|
|
29
|
+
{"type": "req", "id": "r3", "method": "seat.write", "params": {"mode": "note", "text": ""}})
|
|
30
|
+
afirma("a note may be empty (it does not wake the seat)", ok)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def req_rechazado():
|
|
34
|
+
ok, err = hp.valida_req({"type": "req", "id": "r", "method": "os.system", "params": {}})
|
|
35
|
+
afirma("an unknown method is refused", not ok)
|
|
36
|
+
comprueba("with a structured code", err["code"], "unknown_method")
|
|
37
|
+
ok, err = hp.valida_req({"type": "req", "id": "r", "method": "seat.write",
|
|
38
|
+
"params": {"mode": "steer", "text": " "}})
|
|
39
|
+
afirma("a steer write without text is refused", not ok and err["code"] == "bad_frame")
|
|
40
|
+
ok, err = hp.valida_req({"type": "req", "id": "r", "method": "seat.write",
|
|
41
|
+
"params": {"mode": "teleport", "text": "x"}})
|
|
42
|
+
afirma("an unknown write mode is refused", not ok)
|
|
43
|
+
ok, _ = hp.valida_req({"type": "event", "id": "r", "method": "cities.snapshot"})
|
|
44
|
+
afirma("a non-req type is refused by valida_req", not ok)
|
|
45
|
+
ok, _ = hp.valida_req({"type": "req", "method": "cities.snapshot", "params": {}})
|
|
46
|
+
afirma("a request without an id is refused", not ok)
|
|
47
|
+
ok, err = hp.valida_req({"type": "req", "id": "r", "method": "seat.write",
|
|
48
|
+
"params": {"mode": "start", "text": {"x": 1}}})
|
|
49
|
+
afirma("a non-string text is refused, not silently stringified",
|
|
50
|
+
not ok and err["code"] == "bad_frame")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def respuestas():
|
|
54
|
+
r = hp.respuesta("r1", payload={"cities": []})
|
|
55
|
+
afirma("a payload response is ok", r["ok"] and "payload" in r and "error" not in r)
|
|
56
|
+
r = hp.respuesta("r1", err=hp.error("seat_busy", "turn in progress", reintentable=True))
|
|
57
|
+
afirma("an error response is not ok and is retryable",
|
|
58
|
+
not r["ok"] and r["error"]["retryable"])
|
|
59
|
+
try:
|
|
60
|
+
hp.respuesta("r1", payload={}, err=hp.error("bad_frame", "x"))
|
|
61
|
+
afirma("a response cannot carry both payload and error", False)
|
|
62
|
+
except ValueError:
|
|
63
|
+
afirma("a response cannot carry both payload and error", True)
|
|
64
|
+
try:
|
|
65
|
+
hp.error("not_a_code", "x")
|
|
66
|
+
afirma("an error code outside the closed set is refused", False)
|
|
67
|
+
except ValueError:
|
|
68
|
+
afirma("an error code outside the closed set is refused", True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def secuencia():
|
|
72
|
+
seq = hp.Secuencia()
|
|
73
|
+
e1 = seq.evento("presence", {})
|
|
74
|
+
e2 = seq.evento("cities.changed", {})
|
|
75
|
+
comprueba("the sequence starts at 1", e1["seq"], 1)
|
|
76
|
+
comprueba("and increments monotonically", e2["seq"], 2)
|
|
77
|
+
afirma("consecutive seqs show no gap", not hp.Secuencia.hay_hueco(1, 2))
|
|
78
|
+
afirma("a jump is a detectable gap", hp.Secuencia.hay_hueco(1, 5))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def wire():
|
|
82
|
+
frame, err = hp.descodifica(hp.codifica({"type": "event", "seq": 1}))
|
|
83
|
+
afirma("codifica/descodifica round-trip", err is None and frame["seq"] == 1)
|
|
84
|
+
frame, err = hp.descodifica("{not json")
|
|
85
|
+
afirma("malformed wire input fails closed", frame is None and err["code"] == "bad_frame")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
req_valido()
|
|
89
|
+
req_rechazado()
|
|
90
|
+
respuestas()
|
|
91
|
+
secuencia()
|
|
92
|
+
wire()
|
|
93
|
+
sys.exit(resumen("hall-protocol"))
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Real tmux E2E for short launchers: full command and visible failure."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
RAIZ = os.path.dirname(AQUI)
|
|
15
|
+
sys.path.insert(0, AQUI)
|
|
16
|
+
from testlib import afirma, detiene_hubs_de_ciudad, resumen # noqa: E402
|
|
17
|
+
|
|
18
|
+
LAUNCH = os.path.join(RAIZ, 'plugin', 'scripts', 'launch.py')
|
|
19
|
+
LOGS = os.path.join(RAIZ, 'bin', 'logs')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def espera(condition, seconds=8):
|
|
23
|
+
deadline = time.monotonic() + seconds
|
|
24
|
+
while time.monotonic() < deadline:
|
|
25
|
+
if condition():
|
|
26
|
+
return True
|
|
27
|
+
time.sleep(.05)
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read(path):
|
|
32
|
+
try:
|
|
33
|
+
with open(path, encoding='utf-8') as stream:
|
|
34
|
+
return stream.read()
|
|
35
|
+
except OSError:
|
|
36
|
+
return ''
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def json_lines(path):
|
|
40
|
+
try:
|
|
41
|
+
with open(path, encoding='utf-8') as stream:
|
|
42
|
+
return [json.loads(line) for line in stream if line.strip()]
|
|
43
|
+
except (OSError, ValueError):
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_launcher(env, city, actor, cwd, command):
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
[sys.executable, LAUNCH, 'create', '--data', city, '--actor', actor,
|
|
50
|
+
'--cwd', cwd, '--client', os.path.join(RAIZ, 'plugin', 'channel', 'client.js'),
|
|
51
|
+
'--command', command],
|
|
52
|
+
capture_output=True, text=True, env=env, timeout=10)
|
|
53
|
+
return result, result.stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tmux_run(session, cwd, launcher):
|
|
57
|
+
subprocess.run(['tmux', 'new-session', '-d', '-s', session, '-c', cwd],
|
|
58
|
+
check=True, capture_output=True)
|
|
59
|
+
# This is the production contract: one short literal path, then Enter.
|
|
60
|
+
typed = shlex.quote(launcher)
|
|
61
|
+
subprocess.run(['tmux', 'send-keys', '-t', session, '-l', '--', typed], check=True)
|
|
62
|
+
subprocess.run(['tmux', 'send-keys', '-t', session, 'C-m'], check=True)
|
|
63
|
+
return typed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
print('\n short audited launchers through real tmux')
|
|
68
|
+
if not shutil.which('tmux'):
|
|
69
|
+
afirma('· tmux is available', False, 'tmux missing')
|
|
70
|
+
return resumen('launch')
|
|
71
|
+
base = tempfile.mkdtemp(prefix='agents-city-launch-')
|
|
72
|
+
app = os.path.join(base, 'app')
|
|
73
|
+
city = os.path.join(app, 'alice', 'home')
|
|
74
|
+
repo = os.path.join(base, 'repo')
|
|
75
|
+
os.makedirs(city)
|
|
76
|
+
os.makedirs(repo)
|
|
77
|
+
open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
|
|
78
|
+
'id: city_launcher\nname: Home\nslug: home\nowner: alice\n')
|
|
79
|
+
open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
|
|
80
|
+
'{"version": 1, "roads": []}\n')
|
|
81
|
+
open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
|
|
82
|
+
'---\nuser: alice\nagent: alice/ceo\nrepos: [repo]\nrole.repo: engineer\n---\n')
|
|
83
|
+
env = dict(os.environ, AGENTS_CITY_HOME=app, AGENTS_CITY_DATA=city,
|
|
84
|
+
AGENTS_CITY_USER='alice')
|
|
85
|
+
for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
|
|
86
|
+
env.pop(key, None)
|
|
87
|
+
session = 'agents-city-test-launch-' + uuid.uuid4().hex[:10]
|
|
88
|
+
capture = os.path.join(base, 'capture.json')
|
|
89
|
+
fake = os.path.join(base, 'fake-agent.py')
|
|
90
|
+
open(fake, 'w', encoding='utf-8').write(
|
|
91
|
+
'#!/usr/bin/env python3\n'
|
|
92
|
+
'import json, os, sys\n'
|
|
93
|
+
'open(os.environ["CAPTURE"], "w").write(json.dumps({'
|
|
94
|
+
'"args": sys.argv[1:], "actor": os.environ.get("CITY_BUS_ACTOR"), '
|
|
95
|
+
'"role": os.environ.get("CITY_AGENT_ROLE")}))\n')
|
|
96
|
+
os.chmod(fake, 0o755)
|
|
97
|
+
runtime_dir = os.path.join(app, '.runtime', 'bus', 'city-launcher')
|
|
98
|
+
diagnostics = os.path.join(runtime_dir, 'diagnostics.jsonl')
|
|
99
|
+
activity = os.path.join(runtime_dir, 'activity.jsonl')
|
|
100
|
+
try:
|
|
101
|
+
padding = ''.join(': "padding-%04d-%s"; ' % (n, 'x' * 60) for n in range(120))
|
|
102
|
+
command = (
|
|
103
|
+
padding + f'CAPTURE={shlex.quote(capture)} CITY_BUS_ACTOR=repo '
|
|
104
|
+
f'CITY_AGENT_ROLE=engineer {shlex.quote(fake)} --name alice-home-repo '
|
|
105
|
+
'--settings safe --print --input-format stream-json '
|
|
106
|
+
'--output-format stream-json --replay-user-messages')
|
|
107
|
+
made, launcher = create_launcher(env, city, 'repo', repo, command)
|
|
108
|
+
afirma('· happy: a command much longer than the broken tmux input is stored privately',
|
|
109
|
+
made.returncode == 0 and len(command) > 8_000
|
|
110
|
+
and os.path.isfile(launcher)
|
|
111
|
+
and (os.stat(launcher).st_mode & 0o077) == 0,
|
|
112
|
+
made.stderr or launcher)
|
|
113
|
+
typed = tmux_run(session, repo, launcher)
|
|
114
|
+
afirma('· happy: tmux receives only the short launcher path',
|
|
115
|
+
len(typed) < 300 and len(typed) * 20 < len(command),
|
|
116
|
+
f'typed={len(typed)} full={len(command)}')
|
|
117
|
+
afirma('· happy: the complete Claude stream suffix and environment survive',
|
|
118
|
+
espera(lambda: os.path.exists(capture))
|
|
119
|
+
and json.loads(read(capture)).get('args', [])[-5:]
|
|
120
|
+
== ['--input-format', 'stream-json', '--output-format',
|
|
121
|
+
'stream-json', '--replay-user-messages']
|
|
122
|
+
and json.loads(read(capture)).get('actor') == 'repo'
|
|
123
|
+
and json.loads(read(capture)).get('role') == 'engineer',
|
|
124
|
+
read(capture))
|
|
125
|
+
afirma('· happy: launcher start and clean exit are durable diagnostics',
|
|
126
|
+
espera(lambda: 'launch.exited' in read(diagnostics))
|
|
127
|
+
and 'launch.started' in read(diagnostics), read(diagnostics))
|
|
128
|
+
|
|
129
|
+
subprocess.run(['tmux', 'kill-session', '-t', session], capture_output=True)
|
|
130
|
+
session += '-failure'
|
|
131
|
+
failing = os.path.join(base, 'failing-agent.sh')
|
|
132
|
+
open(failing, 'w', encoding='utf-8').write('#!/bin/sh\nexit 23\n')
|
|
133
|
+
os.chmod(failing, 0o755)
|
|
134
|
+
failed_command = (
|
|
135
|
+
'CITY_BUS_ACTOR=repo CITY_BUS_TOKEN=DO_NOT_LOG_THIS_SECRET '
|
|
136
|
+
+ shlex.quote(failing))
|
|
137
|
+
made_failure, failure_launcher = create_launcher(
|
|
138
|
+
env, city, 'repo', repo, failed_command)
|
|
139
|
+
tmux_run(session, repo, failure_launcher)
|
|
140
|
+
afirma('· non-happy: exit 23 is recorded and never hides as a half command',
|
|
141
|
+
made_failure.returncode == 0
|
|
142
|
+
and espera(lambda: any(row.get('event') == 'launch.failed'
|
|
143
|
+
and row.get('message') == 'exit 23'
|
|
144
|
+
for row in json_lines(diagnostics))),
|
|
145
|
+
read(diagnostics))
|
|
146
|
+
afirma('· non-happy: launch failure appears in the same durable live feed',
|
|
147
|
+
espera(lambda: any(row.get('kind') == 'runtime.launch.failed'
|
|
148
|
+
and row.get('actor') == 'repo'
|
|
149
|
+
for row in json_lines(activity))),
|
|
150
|
+
read(activity))
|
|
151
|
+
afirma('· non-happy: diagnostics never persist command credentials',
|
|
152
|
+
'DO_NOT_LOG_THIS_SECRET' not in read(diagnostics), read(diagnostics))
|
|
153
|
+
|
|
154
|
+
logs = subprocess.run([LOGS, '--json', '--lines', '5'], capture_output=True,
|
|
155
|
+
text=True, env=env, timeout=10)
|
|
156
|
+
afirma('· the public logs command exposes both durable streams',
|
|
157
|
+
logs.returncode == 0
|
|
158
|
+
and 'agents-city-diagnostic/1' in logs.stdout
|
|
159
|
+
and 'agents-city-activity/1' in logs.stdout,
|
|
160
|
+
logs.stderr or logs.stdout)
|
|
161
|
+
finally:
|
|
162
|
+
subprocess.run(['tmux', 'kill-session', '-t', session], capture_output=True)
|
|
163
|
+
detiene_hubs_de_ciudad(city)
|
|
164
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
165
|
+
return resumen('launch')
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == '__main__':
|
|
169
|
+
sys.exit(main())
|