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,252 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Max/Pro uses native stream-json without Channels or admin policy."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
RAIZ = os.path.dirname(AQUI)
|
|
13
|
+
sys.path.insert(0, AQUI)
|
|
14
|
+
from testlib import afirma, comprueba, detiene_hubs_de_ciudad, resumen # noqa: E402
|
|
15
|
+
|
|
16
|
+
GATEWAY = os.path.join(RAIZ, 'plugin', 'channel', 'runtime-gateway.js')
|
|
17
|
+
CLIENT = os.path.join(RAIZ, 'plugin', 'channel', 'client.js')
|
|
18
|
+
FAKE = os.path.join(RAIZ, 'benchmarks', 'latency', 'fake-claude-cli.mjs')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def espera(condicion, segundos=8):
|
|
22
|
+
limite = time.monotonic() + segundos
|
|
23
|
+
while time.monotonic() < limite:
|
|
24
|
+
if condicion():
|
|
25
|
+
return True
|
|
26
|
+
time.sleep(.05)
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def rows(path):
|
|
31
|
+
if not os.path.exists(path):
|
|
32
|
+
return []
|
|
33
|
+
with open(path, encoding='utf-8') as stream:
|
|
34
|
+
result = []
|
|
35
|
+
for line in stream:
|
|
36
|
+
try:
|
|
37
|
+
result.append(json.loads(line))
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
pass
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def text(path):
|
|
44
|
+
try:
|
|
45
|
+
return open(path, encoding='utf-8', errors='replace').read()
|
|
46
|
+
except OSError:
|
|
47
|
+
return ''
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def stop(process):
|
|
51
|
+
if process and process.poll() is None:
|
|
52
|
+
process.terminate()
|
|
53
|
+
if process:
|
|
54
|
+
try:
|
|
55
|
+
process.wait(timeout=3)
|
|
56
|
+
except subprocess.TimeoutExpired:
|
|
57
|
+
process.kill()
|
|
58
|
+
process.wait(timeout=2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def open_committee(env, question):
|
|
62
|
+
opened = subprocess.run(
|
|
63
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
64
|
+
input=json.dumps({
|
|
65
|
+
'question': question,
|
|
66
|
+
'desiredOutcome': 'One accepted Claude stream turn with evidence.',
|
|
67
|
+
'definitionOfDone': ['claude-agent receives exactly one assignment'],
|
|
68
|
+
'participants': ['claude-agent'],
|
|
69
|
+
}),
|
|
70
|
+
capture_output=True, text=True, timeout=12,
|
|
71
|
+
env=dict(env, CITY_BUS_ACTOR='seat'))
|
|
72
|
+
try:
|
|
73
|
+
return json.loads(opened.stdout).get('id', '')
|
|
74
|
+
except json.JSONDecodeError:
|
|
75
|
+
return ''
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main(): # noqa: C901 - this is one complete process lifecycle
|
|
79
|
+
print('\n Claude native stream-json happy and non-happy paths')
|
|
80
|
+
base = tempfile.mkdtemp(prefix='agents-city-claude-runtime-')
|
|
81
|
+
app = os.path.join(base, 'app')
|
|
82
|
+
city = os.path.join(app, 'alice', 'home')
|
|
83
|
+
repo = os.path.join(base, 'repo')
|
|
84
|
+
fake_bin = os.path.join(base, 'bin')
|
|
85
|
+
os.makedirs(city)
|
|
86
|
+
os.makedirs(repo)
|
|
87
|
+
os.makedirs(fake_bin)
|
|
88
|
+
fake_claude = os.path.join(fake_bin, 'claude')
|
|
89
|
+
os.symlink(FAKE, fake_claude)
|
|
90
|
+
open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
|
|
91
|
+
'id: city_claude_native\nname: Home\nslug: home\nowner: alice\n')
|
|
92
|
+
open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
|
|
93
|
+
'{"version": 1, "roads": []}\n')
|
|
94
|
+
open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
|
|
95
|
+
'---\nuser: alice\nagent: alice/ceo\nrepos: [claude-agent]\n'
|
|
96
|
+
'role.claude-agent: product-manager\nruns.claude-agent: claude\n---\n')
|
|
97
|
+
capture = os.path.join(base, 'claude.jsonl')
|
|
98
|
+
behavior = os.path.join(base, 'behavior')
|
|
99
|
+
open(behavior, 'w', encoding='utf-8').write('healthy\n')
|
|
100
|
+
runtime_dir = os.path.join(app, '.runtime', 'bus', 'city-claude-native')
|
|
101
|
+
status = os.path.join(runtime_dir, 'gateways', 'claude-agent.json')
|
|
102
|
+
pid = os.path.join(runtime_dir, 'gateways', 'claude-agent.pid')
|
|
103
|
+
metrics = os.path.join(runtime_dir, 'runtime-latency.jsonl')
|
|
104
|
+
activity = os.path.join(runtime_dir, 'activity.jsonl')
|
|
105
|
+
outbox = os.path.join(runtime_dir, 'outbox', 'claude-agent')
|
|
106
|
+
log = os.path.join(base, 'gateway.log')
|
|
107
|
+
env = dict(
|
|
108
|
+
os.environ,
|
|
109
|
+
AGENTS_CITY_HOME=app,
|
|
110
|
+
AGENTS_CITY_DATA=city,
|
|
111
|
+
AGENTS_CITY_USER='alice',
|
|
112
|
+
CITY_CLAUDE_FAKE_CAPTURE=capture,
|
|
113
|
+
CITY_CLAUDE_FAKE_BEHAVIOR=behavior,
|
|
114
|
+
CITY_CLAUDE_ACK_TIMEOUT_MS='350',
|
|
115
|
+
CITY_CLAUDE_STARTUP_TIMEOUT_MS='2000',
|
|
116
|
+
)
|
|
117
|
+
for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
|
|
118
|
+
env.pop(key, None)
|
|
119
|
+
processes = []
|
|
120
|
+
|
|
121
|
+
def launch(command=None):
|
|
122
|
+
stream = open(log, 'a+', encoding='utf-8')
|
|
123
|
+
command = command or (
|
|
124
|
+
f'{fake_claude} --model sonnet --channels plugin:city@agents-city '
|
|
125
|
+
'--dangerously-load-development-channels')
|
|
126
|
+
process = subprocess.Popen(
|
|
127
|
+
['node', GATEWAY, '--data', city, '--actor', 'claude-agent',
|
|
128
|
+
'--cwd', repo, '--command', command, '--auto', '1', '--interactive', '0'],
|
|
129
|
+
env=env, stdin=subprocess.DEVNULL, stdout=stream, stderr=stream, text=True)
|
|
130
|
+
processes.append((process, stream))
|
|
131
|
+
return process
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
gateway = launch()
|
|
135
|
+
afirma('· happy: Claude authenticates on the city bus without an account double',
|
|
136
|
+
espera(lambda: os.path.exists(status)), text(log)[-800:])
|
|
137
|
+
state = json.load(open(status, encoding='utf-8')) if os.path.exists(status) else {}
|
|
138
|
+
afirma('· happy: status names stream-json and forbids terminal injection',
|
|
139
|
+
state.get('runtime') == 'claude'
|
|
140
|
+
and state.get('transport') == 'claude-stream-json'
|
|
141
|
+
and state.get('terminalInjection') is False, str(state))
|
|
142
|
+
|
|
143
|
+
thread = open_committee(env, 'Prove personal Claude delivery.')
|
|
144
|
+
afirma('· happy: the chair opens the Claude assignment', bool(thread))
|
|
145
|
+
afirma('· happy: Claude receives one typed assignment over stdin JSONL',
|
|
146
|
+
espera(lambda: len(rows(capture)) == 1), text(log)[-1000:])
|
|
147
|
+
record = rows(capture)[0] if rows(capture) else {}
|
|
148
|
+
prompt = json.dumps(record.get('request', {}), ensure_ascii=False)
|
|
149
|
+
args = record.get('args', [])
|
|
150
|
+
afirma('· happy: assignment preserves committee id and repo operating role',
|
|
151
|
+
thread in prompt and 'product-manager' in prompt, prompt[-600:])
|
|
152
|
+
afirma('· happy: the official persistent input/output contract is selected',
|
|
153
|
+
'--print' in args
|
|
154
|
+
and args[args.index('--input-format') + 1] == 'stream-json'
|
|
155
|
+
and args[args.index('--output-format') + 1] == 'stream-json'
|
|
156
|
+
and '--replay-user-messages' in args, str(args))
|
|
157
|
+
afirma('· happy: repo automation retains its explicit local permission mode',
|
|
158
|
+
'--dangerously-skip-permissions' in args, str(args))
|
|
159
|
+
afirma('· regression: no Channel flag or development bypass reaches Claude',
|
|
160
|
+
'--channels' not in args
|
|
161
|
+
and '--dangerously-load-development-channels' not in args
|
|
162
|
+
and record.get('channelEnabled') == '0'
|
|
163
|
+
and record.get('streamGateway') == '1', str(record))
|
|
164
|
+
afirma('· happy: native replay ACK drains the durable outbox',
|
|
165
|
+
espera(lambda: not os.path.isdir(outbox) or not os.listdir(outbox)))
|
|
166
|
+
afirma('· happy: latency ledger records the real native acceptance',
|
|
167
|
+
espera(lambda: len(rows(metrics)) == 1)
|
|
168
|
+
and rows(metrics)[0].get('transport') == 'claude-stream-json',
|
|
169
|
+
json.dumps(rows(metrics)))
|
|
170
|
+
afirma('· happy: the visible answer joins the semantic committee thread',
|
|
171
|
+
espera(lambda: any(
|
|
172
|
+
row.get('kind') == 'conversation.agent'
|
|
173
|
+
and row.get('thread') == thread
|
|
174
|
+
and 'Fake Claude answer' in row.get('summary', '')
|
|
175
|
+
for row in rows(activity))), json.dumps(rows(activity)[-4:]))
|
|
176
|
+
afirma('· regression: no terminal adapter or managed machine policy is created',
|
|
177
|
+
not os.path.exists(os.path.join(runtime_dir, 'adapters'))
|
|
178
|
+
and not os.path.exists(os.path.join(base, 'Library', 'Application Support',
|
|
179
|
+
'ClaudeCode', 'managed-settings.json')))
|
|
180
|
+
stop(gateway)
|
|
181
|
+
afirma('· happy: graceful exit removes Claude gateway identity',
|
|
182
|
+
espera(lambda: not os.path.exists(status) and not os.path.exists(pid)))
|
|
183
|
+
|
|
184
|
+
# Provider rejection must not become a false bus ACK. The durable task is
|
|
185
|
+
# accepted only after a healthy process replays the exact user message.
|
|
186
|
+
open(behavior, 'w', encoding='utf-8').write('reject-prompt\n')
|
|
187
|
+
rejected = launch()
|
|
188
|
+
afirma('· non-happy: rejected provider still joins before work arrives',
|
|
189
|
+
espera(lambda: os.path.exists(status)), text(log)[-600:])
|
|
190
|
+
rejected_thread = open_committee(env, 'Retain a provider-rejected assignment.')
|
|
191
|
+
afirma('· non-happy: Claude rejection is visible in diagnostics',
|
|
192
|
+
espera(lambda: 'fake Claude rejected the prompt' in text(log)), text(log)[-800:])
|
|
193
|
+
comprueba('· non-happy: rejection writes no false native metric', len(rows(metrics)), 1)
|
|
194
|
+
afirma('· non-happy: rejected work remains exactly once in the actor outbox',
|
|
195
|
+
espera(lambda: os.path.isdir(outbox) and len(os.listdir(outbox)) == 1),
|
|
196
|
+
str(os.listdir(outbox) if os.path.isdir(outbox) else []))
|
|
197
|
+
stop(rejected)
|
|
198
|
+
espera(lambda: not os.path.exists(status))
|
|
199
|
+
|
|
200
|
+
open(behavior, 'w', encoding='utf-8').write('healthy\n')
|
|
201
|
+
recovered = launch()
|
|
202
|
+
afirma('· recovery: a fresh stream accepts the retained assignment',
|
|
203
|
+
espera(lambda: any(
|
|
204
|
+
row.get('behavior') == 'healthy'
|
|
205
|
+
and rejected_thread in json.dumps(row)
|
|
206
|
+
for row in rows(capture))), text(log)[-1000:])
|
|
207
|
+
afirma('· recovery: only native acceptance drains retained work',
|
|
208
|
+
espera(lambda: (not os.path.isdir(outbox) or not os.listdir(outbox))
|
|
209
|
+
and len(rows(metrics)) == 2), json.dumps(rows(metrics)))
|
|
210
|
+
stop(recovered)
|
|
211
|
+
espera(lambda: not os.path.exists(status))
|
|
212
|
+
|
|
213
|
+
# A silent/broken provider causes a bounded gateway failure. It leaves
|
|
214
|
+
# the envelope durable and clears online identity instead of pretending.
|
|
215
|
+
open(behavior, 'w', encoding='utf-8').write('no-ack\n')
|
|
216
|
+
silent = launch()
|
|
217
|
+
afirma('· non-happy: silent provider starts before its first assignment',
|
|
218
|
+
espera(lambda: os.path.exists(status)), text(log)[-500:])
|
|
219
|
+
silent_thread = open_committee(env, 'Timeout a stream that never ACKs.')
|
|
220
|
+
afirma('· non-happy: missing native replay stops the failed gateway visibly',
|
|
221
|
+
bool(silent_thread)
|
|
222
|
+
and espera(lambda: silent.poll() is not None
|
|
223
|
+
and not os.path.exists(status) and not os.path.exists(pid)),
|
|
224
|
+
text(log)[-1000:])
|
|
225
|
+
afirma('· non-happy: timed-out work remains durable and unmeasured',
|
|
226
|
+
os.path.isdir(outbox) and len(os.listdir(outbox)) == 1
|
|
227
|
+
and len(rows(metrics)) == 2,
|
|
228
|
+
f'outbox={os.listdir(outbox) if os.path.isdir(outbox) else []} '
|
|
229
|
+
f'metrics={rows(metrics)}')
|
|
230
|
+
|
|
231
|
+
missing = subprocess.run(
|
|
232
|
+
['node', GATEWAY, '--data', city, '--actor', 'claude-agent',
|
|
233
|
+
'--cwd', repo, '--command', os.path.join(base, 'missing', 'claude'),
|
|
234
|
+
'--auto', '1', '--interactive', '0'],
|
|
235
|
+
env=env, capture_output=True, text=True, timeout=5)
|
|
236
|
+
afirma('· non-happy: missing Claude CLI fails closed with an actionable error',
|
|
237
|
+
missing.returncode != 0 and 'could not start Claude Code' in missing.stderr,
|
|
238
|
+
missing.stderr[-800:])
|
|
239
|
+
afirma('· non-happy: missing CLI leaves no status, PID or terminal fallback',
|
|
240
|
+
not os.path.exists(status) and not os.path.exists(pid)
|
|
241
|
+
and not os.path.exists(os.path.join(runtime_dir, 'adapters')))
|
|
242
|
+
finally:
|
|
243
|
+
for process, stream in processes:
|
|
244
|
+
stop(process)
|
|
245
|
+
stream.close()
|
|
246
|
+
afirma('· cleanup: the test leaves no city hub process', detiene_hubs_de_ciudad(city))
|
|
247
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
248
|
+
return resumen('claude-runtime')
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == '__main__':
|
|
252
|
+
sys.exit(main())
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Protocol invariants for the chaired, multimodel city committee."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
RAIZ = os.path.dirname(AQUI)
|
|
14
|
+
sys.path.insert(0, AQUI)
|
|
15
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
16
|
+
|
|
17
|
+
CLIENT = os.path.join(RAIZ, 'plugin', 'channel', 'client.js')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ejecuta(env, actor, verb, thread='', payload=None):
|
|
21
|
+
args = ['node', CLIENT, 'committee', verb]
|
|
22
|
+
if thread:
|
|
23
|
+
args.append(thread)
|
|
24
|
+
entrada = None
|
|
25
|
+
if payload is not None:
|
|
26
|
+
args += ['--input', '-']
|
|
27
|
+
entrada = json.dumps(payload)
|
|
28
|
+
entorno = dict(env, CITY_BUS_ACTOR=actor)
|
|
29
|
+
return subprocess.run(args, input=entrada, capture_output=True, text=True,
|
|
30
|
+
env=entorno, timeout=12)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def valor(resultado):
|
|
34
|
+
try:
|
|
35
|
+
return json.loads(resultado.stdout)
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def para_hub(app):
|
|
41
|
+
raiz = os.path.join(app, '.runtime', 'bus')
|
|
42
|
+
for carpeta, _, nombres in os.walk(raiz):
|
|
43
|
+
if 'endpoint.json' not in nombres:
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
endpoint = json.load(open(os.path.join(carpeta, 'endpoint.json'),
|
|
47
|
+
encoding='utf-8'))
|
|
48
|
+
os.kill(int(endpoint['pid']), signal.SIGTERM)
|
|
49
|
+
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
|
50
|
+
pass
|
|
51
|
+
time.sleep(.15)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
print('\n chaired committee protocol')
|
|
56
|
+
base = tempfile.mkdtemp(prefix='agents-city-committee-')
|
|
57
|
+
app = os.path.join(base, 'app')
|
|
58
|
+
city = os.path.join(app, 'alice', 'home')
|
|
59
|
+
os.makedirs(city)
|
|
60
|
+
open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
|
|
61
|
+
'id: city_committee\nname: Home\nslug: home\nowner: alice\n')
|
|
62
|
+
open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
|
|
63
|
+
'{"version": 1, "roads": []}\n')
|
|
64
|
+
open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
|
|
65
|
+
'---\nuser: alice\nagent: alice/ceo\n'
|
|
66
|
+
'repos: [api, web, ops, ai]\n'
|
|
67
|
+
'role.api: data-engineer\nrole.web: seo\n'
|
|
68
|
+
'role.ops: quality\nrole.ai: blank\n'
|
|
69
|
+
'runs.api: claude\nruns.web: codex\n'
|
|
70
|
+
'runs.ops: opencode\nruns.ai: kimi\n---\n')
|
|
71
|
+
env = dict(os.environ, AGENTS_CITY_HOME=app, AGENTS_CITY_DATA=city,
|
|
72
|
+
AGENTS_CITY_USER='alice')
|
|
73
|
+
for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
|
|
74
|
+
env.pop(key, None)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
brief = {
|
|
78
|
+
'question': 'Do we ship the migration?',
|
|
79
|
+
'desiredOutcome': 'A verified go or no-go',
|
|
80
|
+
'context': 'Release candidate 7',
|
|
81
|
+
'definitionOfDone': ['tests reproduced', 'rollback named'],
|
|
82
|
+
'participants': ['api', 'web'],
|
|
83
|
+
'authority': 'decide',
|
|
84
|
+
'maxRebuttals': 2,
|
|
85
|
+
}
|
|
86
|
+
forbidden = ejecuta(env, 'api', 'open', payload=brief)
|
|
87
|
+
afirma('· only the seat can open a committee',
|
|
88
|
+
forbidden.returncode != 0 and 'only the city chair' in forbidden.stderr,
|
|
89
|
+
forbidden.stderr.strip())
|
|
90
|
+
|
|
91
|
+
opened = ejecuta(env, 'seat', 'open', payload=brief)
|
|
92
|
+
state = valor(opened)
|
|
93
|
+
afirma('· a complete chair brief opens',
|
|
94
|
+
opened.returncode == 0 and state.get('progress', {}).get('status') == 'collecting',
|
|
95
|
+
opened.stderr.strip())
|
|
96
|
+
thread = state.get('id', '')
|
|
97
|
+
afirma('· it selects only relevant repo agents',
|
|
98
|
+
state.get('brief', {}).get('participants') == ['api', 'web'], str(state))
|
|
99
|
+
comprueba(
|
|
100
|
+
'· the act records each participant specialty separately from authority',
|
|
101
|
+
state.get('participantRoles'),
|
|
102
|
+
{'api': 'data-engineer', 'web': 'seo'},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
api_position = {
|
|
106
|
+
'stance': 'conditional',
|
|
107
|
+
'recommendation': 'ship only after replaying the migration',
|
|
108
|
+
'evidence': ['api:test_migration passed at commit abc'],
|
|
109
|
+
'expectedImpact': 'zero failed upgrades',
|
|
110
|
+
'visibleWhen': 'staging replay completes',
|
|
111
|
+
'withdrawIf': 'the replay fails',
|
|
112
|
+
}
|
|
113
|
+
first = ejecuta(env, 'api', 'respond', thread, api_position)
|
|
114
|
+
afirma('· a selected specialist submits one isolated position',
|
|
115
|
+
first.returncode == 0, first.stderr.strip())
|
|
116
|
+
early_chair = valor(ejecuta(env, 'seat', 'show', thread))
|
|
117
|
+
comprueba('· early content is hidden even from the chair',
|
|
118
|
+
early_chair.get('positions', {}).get('api'), 'received-hidden')
|
|
119
|
+
afirma('· the chair view does not leak the recommendation',
|
|
120
|
+
api_position['recommendation'] not in json.dumps(early_chair))
|
|
121
|
+
early_peer = valor(ejecuta(env, 'web', 'show', thread))
|
|
122
|
+
afirma('· another member sees neither its peer position nor a positions map',
|
|
123
|
+
early_peer.get('myPosition') is None
|
|
124
|
+
and 'positions' not in early_peer
|
|
125
|
+
and api_position['recommendation'] not in json.dumps(early_peer))
|
|
126
|
+
|
|
127
|
+
duplicate = ejecuta(env, 'api', 'respond', thread, api_position)
|
|
128
|
+
afirma('· the same agent cannot anchor with repeated positions',
|
|
129
|
+
duplicate.returncode != 0 and 'already submitted' in duplicate.stderr)
|
|
130
|
+
web_position = {
|
|
131
|
+
'stance': 'support',
|
|
132
|
+
'recommendation': 'ship with the tested rollback banner',
|
|
133
|
+
'evidence': ['web:e2e upgrade and rollback passed'],
|
|
134
|
+
}
|
|
135
|
+
second = ejecuta(env, 'web', 'respond', thread, web_position)
|
|
136
|
+
afirma('· the barrier opens only after every selected position arrives',
|
|
137
|
+
valor(second).get('progress', {}).get('status') == 'review')
|
|
138
|
+
revealed = valor(ejecuta(env, 'seat', 'show', thread))
|
|
139
|
+
afirma('· then the chair receives all positions together',
|
|
140
|
+
revealed.get('positions', {}).get('api', {}).get('recommendation')
|
|
141
|
+
== api_position['recommendation']
|
|
142
|
+
and revealed.get('positions', {}).get('web', {}).get('recommendation')
|
|
143
|
+
== web_position['recommendation'])
|
|
144
|
+
|
|
145
|
+
premature = ejecuta(env, 'seat', 'decide', thread, {
|
|
146
|
+
'outcome': 'ship', 'rationale': 'both support it', 'owner': 'alice',
|
|
147
|
+
'executor': 'seat', 'verifier': 'ops',
|
|
148
|
+
'verificationQuestion': 'does replay pass?',
|
|
149
|
+
'selectedEvidence': ['two test runs'], 'reopenIf': ['replay fails'],
|
|
150
|
+
})
|
|
151
|
+
afirma('· the CEO cannot skip synthesis and jump to a decision',
|
|
152
|
+
premature.returncode != 0 and 'publish a synthesis' in premature.stderr)
|
|
153
|
+
synthesis = ejecuta(env, 'seat', 'synthesize', thread, {
|
|
154
|
+
'summary': 'Both paths support a conditional release',
|
|
155
|
+
'agreements': ['replay first'], 'conflicts': ['banner timing'],
|
|
156
|
+
'unknowns': ['production lock duration'],
|
|
157
|
+
})
|
|
158
|
+
afirma('· the chair publishes one integration, not a vote',
|
|
159
|
+
valor(synthesis).get('progress', {}).get('status') == 'deliberating')
|
|
160
|
+
|
|
161
|
+
bad_basis = ejecuta(env, 'api', 'floor-request', thread, {
|
|
162
|
+
'basis': 'opinion', 'reason': 'I disagree', 'evidence': ['none'],
|
|
163
|
+
})
|
|
164
|
+
afirma('· opinion alone cannot take the floor',
|
|
165
|
+
bad_basis.returncode != 0 and 'basis must be one of' in bad_basis.stderr)
|
|
166
|
+
requested = valor(ejecuta(env, 'api', 'floor-request', thread, {
|
|
167
|
+
'basis': 'new_evidence', 'reason': 'staging replay just completed',
|
|
168
|
+
'evidence': ['run 482 passed'],
|
|
169
|
+
}))
|
|
170
|
+
req_id = requested.get('myFloorRequests', [{}])[-1].get('id', '')
|
|
171
|
+
afirma('· a member requests, rather than takes, the floor', bool(req_id))
|
|
172
|
+
self_grant = ejecuta(env, 'web', 'floor-grant', thread, {'requestId': req_id})
|
|
173
|
+
afirma('· a repo agent cannot grant another repo agent a turn',
|
|
174
|
+
self_grant.returncode != 0 and 'only the city chair' in self_grant.stderr)
|
|
175
|
+
granted = ejecuta(env, 'seat', 'floor-grant', thread, {'requestId': req_id})
|
|
176
|
+
afirma('· the chair grants exactly one active reply', granted.returncode == 0)
|
|
177
|
+
wrong_reply = ejecuta(env, 'web', 'reply', thread, {
|
|
178
|
+
'claim': 'my reply', 'evidence': ['web log'], 'consequence': 'none',
|
|
179
|
+
})
|
|
180
|
+
afirma('· an ungranted agent cannot interrupt',
|
|
181
|
+
wrong_reply.returncode != 0 and 'has not granted' in wrong_reply.stderr)
|
|
182
|
+
reply = ejecuta(env, 'api', 'reply', thread, {
|
|
183
|
+
'claim': 'the migration replay now passes',
|
|
184
|
+
'evidence': ['run 482 immutable log'], 'consequence': 'release gate is satisfied',
|
|
185
|
+
})
|
|
186
|
+
web_outbox = os.path.join(
|
|
187
|
+
app, '.runtime', 'bus', 'city-committee', 'outbox', 'web')
|
|
188
|
+
web_deliveries = []
|
|
189
|
+
for name in os.listdir(web_outbox):
|
|
190
|
+
if name.endswith('.json'):
|
|
191
|
+
web_deliveries.append(json.load(open(
|
|
192
|
+
os.path.join(web_outbox, name), encoding='utf-8')))
|
|
193
|
+
heard = next((item for item in web_deliveries
|
|
194
|
+
if item.get('kind') == 'committee.reply.heard'), {})
|
|
195
|
+
afirma('· granted evidence reaches the chair and is heard by every peer',
|
|
196
|
+
reply.returncode == 0
|
|
197
|
+
and heard.get('payload', {}).get('reply', {}).get('claim')
|
|
198
|
+
== 'the migration replay now passes', json.dumps(heard))
|
|
199
|
+
repeated_reply = ejecuta(env, 'api', 'reply', thread, {
|
|
200
|
+
'claim': 'again', 'evidence': ['same'], 'consequence': 'noise',
|
|
201
|
+
})
|
|
202
|
+
afirma('· one grant cannot become an open debate',
|
|
203
|
+
repeated_reply.returncode != 0 and 'has not granted' in repeated_reply.stderr)
|
|
204
|
+
|
|
205
|
+
pending = valor(ejecuta(env, 'web', 'floor-request', thread, {
|
|
206
|
+
'basis': 'risk', 'reason': 'rollback banner may cache',
|
|
207
|
+
'evidence': ['cdn cache policy'],
|
|
208
|
+
}))
|
|
209
|
+
pending_id = pending.get('myFloorRequests', [{}])[-1].get('id', '')
|
|
210
|
+
blocked = ejecuta(env, 'seat', 'decide', thread, {
|
|
211
|
+
'outcome': 'ship', 'rationale': 'gates pass', 'owner': 'alice',
|
|
212
|
+
'executor': 'seat', 'verifier': 'ops',
|
|
213
|
+
'verificationQuestion': 'does replay pass?',
|
|
214
|
+
'selectedEvidence': ['run 482'], 'reopenIf': ['replay fails'],
|
|
215
|
+
})
|
|
216
|
+
afirma('· a decision cannot strand a pending request',
|
|
217
|
+
blocked.returncode != 0 and 'pending floor request' in blocked.stderr)
|
|
218
|
+
denied = ejecuta(env, 'seat', 'floor-deny', thread, {
|
|
219
|
+
'requestId': pending_id, 'reason': 'covered by the rollback check',
|
|
220
|
+
})
|
|
221
|
+
afirma('· the chair resolves the request explicitly', denied.returncode == 0)
|
|
222
|
+
|
|
223
|
+
decision = {
|
|
224
|
+
'outcome': 'ship after staging replay',
|
|
225
|
+
'rationale': 'migration and rollback evidence agree',
|
|
226
|
+
'owner': 'alice', 'executor': 'seat', 'verifier': 'ops',
|
|
227
|
+
'verificationQuestion': 'Can an independent clean replay and rollback pass?',
|
|
228
|
+
'selectedEvidence': ['run 482', 'web e2e'],
|
|
229
|
+
'decisiveContributors': ['api', 'web'],
|
|
230
|
+
'rejectedOptions': ['ship without replay'],
|
|
231
|
+
'dissent': ['banner timing remains uncertain'],
|
|
232
|
+
'reopenIf': ['clean replay fails'],
|
|
233
|
+
}
|
|
234
|
+
unattributed = dict(decision)
|
|
235
|
+
unattributed.pop('decisiveContributors')
|
|
236
|
+
missing_attribution = ejecuta(env, 'seat', 'decide', thread, unattributed)
|
|
237
|
+
afirma('· a chair must name which voices materially shaped the decision',
|
|
238
|
+
missing_attribution.returncode != 0
|
|
239
|
+
and 'decisiveContributors' in missing_attribution.stderr)
|
|
240
|
+
wrong_attribution = ejecuta(env, 'seat', 'decide', thread, {
|
|
241
|
+
**decision, 'decisiveContributors': ['unselected'],
|
|
242
|
+
})
|
|
243
|
+
afirma('· attribution cannot invent an unselected adviser',
|
|
244
|
+
wrong_attribution.returncode != 0
|
|
245
|
+
and 'not a selected contributor' in wrong_attribution.stderr)
|
|
246
|
+
decided = ejecuta(env, 'seat', 'decide', thread, decision)
|
|
247
|
+
afirma('· the chair records rationale, dissent and reopen conditions',
|
|
248
|
+
valor(decided).get('progress', {}).get('status') == 'verifying')
|
|
249
|
+
wrong_verifier = ejecuta(env, 'api', 'verify', thread, {
|
|
250
|
+
'result': 'pass', 'evidence': ['trust me'], 'checks': ['none'],
|
|
251
|
+
})
|
|
252
|
+
afirma('· only the assigned independent verifier can attest',
|
|
253
|
+
wrong_verifier.returncode != 0 and 'belongs to ops' in wrong_verifier.stderr)
|
|
254
|
+
failed = ejecuta(env, 'ops', 'verify', thread, {
|
|
255
|
+
'result': 'fail', 'evidence': ['clean DB replay failed at step 9'],
|
|
256
|
+
'checks': ['fresh database', 'rollback'], 'residualRisks': ['lock timeout'],
|
|
257
|
+
})
|
|
258
|
+
afirma('· failed independent evidence blocks closure',
|
|
259
|
+
valor(failed).get('progress', {}).get('status') == 'verification_failed')
|
|
260
|
+
cannot_close = ejecuta(env, 'seat', 'close', thread, {'summary': 'done'})
|
|
261
|
+
afirma('· the chair cannot close a failed verification',
|
|
262
|
+
cannot_close.returncode != 0
|
|
263
|
+
and 'only after verification passes' in cannot_close.stderr)
|
|
264
|
+
replanned = ejecuta(env, 'seat', 'replan', thread, {
|
|
265
|
+
'reason': 'fix step 9 and repeat the clean replay',
|
|
266
|
+
})
|
|
267
|
+
afirma('· failure creates a new controlled revision',
|
|
268
|
+
valor(replanned).get('progress', {}).get('revision') == 2)
|
|
269
|
+
afirma('· replanning still requires a fresh synthesis',
|
|
270
|
+
ejecuta(env, 'seat', 'decide', thread, decision).returncode != 0)
|
|
271
|
+
ejecuta(env, 'seat', 'synthesize', thread, {
|
|
272
|
+
'summary': 'Step 9 fixed; original evidence plus new replay required',
|
|
273
|
+
'agreements': ['repeat verification'], 'conflicts': [], 'unknowns': [],
|
|
274
|
+
})
|
|
275
|
+
ejecuta(env, 'seat', 'decide', thread, {
|
|
276
|
+
**decision, 'rationale': 'step 9 fixed and re-synthesised',
|
|
277
|
+
'selectedEvidence': ['run 482', 'fix step 9'],
|
|
278
|
+
})
|
|
279
|
+
passed = ejecuta(env, 'ops', 'verify', thread, {
|
|
280
|
+
'result': 'pass', 'evidence': ['clean replay 483 and rollback passed'],
|
|
281
|
+
'checks': ['fresh database', 'rollback'], 'residualRisks': [],
|
|
282
|
+
})
|
|
283
|
+
afirma('· a non-participant specialist can independently verify',
|
|
284
|
+
passed.returncode == 0
|
|
285
|
+
and valor(passed).get('progress', {}).get('status') == 'verified',
|
|
286
|
+
passed.stderr.strip())
|
|
287
|
+
closed = ejecuta(env, 'seat', 'close', thread, {
|
|
288
|
+
'summary': 'Release approved after independent replay',
|
|
289
|
+
'learnings': ['step 9 needs a permanent fixture'],
|
|
290
|
+
'followups': ['add the fixture'],
|
|
291
|
+
})
|
|
292
|
+
afirma('· only a passed act closes',
|
|
293
|
+
valor(closed).get('progress', {}).get('status') == 'closed')
|
|
294
|
+
|
|
295
|
+
api_road = subprocess.run(
|
|
296
|
+
['node', CLIENT, 'bus', 'send', 'alice/elsewhere', 'hello'],
|
|
297
|
+
capture_output=True, text=True, env=dict(env, CITY_BUS_ACTOR='api'), timeout=12)
|
|
298
|
+
afirma('· repo credentials cannot cross a road',
|
|
299
|
+
api_road.returncode != 0 and 'only the city chair' in api_road.stderr,
|
|
300
|
+
api_road.stderr.strip())
|
|
301
|
+
act_dir = os.path.join(city, 'deliberations', thread)
|
|
302
|
+
act = open(os.path.join(act_dir, 'ACT.md'), encoding='utf-8').read()
|
|
303
|
+
events = [json.loads(line) for line in open(
|
|
304
|
+
os.path.join(act_dir, 'events.jsonl'), encoding='utf-8') if line.strip()]
|
|
305
|
+
afirma('· the decision, verification and closure survive as a readable act',
|
|
306
|
+
'## Decision' in act and '**PASS** by ops' in act and '## Closure' in act)
|
|
307
|
+
afirma(
|
|
308
|
+
'· the readable participant list preserves repo roles',
|
|
309
|
+
'role data-engineer' in act and 'role seo' in act,
|
|
310
|
+
act[:700],
|
|
311
|
+
)
|
|
312
|
+
afirma('· every transition is append-only auditable',
|
|
313
|
+
len(events) >= 16 and events[-1]['type'] == 'committee.close',
|
|
314
|
+
f'{len(events)} events')
|
|
315
|
+
history = valor(ejecuta(env, 'seat', 'history'))
|
|
316
|
+
afirma('· the chair gets concise cross-decision memory against capture',
|
|
317
|
+
history.get('recent', [{}])[0].get('deliberation') == thread
|
|
318
|
+
and history.get('contributorCounts', [{}])[0].get('decisions') == 2
|
|
319
|
+
and 'not proof of capture' in history.get('note', ''), str(history))
|
|
320
|
+
denied_history = ejecuta(env, 'api', 'history')
|
|
321
|
+
afirma('· decision influence history is chair-only',
|
|
322
|
+
denied_history.returncode != 0
|
|
323
|
+
and 'only the city chair' in denied_history.stderr)
|
|
324
|
+
ai_list = valor(ejecuta(env, 'ai', 'list'))
|
|
325
|
+
ops_list = valor(ejecuta(env, 'ops', 'list'))
|
|
326
|
+
afirma('· unselected agents cannot browse the committee, but its verifier can',
|
|
327
|
+
ai_list == [] and any(item.get('id') == thread for item in ops_list))
|
|
328
|
+
cred_dir = os.path.join(app, '.runtime', 'bus', 'city-committee', 'actors')
|
|
329
|
+
comprueba('· one bus identity exists for every configured runtime',
|
|
330
|
+
sorted(name[:-5] for name in os.listdir(cred_dir)
|
|
331
|
+
if name.endswith('.json')),
|
|
332
|
+
['ai', 'api', 'ops', 'seat', 'web'])
|
|
333
|
+
finally:
|
|
334
|
+
para_hub(app)
|
|
335
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
336
|
+
return resumen('committee')
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
if __name__ == '__main__':
|
|
340
|
+
sys.exit(main())
|