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,109 @@
|
|
|
1
|
+
import { busCommand } from './hub-client.js';
|
|
2
|
+
import { parsePayload } from './cli-args.js';
|
|
3
|
+
|
|
4
|
+
const COMMANDS: Record<string, string> = {
|
|
5
|
+
open: 'committee.open',
|
|
6
|
+
respond: 'committee.respond',
|
|
7
|
+
synthesize: 'committee.synthesize',
|
|
8
|
+
'floor-request': 'committee.floor.request',
|
|
9
|
+
'floor-grant': 'committee.floor.grant',
|
|
10
|
+
'floor-deny': 'committee.floor.deny',
|
|
11
|
+
reply: 'committee.reply',
|
|
12
|
+
decide: 'committee.decide',
|
|
13
|
+
verify: 'committee.verify',
|
|
14
|
+
replan: 'committee.replan',
|
|
15
|
+
close: 'committee.close',
|
|
16
|
+
cancel: 'committee.cancel',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function committeeCli(args: string[]): Promise<unknown> {
|
|
20
|
+
const verb = args.shift() || 'list';
|
|
21
|
+
if (['help', '-h', '--help'].includes(verb)) return help();
|
|
22
|
+
if (verb === 'list') return busCommand('committee.list');
|
|
23
|
+
if (verb === 'history') return busCommand('committee.history');
|
|
24
|
+
if (verb === 'schema') return schema(args[0] || 'open');
|
|
25
|
+
const { payload, positional } = parsePayload(args);
|
|
26
|
+
if (verb === 'show' || verb === 'status') {
|
|
27
|
+
const thread = positional[0];
|
|
28
|
+
if (!thread) throw new Error(`committee ${verb} needs a deliberation id`);
|
|
29
|
+
return busCommand('committee.get', {}, thread);
|
|
30
|
+
}
|
|
31
|
+
const command = COMMANDS[verb];
|
|
32
|
+
if (!command) throw new Error(`unknown committee command: ${verb}`);
|
|
33
|
+
const thread = verb === 'open' ? undefined : positional[0];
|
|
34
|
+
if (verb !== 'open' && !thread) throw new Error(`committee ${verb} needs a deliberation id`);
|
|
35
|
+
if (verb === 'open' && payload.outcome && !payload.desiredOutcome) {
|
|
36
|
+
payload.desiredOutcome = payload.outcome;
|
|
37
|
+
delete payload.outcome;
|
|
38
|
+
}
|
|
39
|
+
return busCommand(command, payload, thread);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function help(): Record<string, unknown> {
|
|
43
|
+
return {
|
|
44
|
+
usage: 'agents-city committee <command> [deliberation-id] [--input payload.json | flags]',
|
|
45
|
+
commands: [
|
|
46
|
+
'list',
|
|
47
|
+
'history',
|
|
48
|
+
'show',
|
|
49
|
+
'open',
|
|
50
|
+
...Object.keys(COMMANDS).filter((name) => name !== 'open'),
|
|
51
|
+
'schema',
|
|
52
|
+
],
|
|
53
|
+
note: 'Every mutation travels over the local WebSocket bus; repo agents cannot address one another.',
|
|
54
|
+
examples: [
|
|
55
|
+
'agents-city committee open --question "Ship it?" --outcome "A verified go/no-go" --member api --done "tests pass"',
|
|
56
|
+
'agents-city committee respond DELIB --input /tmp/position.json',
|
|
57
|
+
'agents-city committee schema respond',
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function schema(verb: string): Record<string, unknown> {
|
|
63
|
+
const examples: Record<string, Record<string, unknown>> = {
|
|
64
|
+
open: {
|
|
65
|
+
question: 'What exactly are we deciding?',
|
|
66
|
+
desiredOutcome: 'The concrete result expected from the committee',
|
|
67
|
+
context: 'Only facts needed to answer',
|
|
68
|
+
constraints: ['time, cost or policy boundary'],
|
|
69
|
+
definitionOfDone: ['observable acceptance condition'],
|
|
70
|
+
authority: 'recommend',
|
|
71
|
+
participants: ['api'],
|
|
72
|
+
maxRebuttals: 2,
|
|
73
|
+
},
|
|
74
|
+
respond: {
|
|
75
|
+
stance: 'conditional',
|
|
76
|
+
recommendation: 'What this specialist recommends',
|
|
77
|
+
evidence: ['file:line, command result or source'],
|
|
78
|
+
expectedImpact: 'What number or observable outcome changes',
|
|
79
|
+
visibleWhen: 'When the change can be measured',
|
|
80
|
+
withdrawIf: 'Evidence that would change this position',
|
|
81
|
+
risks: [],
|
|
82
|
+
unknowns: [],
|
|
83
|
+
},
|
|
84
|
+
synthesize: { summary: '', agreements: [], conflicts: [], unknowns: [], proceedWithout: '' },
|
|
85
|
+
'floor-request': { basis: 'new_evidence', reason: '', evidence: [''] },
|
|
86
|
+
'floor-grant': { requestId: '' },
|
|
87
|
+
'floor-deny': { requestId: '', reason: '' },
|
|
88
|
+
reply: { claim: '', evidence: [''], consequence: '' },
|
|
89
|
+
decide: {
|
|
90
|
+
outcome: '',
|
|
91
|
+
rationale: '',
|
|
92
|
+
owner: '',
|
|
93
|
+
executor: 'seat',
|
|
94
|
+
verifier: 'api',
|
|
95
|
+
verificationQuestion: '',
|
|
96
|
+
selectedEvidence: [''],
|
|
97
|
+
decisiveContributors: ['api'],
|
|
98
|
+
rejectedOptions: [],
|
|
99
|
+
dissent: [],
|
|
100
|
+
reopenIf: [''],
|
|
101
|
+
},
|
|
102
|
+
verify: { result: 'pass', evidence: [''], checks: [''], residualRisks: [] },
|
|
103
|
+
replan: { reason: '' },
|
|
104
|
+
close: { summary: '', learnings: [], followups: [] },
|
|
105
|
+
cancel: { reason: '' },
|
|
106
|
+
};
|
|
107
|
+
if (!examples[verb]) throw new Error(`no schema for committee ${verb}`);
|
|
108
|
+
return examples[verb];
|
|
109
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { BusEnvelope } from './protocol.js';
|
|
4
|
+
import { TerminalReceipt } from './terminal-delivery.js';
|
|
5
|
+
|
|
6
|
+
export interface DeliveryMetric {
|
|
7
|
+
schema: 'agents-city/delivery-latency@1';
|
|
8
|
+
envelopeId: string;
|
|
9
|
+
thread: string | null;
|
|
10
|
+
kind: string;
|
|
11
|
+
actor: string;
|
|
12
|
+
runtime: string;
|
|
13
|
+
envelopeCreatedAt: string;
|
|
14
|
+
adapterReceivedAt: string;
|
|
15
|
+
terminalReadyAt: string;
|
|
16
|
+
pastedAt: string;
|
|
17
|
+
submittedAt: string;
|
|
18
|
+
transportToAdapterMs: number;
|
|
19
|
+
adapterToSubmitMs: number;
|
|
20
|
+
totalToSubmitMs: number;
|
|
21
|
+
pasteToSubmitMs: number;
|
|
22
|
+
bytes: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function recordDelivery(
|
|
26
|
+
runtimeDir: string,
|
|
27
|
+
envelope: BusEnvelope,
|
|
28
|
+
actor: string,
|
|
29
|
+
runtime: string,
|
|
30
|
+
adapterReceivedAt: string,
|
|
31
|
+
receipt: TerminalReceipt,
|
|
32
|
+
): DeliveryMetric {
|
|
33
|
+
const metric: DeliveryMetric = {
|
|
34
|
+
schema: 'agents-city/delivery-latency@1',
|
|
35
|
+
envelopeId: envelope.id,
|
|
36
|
+
thread: envelope.thread,
|
|
37
|
+
kind: envelope.kind,
|
|
38
|
+
actor,
|
|
39
|
+
runtime,
|
|
40
|
+
envelopeCreatedAt: envelope.createdAt,
|
|
41
|
+
adapterReceivedAt,
|
|
42
|
+
terminalReadyAt: receipt.readyAt,
|
|
43
|
+
pastedAt: receipt.pastedAt,
|
|
44
|
+
submittedAt: receipt.submittedAt,
|
|
45
|
+
transportToAdapterMs: elapsed(envelope.createdAt, adapterReceivedAt),
|
|
46
|
+
adapterToSubmitMs: elapsed(adapterReceivedAt, receipt.submittedAt),
|
|
47
|
+
totalToSubmitMs: elapsed(envelope.createdAt, receipt.submittedAt),
|
|
48
|
+
pasteToSubmitMs: receipt.pasteToSubmitMs,
|
|
49
|
+
bytes: receipt.bytes,
|
|
50
|
+
};
|
|
51
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
52
|
+
appendFileSync(join(runtimeDir, 'delivery-latency.jsonl'), JSON.stringify(metric) + '\n', {
|
|
53
|
+
mode: 0o600,
|
|
54
|
+
});
|
|
55
|
+
return metric;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function elapsed(start: string, end: string): number {
|
|
59
|
+
const value = Date.parse(end) - Date.parse(start);
|
|
60
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
61
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { atomicJson } from './runtime-files.js';
|
|
4
|
+
import { BusEnvelope, MAX_PENDING, MESSAGE_TTL_MS, safeSegment } from './protocol.js';
|
|
5
|
+
|
|
6
|
+
export function enqueueForActor(runtimeDir: string, envelope: BusEnvelope): void {
|
|
7
|
+
const directory = join(runtimeDir, 'outbox', safeSegment(envelope.to.actor));
|
|
8
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
9
|
+
trim(directory);
|
|
10
|
+
atomicJson(join(directory, `${fileKey(envelope.id)}.json`), envelope);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function pendingForActor(runtimeDir: string, actor: string): BusEnvelope[] {
|
|
14
|
+
const directory = join(runtimeDir, 'outbox', safeSegment(actor));
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
return jsonFiles(directory)
|
|
17
|
+
.map((path) => {
|
|
18
|
+
try {
|
|
19
|
+
const envelope = JSON.parse(readFileSync(path, 'utf8')) as BusEnvelope;
|
|
20
|
+
if (now - Date.parse(envelope.createdAt) > MESSAGE_TTL_MS) {
|
|
21
|
+
unlinkSync(path);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return envelope;
|
|
25
|
+
} catch {
|
|
26
|
+
try {
|
|
27
|
+
unlinkSync(path);
|
|
28
|
+
} catch {}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.filter((envelope): envelope is BusEnvelope => envelope !== null);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function acknowledge(runtimeDir: string, actor: string, envelopeId: string): boolean {
|
|
36
|
+
const path = join(runtimeDir, 'outbox', safeSegment(actor), `${fileKey(envelopeId)}.json`);
|
|
37
|
+
try {
|
|
38
|
+
unlinkSync(path);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function queueRoad(runtimeDir: string, envelope: BusEnvelope): void {
|
|
46
|
+
const directory = join(runtimeDir, 'road-queue');
|
|
47
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
48
|
+
trim(directory);
|
|
49
|
+
atomicJson(join(directory, `${fileKey(envelope.id)}.json`), envelope);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function drainRoadQueue(runtimeDir: string): BusEnvelope[] {
|
|
53
|
+
const directory = join(runtimeDir, 'road-queue');
|
|
54
|
+
const out: BusEnvelope[] = [];
|
|
55
|
+
for (const path of jsonFiles(directory)) {
|
|
56
|
+
try {
|
|
57
|
+
const envelope = JSON.parse(readFileSync(path, 'utf8')) as BusEnvelope;
|
|
58
|
+
if (Date.now() - Date.parse(envelope.createdAt) <= MESSAGE_TTL_MS) out.push(envelope);
|
|
59
|
+
} catch {}
|
|
60
|
+
try {
|
|
61
|
+
unlinkSync(path);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function recordRoadInbox(runtimeDir: string, envelope: BusEnvelope): void {
|
|
68
|
+
const directory = join(runtimeDir, 'road-inbox');
|
|
69
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
70
|
+
trim(directory);
|
|
71
|
+
atomicJson(join(directory, `${fileKey(envelope.id)}.json`), envelope);
|
|
72
|
+
appendFileSync(join(runtimeDir, 'road-history.jsonl'), JSON.stringify(envelope) + '\n', {
|
|
73
|
+
mode: 0o600,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function takeRoadInbox(runtimeDir: string): BusEnvelope[] {
|
|
78
|
+
const directory = join(runtimeDir, 'road-inbox');
|
|
79
|
+
const out: BusEnvelope[] = [];
|
|
80
|
+
for (const path of jsonFiles(directory)) {
|
|
81
|
+
try {
|
|
82
|
+
out.push(JSON.parse(readFileSync(path, 'utf8')) as BusEnvelope);
|
|
83
|
+
} catch {}
|
|
84
|
+
try {
|
|
85
|
+
unlinkSync(path);
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function jsonFiles(directory: string): string[] {
|
|
92
|
+
if (!existsSync(directory)) return [];
|
|
93
|
+
try {
|
|
94
|
+
return readdirSync(directory)
|
|
95
|
+
.filter((name) => name.endsWith('.json'))
|
|
96
|
+
.sort()
|
|
97
|
+
.map((name) => join(directory, name));
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fileKey(value: string): string {
|
|
104
|
+
const out = String(value)
|
|
105
|
+
.replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
|
106
|
+
.slice(0, 160);
|
|
107
|
+
if (!out) throw new Error('invalid message id');
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function trim(directory: string): void {
|
|
112
|
+
const files = jsonFiles(directory);
|
|
113
|
+
for (const path of files.slice(0, Math.max(0, files.length - MAX_PENDING + 1))) {
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(path);
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ActivityDraft, ActivityFeed, ActivityTone } from './activity-feed.js';
|
|
2
|
+
import { ActorRole } from '../protocol.js';
|
|
3
|
+
import { ClientMode } from './connections.js';
|
|
4
|
+
|
|
5
|
+
const TONES = new Set<ActivityTone>([
|
|
6
|
+
'question',
|
|
7
|
+
'work',
|
|
8
|
+
'floor',
|
|
9
|
+
'evidence',
|
|
10
|
+
'decision',
|
|
11
|
+
'verification',
|
|
12
|
+
'error',
|
|
13
|
+
'system',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Authenticated semantic events from native runtimes and Claude hooks.
|
|
18
|
+
*
|
|
19
|
+
* Actor and authority always come from the socket credential. Payloads may
|
|
20
|
+
* describe visible prompts, answers and work, but cannot impersonate committee
|
|
21
|
+
* state transitions or smuggle raw provider frames into the browser.
|
|
22
|
+
*/
|
|
23
|
+
export function activityController(publish: ActivityFeed['publish']) {
|
|
24
|
+
const command = (
|
|
25
|
+
payload: Record<string, unknown>,
|
|
26
|
+
thread: string | undefined,
|
|
27
|
+
actor: string,
|
|
28
|
+
role: ActorRole,
|
|
29
|
+
mode: ClientMode,
|
|
30
|
+
) => {
|
|
31
|
+
if (!['runtime', 'mcp', 'client'].includes(mode)) {
|
|
32
|
+
throw new Error('this connection cannot publish city activity');
|
|
33
|
+
}
|
|
34
|
+
const kind = clean(payload.kind, 100);
|
|
35
|
+
if (!/^(?:conversation|runtime|work)\.[a-z0-9_.-]+$/.test(kind)) {
|
|
36
|
+
throw new Error('activity kind must describe conversation, runtime or work');
|
|
37
|
+
}
|
|
38
|
+
if (/(?:^|[._-])(?:reasoning|thoughts?|chain-of-thought)(?:$|[._-])/.test(kind)) {
|
|
39
|
+
throw new Error('private model reasoning is never city activity');
|
|
40
|
+
}
|
|
41
|
+
const requestedTone = clean(payload.tone, 30) as ActivityTone;
|
|
42
|
+
const draft: ActivityDraft = {
|
|
43
|
+
sourceId: clean(payload.sourceId, 240) || undefined,
|
|
44
|
+
kind,
|
|
45
|
+
thread: clean(thread || payload.thread, 160) || null,
|
|
46
|
+
actor,
|
|
47
|
+
role,
|
|
48
|
+
phase: clean(payload.phase, 40) || 'observed',
|
|
49
|
+
tone: TONES.has(requestedTone) ? requestedTone : 'work',
|
|
50
|
+
title: clean(payload.title, 300) || `${actor} reported activity`,
|
|
51
|
+
summary: clean(payload.summary, 4_000),
|
|
52
|
+
details: Array.isArray(payload.details)
|
|
53
|
+
? payload.details
|
|
54
|
+
.slice(0, 120)
|
|
55
|
+
.map((item) => clean(item, 2_000))
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
: [],
|
|
58
|
+
target: clean(payload.target, 80) || undefined,
|
|
59
|
+
};
|
|
60
|
+
if (!draft.summary && !draft.details?.length) {
|
|
61
|
+
throw new Error('activity needs a visible summary or details');
|
|
62
|
+
}
|
|
63
|
+
return publish(draft);
|
|
64
|
+
};
|
|
65
|
+
return { command };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clean(value: unknown, max: number): string {
|
|
69
|
+
return String(value ?? '')
|
|
70
|
+
.trim()
|
|
71
|
+
.slice(0, max);
|
|
72
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import WebSocket from 'ws';
|
|
12
|
+
import { ActorRole, isoNow, randomId } from '../protocol.js';
|
|
13
|
+
import { CityContext } from '../city-config.js';
|
|
14
|
+
|
|
15
|
+
export const ACTIVITY_PROTOCOL = 'agents-city-activity/1' as const;
|
|
16
|
+
|
|
17
|
+
export type ActivityTone =
|
|
18
|
+
'question' | 'work' | 'floor' | 'evidence' | 'decision' | 'verification' | 'error' | 'system';
|
|
19
|
+
|
|
20
|
+
export interface ActivityDraft {
|
|
21
|
+
/** Stable provider/hook identity used to make reconnects and repeated hooks idempotent. */
|
|
22
|
+
sourceId?: string;
|
|
23
|
+
kind: string;
|
|
24
|
+
thread: string | null;
|
|
25
|
+
actor: string;
|
|
26
|
+
role: ActorRole;
|
|
27
|
+
phase: string;
|
|
28
|
+
tone: ActivityTone;
|
|
29
|
+
title: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
details?: string[];
|
|
32
|
+
target?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ActivityEvent extends ActivityDraft {
|
|
36
|
+
protocol: typeof ACTIVITY_PROTOCOL;
|
|
37
|
+
id: string;
|
|
38
|
+
seq: number;
|
|
39
|
+
city: string;
|
|
40
|
+
at: string;
|
|
41
|
+
details: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read-only semantic feed for the browser.
|
|
46
|
+
*
|
|
47
|
+
* It records protocol actions, never model chain-of-thought or raw transport
|
|
48
|
+
* frames. Keeping this beside the hub means the browser sees the exact same
|
|
49
|
+
* ordering as the committee state machine, independently of the model vendor.
|
|
50
|
+
*/
|
|
51
|
+
export function activityFeed(context: CityContext) {
|
|
52
|
+
const path = join(context.runtimeDir, 'activity.jsonl');
|
|
53
|
+
const spectators = new Set<WebSocket>();
|
|
54
|
+
const restored = readRecent(path, 1_000);
|
|
55
|
+
let recent = restored.slice(-200);
|
|
56
|
+
let seq = recent.at(-1)?.seq || 0;
|
|
57
|
+
const sources = new Map(
|
|
58
|
+
restored.filter((event) => event.sourceId).map((event) => [event.sourceId as string, event]),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const publish = (draft: ActivityDraft): ActivityEvent => {
|
|
62
|
+
const sourceId = String(draft.sourceId || '')
|
|
63
|
+
.trim()
|
|
64
|
+
.slice(0, 240);
|
|
65
|
+
const existing = sourceId ? sources.get(sourceId) : undefined;
|
|
66
|
+
if (existing) return existing;
|
|
67
|
+
const event: ActivityEvent = {
|
|
68
|
+
protocol: ACTIVITY_PROTOCOL,
|
|
69
|
+
id: randomId('activity'),
|
|
70
|
+
seq: ++seq,
|
|
71
|
+
city: context.city.address,
|
|
72
|
+
at: isoNow(),
|
|
73
|
+
...cleanDraft(draft),
|
|
74
|
+
};
|
|
75
|
+
mkdirSync(context.runtimeDir, { recursive: true, mode: 0o700 });
|
|
76
|
+
appendFileSync(path, JSON.stringify(event) + '\n', { mode: 0o600 });
|
|
77
|
+
recent.push(event);
|
|
78
|
+
if (event.sourceId) sources.set(event.sourceId, event);
|
|
79
|
+
if (recent.length > 200) recent = recent.slice(-200);
|
|
80
|
+
if (seq % 100 === 0 && fileIsLarge(path)) compact(path);
|
|
81
|
+
fanOut({ type: 'activity.event', event });
|
|
82
|
+
return event;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const subscribe = (ws: WebSocket): void => {
|
|
86
|
+
spectators.add(ws);
|
|
87
|
+
ws.send(
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
type: 'activity.state',
|
|
90
|
+
protocol: ACTIVITY_PROTOCOL,
|
|
91
|
+
city: context.city.address,
|
|
92
|
+
events: recent,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const remove = (ws: WebSocket): void => {
|
|
98
|
+
spectators.delete(ws);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const fanOut = (message: Record<string, unknown>): void => {
|
|
102
|
+
const encoded = JSON.stringify(message);
|
|
103
|
+
for (const ws of spectators) {
|
|
104
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
105
|
+
try {
|
|
106
|
+
ws.send(encoded);
|
|
107
|
+
} catch {
|
|
108
|
+
spectators.delete(ws);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return { publish, subscribe, remove };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cleanDraft(draft: ActivityDraft): ActivityDraft & { details: string[] } {
|
|
117
|
+
const short = (value: unknown, max: number): string =>
|
|
118
|
+
String(value ?? '')
|
|
119
|
+
.trim()
|
|
120
|
+
.slice(0, max);
|
|
121
|
+
return {
|
|
122
|
+
...(draft.sourceId ? { sourceId: short(draft.sourceId, 240) } : {}),
|
|
123
|
+
kind: short(draft.kind, 100),
|
|
124
|
+
thread: draft.thread ? short(draft.thread, 100) : null,
|
|
125
|
+
actor: short(draft.actor, 80),
|
|
126
|
+
role: draft.role,
|
|
127
|
+
phase: short(draft.phase, 40),
|
|
128
|
+
tone: draft.tone,
|
|
129
|
+
title: short(draft.title, 300),
|
|
130
|
+
summary: short(draft.summary, 4_000),
|
|
131
|
+
details: (draft.details || []).slice(0, 120).map((detail) => short(detail, 2_000)),
|
|
132
|
+
...(draft.target ? { target: short(draft.target, 80) } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readRecent(path: string, limit: number): ActivityEvent[] {
|
|
137
|
+
try {
|
|
138
|
+
return readFileSync(path, 'utf8')
|
|
139
|
+
.split('\n')
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.slice(-limit)
|
|
142
|
+
.map((line) => JSON.parse(line) as ActivityEvent)
|
|
143
|
+
.filter((event) => event.protocol === ACTIVITY_PROTOCOL && Number.isInteger(event.seq));
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function fileIsLarge(path: string): boolean {
|
|
150
|
+
try {
|
|
151
|
+
return statSync(path).size > 2_000_000;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function compact(path: string): void {
|
|
158
|
+
if (!existsSync(path)) return;
|
|
159
|
+
try {
|
|
160
|
+
const kept = readFileSync(path, 'utf8').split('\n').filter(Boolean).slice(-1_000);
|
|
161
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
162
|
+
writeFileSync(tmp, kept.join('\n') + '\n', { mode: 0o600 });
|
|
163
|
+
renameSync(tmp, path);
|
|
164
|
+
} catch {
|
|
165
|
+
// Provider state and committee acts remain authoritative; compaction is best effort.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ActivityFeed = ReturnType<typeof activityFeed>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ActorRole } from '../protocol.js';
|
|
2
|
+
import { committeeActivities } from '../committee/activity.js';
|
|
3
|
+
import { requireChair } from '../committee/guards.js';
|
|
4
|
+
import { committeeService } from '../committee/service.js';
|
|
5
|
+
import type { ActivityDraft } from './activity-feed.js';
|
|
6
|
+
import { EnvelopeRouter } from './envelopes.js';
|
|
7
|
+
|
|
8
|
+
type CommitteeService = ReturnType<typeof committeeService>;
|
|
9
|
+
|
|
10
|
+
export function committeeController(
|
|
11
|
+
service: CommitteeService,
|
|
12
|
+
router: EnvelopeRouter,
|
|
13
|
+
observe: (event: ActivityDraft) => void = () => {},
|
|
14
|
+
) {
|
|
15
|
+
const command = (
|
|
16
|
+
name: string,
|
|
17
|
+
thread: string | undefined,
|
|
18
|
+
payload: Record<string, unknown>,
|
|
19
|
+
actor: string,
|
|
20
|
+
role: ActorRole,
|
|
21
|
+
): unknown => {
|
|
22
|
+
if (name === 'committee.history') {
|
|
23
|
+
requireChair(actor, role);
|
|
24
|
+
return service.history(thread);
|
|
25
|
+
}
|
|
26
|
+
if (name === 'committee.list') {
|
|
27
|
+
return service
|
|
28
|
+
.list()
|
|
29
|
+
.filter(
|
|
30
|
+
(state) =>
|
|
31
|
+
role === 'chair' ||
|
|
32
|
+
state.brief.participants.includes(actor) ||
|
|
33
|
+
state.decisions.at(-1)?.verifier === actor,
|
|
34
|
+
)
|
|
35
|
+
.map((state) => ({
|
|
36
|
+
id: state.id,
|
|
37
|
+
status: state.status,
|
|
38
|
+
question: state.brief.question,
|
|
39
|
+
participants: state.brief.participants,
|
|
40
|
+
received: Object.keys(state.positions).length,
|
|
41
|
+
createdAt: state.createdAt,
|
|
42
|
+
updatedAt: state.updatedAt,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
if (name === 'committee.get') {
|
|
46
|
+
if (!thread) throw new Error('thread is required');
|
|
47
|
+
return service.view(thread, actor, role);
|
|
48
|
+
}
|
|
49
|
+
const result = service.transition(name, thread, payload, actor, role);
|
|
50
|
+
for (const delivery of result.deliveries) {
|
|
51
|
+
router.internal(delivery.kind, actor, role, delivery.to, result.state.id, delivery.payload);
|
|
52
|
+
}
|
|
53
|
+
for (const event of committeeActivities(name, result.state, payload, actor, role)) {
|
|
54
|
+
observe(event);
|
|
55
|
+
}
|
|
56
|
+
return service.view(result.state.id, actor, role);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return { command };
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { ActorRole, BusEnvelope } from '../protocol.js';
|
|
3
|
+
|
|
4
|
+
export type ClientMode = 'runtime' | 'adapter' | 'client' | 'mcp';
|
|
5
|
+
|
|
6
|
+
export interface ActorPeer {
|
|
7
|
+
ws: WebSocket;
|
|
8
|
+
actor: string;
|
|
9
|
+
role: ActorRole;
|
|
10
|
+
mode: ClientMode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function connectionRegistry() {
|
|
14
|
+
const peers = new Set<ActorPeer>();
|
|
15
|
+
|
|
16
|
+
const add = (peer: ActorPeer): void => {
|
|
17
|
+
peers.add(peer);
|
|
18
|
+
};
|
|
19
|
+
const remove = (ws: WebSocket): void => {
|
|
20
|
+
for (const peer of peers) if (peer.ws === ws) peers.delete(peer);
|
|
21
|
+
};
|
|
22
|
+
const deliver = (actor: string, envelope: BusEnvelope): boolean => {
|
|
23
|
+
// A native runtime gateway owns delivery. The terminal adapter is retained
|
|
24
|
+
// only as an explicit compatibility fallback, and is never used while a
|
|
25
|
+
// native gateway for the actor is online. CLI/MCP command connections cannot
|
|
26
|
+
// consume an envelope merely by opening a status command.
|
|
27
|
+
const candidates = [...peers].filter(
|
|
28
|
+
(candidate) => candidate.actor === actor && candidate.ws.readyState === WebSocket.OPEN,
|
|
29
|
+
);
|
|
30
|
+
const peer =
|
|
31
|
+
candidates.find((candidate) => candidate.mode === 'runtime') ||
|
|
32
|
+
candidates.find((candidate) => candidate.mode === 'adapter');
|
|
33
|
+
if (!peer) return false;
|
|
34
|
+
peer.ws.send(JSON.stringify({ type: 'envelope', envelope }));
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
const online = (actor: string): boolean =>
|
|
38
|
+
[...peers].some(
|
|
39
|
+
(peer) =>
|
|
40
|
+
peer.actor === actor &&
|
|
41
|
+
(peer.mode === 'runtime' || peer.mode === 'adapter') &&
|
|
42
|
+
peer.ws.readyState === WebSocket.OPEN,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return { add, remove, deliver, online };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ConnectionRegistry = ReturnType<typeof connectionRegistry>;
|