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,526 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build somebody's tmux session out of their card:
|
|
3
|
+
# window "seat" -> this city's stable address, sitting in the city folder
|
|
4
|
+
# window N -> one repo support agent, on its configured runtime
|
|
5
|
+
#
|
|
6
|
+
# city-session.sh [user] [--claude] [--no-sync] [--no-yolo] [--only r1,r2]
|
|
7
|
+
#
|
|
8
|
+
# The repos on your card are looked for across the whole disk: they do not have
|
|
9
|
+
# to live in one folder, and the folder does not have to be named after the repo.
|
|
10
|
+
#
|
|
11
|
+
# Without --claude it just opens shells. With it, each window starts the
|
|
12
|
+
# runtime configured for that city member (Claude, Codex, OpenCode, or custom).
|
|
13
|
+
#
|
|
14
|
+
# The permissions are NOT the same in every window, and that is the point:
|
|
15
|
+
#
|
|
16
|
+
# the "seat" window -> NO bypass. It receives text from connected cities, and
|
|
17
|
+
# there the confirmations are the only door.
|
|
18
|
+
# repo windows -> bypass on (yolo). Your repos on your machine, and
|
|
19
|
+
# nothing from outside comes in through them. --no-yolo
|
|
20
|
+
# turns it off.
|
|
21
|
+
#
|
|
22
|
+
# Repo agents do not message one another. Every runtime receives assignments
|
|
23
|
+
# through the same authenticated local WebSocket bus; the seat chairs the turns.
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
|
|
27
|
+
USUARIO=""
|
|
28
|
+
RUN_CLAUDE=0
|
|
29
|
+
DO_SYNC=1
|
|
30
|
+
YOLO=1
|
|
31
|
+
SOLO=""
|
|
32
|
+
|
|
33
|
+
while [ $# -gt 0 ]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
-c|--claude) RUN_CLAUDE=1; shift ;;
|
|
36
|
+
-n|--no-sync) DO_SYNC=0; shift ;;
|
|
37
|
+
--no-yolo) YOLO=0; shift ;;
|
|
38
|
+
--only) SOLO="${2:-}"; shift 2 ;;
|
|
39
|
+
-h|--help) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
40
|
+
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
41
|
+
*) USUARIO="$1"; shift ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# ── The selected city, where its one owner card lives ──────────────────────
|
|
46
|
+
# Resolve it through the same owner as seat, hall, hooks and the road channel.
|
|
47
|
+
# An explicit AGENTS_CITY_DATA still wins inside city-env.sh.
|
|
48
|
+
. "$(dirname "$0")/city-env.sh"
|
|
49
|
+
EQUIPO="${AGENTS_CITY_DATA:-}"
|
|
50
|
+
if [ ! -d "$EQUIPO" ]; then
|
|
51
|
+
echo "I cannot resolve the selected city." >&2
|
|
52
|
+
echo "Create one with agents-city setup, or select one with agents-city cities use <city>." >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ── Who ────────────────────────────────────────────────────────────────────
|
|
57
|
+
if [ -z "$USUARIO" ]; then USUARIO="${AGENTS_CITY_USER:-}"; fi
|
|
58
|
+
FICHA="$EQUIPO/$USUARIO.md"
|
|
59
|
+
if [ ! -f "$FICHA" ]; then
|
|
60
|
+
echo "There is no card for '$USUARIO' in $EQUIPO." >&2
|
|
61
|
+
fichas=""
|
|
62
|
+
for f in "$EQUIPO"/*.md; do
|
|
63
|
+
case "$(basename "$f")" in README*|AGENTS*) ;; *) fichas="$fichas $(basename "$f" .md)" ;; esac
|
|
64
|
+
done
|
|
65
|
+
echo "Cards there:$fichas" >&2
|
|
66
|
+
echo "Say who you are: $(basename "$0") <user>" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ── The card: agent and repos ──────────────────────────────────────────────
|
|
71
|
+
LEER="$(dirname "$0")/read-card.py"
|
|
72
|
+
RUNTIME="$(dirname "$0")/city-runtime.sh"
|
|
73
|
+
LAUNCHPY="$(dirname "$0")/launch.py"
|
|
74
|
+
CHANNEL_CLIENT="$(dirname "$0")/../channel/client.js"
|
|
75
|
+
# `agent`, which is what every card actually carries. A v1 reader once asked for
|
|
76
|
+
# `agente`, got nothing every single time,
|
|
77
|
+
# and fell through to the default below: so the seat window never carried anybody's
|
|
78
|
+
# role, and the architect sat on the bus as <user>/dev. The fallback hid it.
|
|
79
|
+
#
|
|
80
|
+
# One call, because card.py accepts `agente` as an alias for a hand-written card.
|
|
81
|
+
AGENTE="$(python3 "$LEER" "$FICHA" agent)"
|
|
82
|
+
REPOS_RAW="$(python3 "$LEER" "$FICHA" repos)"
|
|
83
|
+
[ -z "$AGENTE" ] && AGENTE="$(python3 "$(dirname "$0")/cities.py" address "$USUARIO" "$EQUIPO")"
|
|
84
|
+
|
|
85
|
+
[ -n "$SOLO" ] && REPOS_RAW="$SOLO"
|
|
86
|
+
|
|
87
|
+
CODE_DIR="${CITY_CODE_DIR:-$HOME/codigo}"
|
|
88
|
+
# The session's name comes from cities.py, not from a rule of our own: in the
|
|
89
|
+
# every city it carries owner and city — so two local seats never fight over one
|
|
90
|
+
# tmux session, including the first city named `home`.
|
|
91
|
+
SESSION="$(python3 "$(dirname "$0")/cities.py" sesion "$USUARIO" "$EQUIPO")"
|
|
92
|
+
[ -z "$SESSION" ] && SESSION="$USUARIO"
|
|
93
|
+
ADDRESS="$(python3 "$(dirname "$0")/cities.py" address "$USUARIO" "$EQUIPO")"
|
|
94
|
+
# Claude session names are machine-global. The old literal `seat` meant opening a
|
|
95
|
+
# second local city could address or replace the first one's seat by accident.
|
|
96
|
+
SEAT_NAME="$SESSION"
|
|
97
|
+
|
|
98
|
+
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
99
|
+
echo "Session '$SESSION' is already up — attaching."
|
|
100
|
+
exec tmux attach -d -t "$SESSION"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Claude's native cross-session path is explicitly closed. The city bus is the
|
|
104
|
+
# only route, so Claude, Codex, OpenCode and Kimi obey the same topology.
|
|
105
|
+
# skipDangerousModePermissionPrompt only suppresses Claude's local yolo notice.
|
|
106
|
+
SETTINGS='{"crossSessionInbound":"refuse","skipDangerousModePermissionPrompt":true}'
|
|
107
|
+
NO_PEER_TOOLS=' --disallowed-tools SendMessage,ListAgents'
|
|
108
|
+
YOLO_FLAG=""
|
|
109
|
+
[ "$YOLO" -eq 1 ] && YOLO_FLAG=" --dangerously-skip-permissions"
|
|
110
|
+
|
|
111
|
+
# Whether the chair itself runs yolo. A per-city decision written in city.yml
|
|
112
|
+
# (`seat_yolo: 1`, set at setup or with `agents-city seat --seat-yolo on`):
|
|
113
|
+
# locally the seat is the owner's own hands on the owner's own machine, and an
|
|
114
|
+
# owner who trusts that should not be asked for permission by their own chair.
|
|
115
|
+
# The default stays off, and --no-yolo remains the session-wide brake for both.
|
|
116
|
+
SEAT_YOLO="$(python3 "$(dirname "$0")/cities.py" clave "$EQUIPO" seat_yolo 2>/dev/null || true)"
|
|
117
|
+
[ "$YOLO" -eq 0 ] && SEAT_YOLO=0
|
|
118
|
+
SEAT_AUTO=0
|
|
119
|
+
SEAT_YOLO_FLAG=""
|
|
120
|
+
if [ "${SEAT_YOLO:-0}" = "1" ]; then
|
|
121
|
+
SEAT_AUTO=1
|
|
122
|
+
SEAT_YOLO_FLAG=" --dangerously-skip-permissions"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Claude authentication can also arrive through the parent shell/tmux server.
|
|
126
|
+
# A stale CLAUDE_CODE_OAUTH_TOKEN (or API/gateway variable) takes precedence
|
|
127
|
+
# over the owner's healthy Claude.ai login and makes a Team/Max account appear
|
|
128
|
+
# as "Claude API" with no usage credits. Never delete or rewrite credentials:
|
|
129
|
+
# when a stored Claude.ai login works, remove provider overrides only from each
|
|
130
|
+
# city child process. Owners intentionally using environment auth can keep it
|
|
131
|
+
# with CITY_CLAUDE_AUTH=environment.
|
|
132
|
+
claude_auth_prefix() {
|
|
133
|
+
[ "${CITY_CLAUDE_AUTH:-auto}" = environment ] && return 0
|
|
134
|
+
command -v claude >/dev/null 2>&1 || return 0
|
|
135
|
+
local estado
|
|
136
|
+
estado="$(env \
|
|
137
|
+
-u CLAUDE_CODE_OAUTH_TOKEN \
|
|
138
|
+
-u ANTHROPIC_API_KEY \
|
|
139
|
+
-u ANTHROPIC_AUTH_TOKEN \
|
|
140
|
+
-u ANTHROPIC_BASE_URL \
|
|
141
|
+
-u CLAUDE_CODE_USE_BEDROCK \
|
|
142
|
+
-u CLAUDE_CODE_USE_VERTEX \
|
|
143
|
+
-u CLAUDE_CODE_USE_FOUNDRY \
|
|
144
|
+
claude auth status 2>/dev/null || true)"
|
|
145
|
+
if printf '%s' "$estado" | grep -Eq '"loggedIn"[[:space:]]*:[[:space:]]*true' \
|
|
146
|
+
&& printf '%s' "$estado" | grep -Eq '"authMethod"[[:space:]]*:[[:space:]]*"claude\.ai"'; then
|
|
147
|
+
printf '%s' 'env -u CLAUDE_CODE_OAUTH_TOKEN -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL -u CLAUDE_CODE_USE_BEDROCK -u CLAUDE_CODE_USE_VERTEX -u CLAUDE_CODE_USE_FOUNDRY '
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
CLAUDE_AUTH_PREFIX=""
|
|
151
|
+
[ "$RUN_CLAUDE" -eq 1 ] && CLAUDE_AUTH_PREFIX="$(claude_auth_prefix)"
|
|
152
|
+
|
|
153
|
+
# The cage. Yolo answers "does the agent ask permission?"; the cage answers
|
|
154
|
+
# "what can it touch?". They are independent, so yolo stays and each repo
|
|
155
|
+
# window is wrapped in a per-window macOS seatbelt profile: writes only in its
|
|
156
|
+
# own repo and runtime state, and the credential files (`~/.ssh`,
|
|
157
|
+
# `~/.git-credentials`, gh/cloud configs) sealed at the kernel. On a machine
|
|
158
|
+
# without seatbelt, or with CITY_CAGE=0, the prefix is empty and nothing
|
|
159
|
+
# changes. The seat is deliberately NOT caged: it is the permissioned side.
|
|
160
|
+
CAGE="$(dirname "$0")/cage.py"
|
|
161
|
+
BROKERPY="$(dirname "$0")/broker.py"
|
|
162
|
+
WORKSPACE="$(dirname "$0")/workspace.py"
|
|
163
|
+
jaula_de() { # window, cwd, [broker token file], [colon-joined mount targets] -> prefix or ''
|
|
164
|
+
local args=(line --window "$1" --repo "$2")
|
|
165
|
+
[ -n "${3:-}" ] && args+=(--token-file "$3")
|
|
166
|
+
[ -n "${4:-}" ] && args+=(--mounts "$4")
|
|
167
|
+
python3 "$CAGE" "${args[@]}" 2>/dev/null || true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Which model and effort a window's agent starts with. Three voices, in order:
|
|
171
|
+
# this launch's flags (CITY_MODEL/CITY_EFFORT, set by `seat --model/--effort`),
|
|
172
|
+
# then the card's per-window key (`model.dbt:`), then the card's default
|
|
173
|
+
# (`model:`). Nothing set means no flag at all — the owner's Claude default,
|
|
174
|
+
# which is the right silence.
|
|
175
|
+
motor_de() { # $1 = window name -> prints the extra claude flags
|
|
176
|
+
local ventana="$1" modelo esfuerzo salida=""
|
|
177
|
+
modelo="${CITY_MODEL:-$(python3 "$LEER" "$FICHA" "model.$ventana")}"
|
|
178
|
+
[ -z "$modelo" ] && modelo="$(python3 "$LEER" "$FICHA" model)"
|
|
179
|
+
esfuerzo="${CITY_EFFORT:-$(python3 "$LEER" "$FICHA" "effort.$ventana")}"
|
|
180
|
+
[ -z "$esfuerzo" ] && esfuerzo="$(python3 "$LEER" "$FICHA" effort)"
|
|
181
|
+
[ -n "$modelo" ] && salida=" --model $modelo"
|
|
182
|
+
[ -n "$esfuerzo" ] && salida="$salida --effort $esfuerzo"
|
|
183
|
+
printf '%s' "$salida"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
runtime_de() { # configured command -> claude | codex | opencode | kimi | terminal | unknown
|
|
187
|
+
local raw="$1" first
|
|
188
|
+
case "$raw" in terminal:*) printf 'terminal'; return ;; esac
|
|
189
|
+
first="${raw%% *}"
|
|
190
|
+
first="${first##*/}"
|
|
191
|
+
case "$first" in
|
|
192
|
+
claude|claude-code) printf 'claude' ;;
|
|
193
|
+
codex) printf 'codex' ;;
|
|
194
|
+
opencode) printf 'opencode' ;;
|
|
195
|
+
kimi|kimi-code) printf 'kimi' ;;
|
|
196
|
+
*) printf 'unknown' ;;
|
|
197
|
+
esac
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
gateway_line() { # actor, cwd, full runtime command, optional auto-approve
|
|
201
|
+
printf '%q ' "$RUNTIME" gateway "$1" "$2" "$3" "${4:-$YOLO}"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# Terminal emulators and tmux both have finite input queues. Sending a 1-2 KB
|
|
205
|
+
# shell program as simulated keystrokes can cut it at an arbitrary byte (we saw
|
|
206
|
+
# `--da`, `--dangerously`, and even a lone `-`). Put the exact command in a
|
|
207
|
+
# private launcher and type only its short path into the pane.
|
|
208
|
+
lanza() { # tmux target, actor, cwd, full command
|
|
209
|
+
local target="$1" actor="$2" cwd="$3" command="$4" launcher typed
|
|
210
|
+
launcher="$(python3 "$LAUNCHPY" create --data "$EQUIPO" --actor "$actor" \
|
|
211
|
+
--cwd "$cwd" --client "$CHANNEL_CLIENT" --command "$command")" || return 1
|
|
212
|
+
printf -v typed '%q' "$launcher"
|
|
213
|
+
tmux send-keys -t "$target" -l -- "$typed"
|
|
214
|
+
tmux send-keys -t "$target" C-m
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# ── Claude windows start one at a time ─────────────────────────────────────
|
|
218
|
+
# Every Claude session on a machine shares one OAuth credential, and refreshing
|
|
219
|
+
# it rotates a single-use refresh token: the first process to refresh wins and
|
|
220
|
+
# the rest are left holding one the server has already invalidated. It does not
|
|
221
|
+
# read as an auth problem — it reads as "you have no quota left" on an account
|
|
222
|
+
# with plenty, and nothing but logging out and back in clears it. Upstream has
|
|
223
|
+
# had it reported many times over (claude-code#24317, #25609, #27933, #48786).
|
|
224
|
+
#
|
|
225
|
+
# One window per repo makes this the worst possible caller: eighteen sessions
|
|
226
|
+
# starting in the same millisecond, all refreshing at once. So the seat's Claude
|
|
227
|
+
# goes first and alone, and every repo window holds its own shell for a moment
|
|
228
|
+
# before starting. The wait happens INSIDE the window, so attaching is still
|
|
229
|
+
# instant and you watch the agents wake one by one; it also spaces out eighteen
|
|
230
|
+
# simultaneous `git fetch`es, which were their own small storm.
|
|
231
|
+
#
|
|
232
|
+
# What this does not fix: a token that expires at four in the afternoon expires
|
|
233
|
+
# for every session already open. That one is upstream's, not ours.
|
|
234
|
+
#
|
|
235
|
+
# CITY_SETTLE=0 CITY_STAGGER=0 everything at once, as it was before
|
|
236
|
+
entero() { # $1 = value, $2 = fallback. A typo must not crash somebody's day.
|
|
237
|
+
case "$1" in
|
|
238
|
+
'' | *[!0-9]*) printf '%s' "$2" ;;
|
|
239
|
+
*) printf '%s' "$1" ;;
|
|
240
|
+
esac
|
|
241
|
+
}
|
|
242
|
+
SETTLE="$(entero "${CITY_SETTLE-}" 8)"
|
|
243
|
+
STAGGER="$(entero "${CITY_STAGGER-}" 1)"
|
|
244
|
+
|
|
245
|
+
# Seconds the Nth repo window waits. The first carries the whole settle, because
|
|
246
|
+
# it is the one that would collide with the seat's refresh; after that a token is
|
|
247
|
+
# already fresh in the store and the rest only need to not arrive as a herd. A
|
|
248
|
+
# negative turn means nobody has refreshed yet — the seat runs something else, so
|
|
249
|
+
# this window IS the first Claude and has nothing to wait for.
|
|
250
|
+
retraso() { # $1 = claude repo windows already scheduled
|
|
251
|
+
if [ "$1" -lt 0 ]; then printf '0'; else printf '%s' "$((SETTLE + $1 * STAGGER))"; fi
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
sync_line() {
|
|
255
|
+
[ "$DO_SYNC" -eq 1 ] || return 0
|
|
256
|
+
printf '%s' 'if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git fetch origin -q --prune 2>/dev/null; b=$(for x in main master; do git show-ref -q --verify refs/remotes/origin/$x && echo $x && break; done); [ -n "$b" ] && { git checkout -q "$b" 2>/dev/null; git pull --ff-only -q origin "$b" 2>/dev/null && echo " ✓ $b up to date" || echo " ⚠ could not update"; } || echo " · no main/master on origin"; fi; '
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# ── tmux comforts ──────────────────────────────────────────────────────────
|
|
260
|
+
# Applied to the tmux server, not to anybody's ~/.tmux.conf: this does not touch
|
|
261
|
+
# your configuration. They are additions, so they sit alongside whatever you run.
|
|
262
|
+
comodidades() {
|
|
263
|
+
# Mouse: click a window name in the bar to switch, click a pane to focus it,
|
|
264
|
+
# wheel for the scrollback.
|
|
265
|
+
tmux set-option -g mouse on 2>/dev/null
|
|
266
|
+
|
|
267
|
+
# Windows numbered from 1, so Alt+1 is the first one and not the second.
|
|
268
|
+
tmux set-option -g base-index 1 2>/dev/null
|
|
269
|
+
tmux set-option -g renumber-windows on 2>/dev/null
|
|
270
|
+
|
|
271
|
+
# Alt+1..9 jumps to that window, no prefix.
|
|
272
|
+
for n in 1 2 3 4 5 6 7 8 9; do
|
|
273
|
+
tmux bind-key -n "M-$n" select-window -t "$n" 2>/dev/null
|
|
274
|
+
done
|
|
275
|
+
# Alt+left / Alt+right to move in order.
|
|
276
|
+
tmux bind-key -n M-Left previous-window 2>/dev/null
|
|
277
|
+
tmux bind-key -n M-Right next-window 2>/dev/null
|
|
278
|
+
|
|
279
|
+
# Make the status bar say where you are.
|
|
280
|
+
tmux set-option -g status on 2>/dev/null
|
|
281
|
+
tmux set-option -g status-left '[#S] ' 2>/dev/null
|
|
282
|
+
tmux set-option -g status-left-length 20 2>/dev/null
|
|
283
|
+
tmux set-option -g window-status-current-style 'bg=colour4,fg=black,bold' 2>/dev/null
|
|
284
|
+
|
|
285
|
+
# Which window wants you. With one agent per repo you are looking at one window
|
|
286
|
+
# and five are working, so the useful signal is not "what is on screen" but
|
|
287
|
+
# "which tab needs me". Claude rings the terminal bell when it is waiting on you,
|
|
288
|
+
# so bell is what means come here: that tab turns red and stays red until you
|
|
289
|
+
# visit it. Activity is the softer one — output moved — and only underlines.
|
|
290
|
+
#
|
|
291
|
+
# No plugin manager and no third-party plugins for this: it is a handful of tmux
|
|
292
|
+
# options, and installing somebody else's tmux config to get a coloured tab is a
|
|
293
|
+
# bad trade.
|
|
294
|
+
#
|
|
295
|
+
# `-gw`, not `-g`. These four are *window* options, and setting them as server
|
|
296
|
+
# options fails — silently, because of the 2>/dev/null. Half of this block was
|
|
297
|
+
# doing nothing at all: monitor-activity stayed off and the styles stayed default.
|
|
298
|
+
tmux set-option -gw monitor-bell on 2>/dev/null
|
|
299
|
+
tmux set-option -gw monitor-activity on 2>/dev/null
|
|
300
|
+
tmux set-option -gw window-status-bell-style 'fg=colour231,bg=colour160,bold' 2>/dev/null
|
|
301
|
+
tmux set-option -gw window-status-activity-style 'fg=colour222,underscore' 2>/dev/null
|
|
302
|
+
# Session options: where the bell is allowed to come from, and no flashing bar
|
|
303
|
+
# over the pane you are reading.
|
|
304
|
+
tmux set-option -g bell-action other 2>/dev/null
|
|
305
|
+
tmux set-option -g visual-bell off 2>/dev/null
|
|
306
|
+
tmux set-option -g visual-activity off 2>/dev/null
|
|
307
|
+
# A clock, so a frozen status bar is visibly frozen. tmux expands strftime itself,
|
|
308
|
+
# so this needs no shell — `#(date ...)` here spawns a process every interval and
|
|
309
|
+
# was being written with a printf-escaped %% that tmux never understood.
|
|
310
|
+
tmux set-option -g status-interval 5 2>/dev/null
|
|
311
|
+
tmux set-option -g status-right ' %H:%M ' 2>/dev/null
|
|
312
|
+
tmux set-option -g status-right-length 12 2>/dev/null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# ── Work out where each agent works ────────────────────────────────────────
|
|
316
|
+
# Agents come first. A card that declares `agents:` drives the agent-first
|
|
317
|
+
# model: each agent's cwd is its workspace folder, and its mounts (symlinks to
|
|
318
|
+
# repos, worktrees or document folders) become the extra writable roots the
|
|
319
|
+
# cage allows. A legacy card with only `repos:` takes the path below, unchanged
|
|
320
|
+
# — every repo is still an agent whose one mount is that repo.
|
|
321
|
+
LOCALIZA="$(dirname "$0")/find-repos.sh"
|
|
322
|
+
faltan=()
|
|
323
|
+
RUTAS=()
|
|
324
|
+
NOMBRES=()
|
|
325
|
+
MONTAJES=()
|
|
326
|
+
AGENTS_RAW="$(python3 "$LEER" "$FICHA" agents 2>/dev/null || true)"
|
|
327
|
+
|
|
328
|
+
if [ -n "$AGENTS_RAW" ] && [ -z "$SOLO" ]; then
|
|
329
|
+
# Agent-first: one `sync-all` pass reads the card once, materialises every
|
|
330
|
+
# agent's workspace and mounts, and emits `slug<TAB>cwd<TAB>targets` per line —
|
|
331
|
+
# so the whole city costs one interpreter start and one card parse, not N+1.
|
|
332
|
+
while IFS=$'\t' read -r slug cwd targets; do
|
|
333
|
+
[ -z "$slug" ] && continue
|
|
334
|
+
RUTAS+=("$cwd")
|
|
335
|
+
NOMBRES+=("$slug")
|
|
336
|
+
MONTAJES+=("$targets")
|
|
337
|
+
# stderr is left attached so workspace.py's "skipping mount <x>" warnings
|
|
338
|
+
# reach the owner — a typo'd or missing mount source must not degrade in
|
|
339
|
+
# silence, the way the legacy path reports missing repos via `faltan`.
|
|
340
|
+
done < <(python3 "$WORKSPACE" sync-all --data "$EQUIPO" --card "$FICHA")
|
|
341
|
+
else
|
|
342
|
+
REPOS=()
|
|
343
|
+
# bash 3.2 — the one macOS ships — blows up on "${REPOS[@]}" when it is empty.
|
|
344
|
+
[ -n "$REPOS_RAW" ] && IFS=',' read -ra REPOS <<< "$REPOS_RAW"
|
|
345
|
+
for r in ${REPOS[@]+"${REPOS[@]}"}; do
|
|
346
|
+
r="$(echo "$r" | xargs)"; [ -z "$r" ] && continue
|
|
347
|
+
# 1) the usual place; 2) wherever you keep it, found by its remote.
|
|
348
|
+
path="$CODE_DIR/$r"
|
|
349
|
+
[ -d "$path" ] || path="$("$LOCALIZA" "$r" 2>/dev/null)"
|
|
350
|
+
if [ -z "$path" ] || [ ! -d "$path" ]; then faltan+=("$r"); continue; fi
|
|
351
|
+
RUTAS+=("$path")
|
|
352
|
+
# One canonical slug is also the tmux name, engine-key suffix and bus actor.
|
|
353
|
+
NOMBRES+=("$(python3 "$LEER" --window "$r")")
|
|
354
|
+
MONTAJES+=("")
|
|
355
|
+
done
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Trust every folder up front, so no window sits waiting on the dialog.
|
|
359
|
+
python3 "$(dirname "$0")/trust-repos.py" "$EQUIPO" ${RUTAS[@]+"${RUTAS[@]}"}
|
|
360
|
+
|
|
361
|
+
# ── The seat window: this city's identity on its roads ─────────────────────
|
|
362
|
+
"$RUNTIME" ensure >/dev/null
|
|
363
|
+
comodidades
|
|
364
|
+
tmux new-session -d -s "$SESSION" -n seat -c "$EQUIPO"
|
|
365
|
+
|
|
366
|
+
# Where the cards live, told to the session rather than assumed.
|
|
367
|
+
#
|
|
368
|
+
# A window inherits the environment of the tmux *server*, not of whoever ran this.
|
|
369
|
+
# So on any machine where a tmux server was already up, AGENTS_CITY_DATA never
|
|
370
|
+
# reached the windows and the plugin inside them looked for the cards in the
|
|
371
|
+
# default place. Set on the session, and also written in front of each command
|
|
372
|
+
# below, because the two cost nothing and the failure is silent.
|
|
373
|
+
tmux set-environment -t "$SESSION" AGENTS_CITY_DATA "$EQUIPO" 2>/dev/null
|
|
374
|
+
tmux set-environment -t "$SESSION" AGENTS_CITY_HOME "${AGENTS_CITY_HOME:-$HOME/.agents-city}" 2>/dev/null
|
|
375
|
+
tmux set-environment -t "$SESSION" AGENTS_CITY_USER "$USUARIO" 2>/dev/null
|
|
376
|
+
tmux set-environment -t "$SESSION" CITY_ADDRESS "$ADDRESS" 2>/dev/null
|
|
377
|
+
tmux set-environment -t "$SESSION" CITY_SEAT_NAME "$SEAT_NAME" 2>/dev/null
|
|
378
|
+
|
|
379
|
+
# The seat runs Claude unless the card says otherwise. `runs.seat` is the same key
|
|
380
|
+
# the repo windows use, and it makes a city with no Claude in it possible at all:
|
|
381
|
+
# what that seat gives up is the `/city:` commands, which are Claude's, and what it
|
|
382
|
+
# keeps is the folder, the identity, and everything the terminal does.
|
|
383
|
+
SEAT_OTRO="$(python3 "$LEER" "$FICHA" "runs.seat")"
|
|
384
|
+
turno=0
|
|
385
|
+
[ -n "$SEAT_OTRO" ] && [ "$(runtime_de "$SEAT_OTRO")" != claude ] && turno=-1
|
|
386
|
+
# If the seat runs another engine, the first repo window is the first Claude.
|
|
387
|
+
if [ "$RUN_CLAUDE" -eq 1 ]; then
|
|
388
|
+
if [ -n "$SEAT_OTRO" ]; then
|
|
389
|
+
SEAT_RUNTIME="$(runtime_de "$SEAT_OTRO")"
|
|
390
|
+
if [ "$SEAT_RUNTIME" = claude ]; then
|
|
391
|
+
CLAUDE_SEAT="$SEAT_OTRO --name $SEAT_NAME --settings '$SETTINGS'$NO_PEER_TOOLS$SEAT_YOLO_FLAG"
|
|
392
|
+
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
393
|
+
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE ${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$CLAUDE_SEAT" "$SEAT_AUTO")"
|
|
394
|
+
elif [ "$SEAT_RUNTIME" = codex ] || [ "$SEAT_RUNTIME" = opencode ] || [ "$SEAT_RUNTIME" = kimi ]; then
|
|
395
|
+
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
396
|
+
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE $(gateway_line seat "$EQUIPO" "$SEAT_OTRO" "$SEAT_AUTO")"
|
|
397
|
+
elif [ "$SEAT_RUNTIME" = terminal ]; then
|
|
398
|
+
SEAT_COMMAND="${SEAT_OTRO#terminal:}"
|
|
399
|
+
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
400
|
+
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE $SEAT_COMMAND"
|
|
401
|
+
"$RUNTIME" fallback seat "$SESSION:seat" "${SEAT_COMMAND%% *}"
|
|
402
|
+
else
|
|
403
|
+
tmux send-keys -t "$SESSION:seat" \
|
|
404
|
+
"echo 'No native Agents City gateway for: $SEAT_OTRO. Use terminal:$SEAT_OTRO only if you explicitly accept terminal injection.'" C-m
|
|
405
|
+
fi
|
|
406
|
+
else
|
|
407
|
+
CLAUDE_SEAT="claude --name $SEAT_NAME$(motor_de seat) --settings '$SETTINGS'$NO_PEER_TOOLS$SEAT_YOLO_FLAG"
|
|
408
|
+
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
409
|
+
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE ${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$CLAUDE_SEAT" "$SEAT_AUTO")"
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
|
|
413
|
+
# The broker, opt-in with CITY_BROKER=1: caged windows cannot read the gh
|
|
414
|
+
# token — on purpose — so PRs and pushes go through a small owner-side process
|
|
415
|
+
# that validates (declared repo only, never the default branch) and audits.
|
|
416
|
+
# Each window gets its own token file, and its cage re-allows exactly that one.
|
|
417
|
+
BROKER_URL=""
|
|
418
|
+
if [ "${CITY_BROKER:-0}" = "1" ]; then
|
|
419
|
+
python3 "$BROKERPY" stop --data "$EQUIPO" >/dev/null 2>&1 || true
|
|
420
|
+
nohup python3 "$BROKERPY" serve --data "$EQUIPO" >/dev/null 2>&1 &
|
|
421
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
422
|
+
BROKER_URL="$(python3 "$BROKERPY" url --data "$EQUIPO" 2>/dev/null || true)"
|
|
423
|
+
[ -n "$BROKER_URL" ] && break
|
|
424
|
+
sleep 0.3
|
|
425
|
+
done
|
|
426
|
+
[ -z "$BROKER_URL" ] && echo "WARNING: the credential broker did not start; windows launch without it." >&2
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
# ── One window per repo, reachable only through its chaired bus identity ───
|
|
430
|
+
#
|
|
431
|
+
# Claude by default, with its engine keys and its yolo flag. But a window can
|
|
432
|
+
# run a supported agent CLI instead — `runs.dbt: codex` on the card. Claude uses
|
|
433
|
+
# its persistent stream-json process; Codex, OpenCode and Kimi use their native
|
|
434
|
+
# gateways. None receives assignments through tmux. An unknown CLI must be
|
|
435
|
+
# prefixed with `terminal:` to opt into the visibly labelled compatibility path.
|
|
436
|
+
i=0
|
|
437
|
+
for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
438
|
+
win="${NOMBRES[$i]}"
|
|
439
|
+
i=$((i + 1))
|
|
440
|
+
# Professional specialty is separate from bus authority. Every repo actor is
|
|
441
|
+
# still a member; only `seat` is chair. A malformed or legacy value resolves
|
|
442
|
+
# to the explicit blank role in read-card.py.
|
|
443
|
+
ROL_REPO="$(python3 "$LEER" --actor-role "$FICHA" "$win")"
|
|
444
|
+
# This window's cage and, when the broker runs, its own single-repo token.
|
|
445
|
+
# The token travels as a file path, never on the command line, and the cage
|
|
446
|
+
# profile re-allows reading exactly that file and nothing else in the broker.
|
|
447
|
+
TOKEN_FILE=""
|
|
448
|
+
BROKER_ENV=""
|
|
449
|
+
if [ -n "$BROKER_URL" ]; then
|
|
450
|
+
TOKEN_FILE="$(python3 "$BROKERPY" mint --data "$EQUIPO" "$win" --repo "$path" --file-only 2>/dev/null || true)"
|
|
451
|
+
[ -n "$TOKEN_FILE" ] && BROKER_ENV="CITY_BROKER_URL=$BROKER_URL CITY_BROKER_TOKEN_FILE=$TOKEN_FILE "
|
|
452
|
+
fi
|
|
453
|
+
MOUNTS_WIN="${MONTAJES[$((i - 1))]:-}"
|
|
454
|
+
JAULA="$(jaula_de "$win" "$path" "$TOKEN_FILE" "$MOUNTS_WIN")"
|
|
455
|
+
CAGE_RUNTIME_ENV=""
|
|
456
|
+
[ -n "$JAULA" ] && CAGE_RUNTIME_ENV="env CITY_OUTER_CAGE=1 "
|
|
457
|
+
tmux new-window -t "$SESSION" -n "$win" -c "$path"
|
|
458
|
+
if [ "$RUN_CLAUDE" -eq 1 ]; then
|
|
459
|
+
OTRO="$(python3 "$LEER" "$FICHA" "runs.$win")"
|
|
460
|
+
if [ -n "$OTRO" ]; then
|
|
461
|
+
KIND="$(runtime_de "$OTRO")"
|
|
462
|
+
if [ "$KIND" = claude ]; then
|
|
463
|
+
espera="$(retraso "$turno")"
|
|
464
|
+
turno=$((turno + 1))
|
|
465
|
+
if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
|
|
466
|
+
CLAUDE_REPO="$OTRO --name $SESSION-$win --settings '$SETTINGS'$NO_PEER_TOOLS$YOLO_FLAG"
|
|
467
|
+
lanza "$SESSION:$win" "$win" "$path" \
|
|
468
|
+
"$espera$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV$JAULA$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
|
|
469
|
+
elif [ "$KIND" = codex ] || [ "$KIND" = opencode ] || [ "$KIND" = kimi ]; then
|
|
470
|
+
# Native servers have their own credentials and do not share Claude's OAuth race.
|
|
471
|
+
# Codex must remain outside the outer macOS seatbelt: its node_repl MCP
|
|
472
|
+
# applies its own sandbox and macOS rejects that nested sandbox_apply.
|
|
473
|
+
# Codex's app-server receives workspace-write below; OpenCode and Kimi
|
|
474
|
+
# still use the per-repo outer cage.
|
|
475
|
+
RUNTIME_CAGE="$JAULA"
|
|
476
|
+
RUNTIME_CAGE_ENV="$CAGE_RUNTIME_ENV"
|
|
477
|
+
if [ "$KIND" = codex ]; then
|
|
478
|
+
RUNTIME_CAGE=""
|
|
479
|
+
RUNTIME_CAGE_ENV=""
|
|
480
|
+
fi
|
|
481
|
+
lanza "$SESSION:$win" "$win" "$path" \
|
|
482
|
+
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV$RUNTIME_CAGE$RUNTIME_CAGE_ENV$(gateway_line "$win" "$path" "$OTRO")"
|
|
483
|
+
elif [ "$KIND" = terminal ]; then
|
|
484
|
+
FALLBACK_COMMAND="${OTRO#terminal:}"
|
|
485
|
+
lanza "$SESSION:$win" "$win" "$path" \
|
|
486
|
+
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV$JAULA$FALLBACK_COMMAND"
|
|
487
|
+
"$RUNTIME" fallback "$win" "$SESSION:$win" "${FALLBACK_COMMAND%% *}"
|
|
488
|
+
else
|
|
489
|
+
tmux send-keys -t "$SESSION:$win" \
|
|
490
|
+
"echo 'No native Agents City gateway for: $OTRO. Use terminal:$OTRO only if you explicitly accept terminal injection.'" C-m
|
|
491
|
+
fi
|
|
492
|
+
else
|
|
493
|
+
espera="$(retraso "$turno")"
|
|
494
|
+
turno=$((turno + 1))
|
|
495
|
+
if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
|
|
496
|
+
CLAUDE_REPO="claude --name $SESSION-$win$(motor_de "$win") --settings '$SETTINGS'$NO_PEER_TOOLS$YOLO_FLAG"
|
|
497
|
+
lanza "$SESSION:$win" "$win" "$path" \
|
|
498
|
+
"$espera$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=$win CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= $BROKER_ENV${JAULA}$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
|
|
499
|
+
fi
|
|
500
|
+
fi
|
|
501
|
+
done
|
|
502
|
+
|
|
503
|
+
if [ ${#faltan[@]-0} -gt 0 ]; then
|
|
504
|
+
echo >&2
|
|
505
|
+
echo "I could not find these repos on this machine (window skipped):" >&2
|
|
506
|
+
printf ' %s\n' "${faltan[@]}" >&2
|
|
507
|
+
echo >&2
|
|
508
|
+
echo "If they live somewhere unusual: CITY_SEARCH_IN=/where/they/are $(basename "$0")" >&2
|
|
509
|
+
echo "If you simply do not have them, clone them anywhere and run this again." >&2
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
if [ "${#RUTAS[@]}" -eq 0 ]; then
|
|
513
|
+
# Not "yet". A role that owns no folders is a real answer, not an unfinished
|
|
514
|
+
# setup: the architect, the surveyor and the land surveyor own a property of
|
|
515
|
+
# everybody else's folders and none of their own. What they are for begins when
|
|
516
|
+
# there are other cities to reach. Keyed on the windows actually opened, so an
|
|
517
|
+
# agent-first card with agents but no `repos:` field does not trip it.
|
|
518
|
+
echo "You own no folders, so this is the seat window on its own — which is what" >&2
|
|
519
|
+
echo "a $AGENTE day looks like. Your role's work starts when there are other" >&2
|
|
520
|
+
echo "cities to reach: that is the bus." >&2
|
|
521
|
+
echo >&2
|
|
522
|
+
echo "If that is wrong and you do own folders: ./bin/seat --repos" >&2
|
|
523
|
+
fi
|
|
524
|
+
|
|
525
|
+
tmux select-window -t "$SESSION:seat"
|
|
526
|
+
exec tmux attach -d -t "$SESSION"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Where a seat's settings come from, for the Python side of the plugin.
|
|
3
|
+
|
|
4
|
+
The shell has scripts/city-env.sh; this is the same order for the same keys, so
|
|
5
|
+
there is one answer to "which city, which token, which data" and not one per
|
|
6
|
+
script. Both reporters used to carry their own copy, and a copy is a thing that
|
|
7
|
+
drifts.
|
|
8
|
+
|
|
9
|
+
Order: the environment wins — that is how the MCP server passes user config —
|
|
10
|
+
then the .env the setup writes, then the OS keychain for the token alone.
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
import cities
|
|
16
|
+
|
|
17
|
+
CANAL = os.environ.get('CITY_DIR', os.path.expanduser('~/.claude/channels/city-bus'))
|
|
18
|
+
LLAVERO = 'city@agents-city'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fichero():
|
|
22
|
+
"""The keys in ~/.claude/channels/city-bus/.env, or an empty dict."""
|
|
23
|
+
d = {}
|
|
24
|
+
try:
|
|
25
|
+
with open(os.path.join(CANAL, '.env')) as f:
|
|
26
|
+
for linea in f:
|
|
27
|
+
if '=' in linea and not linea.strip().startswith('#'):
|
|
28
|
+
k, v = linea.split('=', 1)
|
|
29
|
+
d[k.strip()] = v.strip().strip('"').strip("'")
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
return d
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ajuste(clave, defecto=''):
|
|
36
|
+
return os.environ.get(clave) or fichero().get(clave) or defecto
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def url():
|
|
40
|
+
"""The city's URL. Falls back to the bus's, because unless somebody split
|
|
41
|
+
them they are the same Worker."""
|
|
42
|
+
return ajuste('AGENTS_CITY_URL') or ajuste('CITY_BUS_URL')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def datos():
|
|
46
|
+
"""The selected personal city; an explicit external folder still wins."""
|
|
47
|
+
puesto = ajuste('AGENTS_CITY_DATA')
|
|
48
|
+
if puesto and os.path.isdir(os.path.expanduser(puesto)):
|
|
49
|
+
return os.path.expanduser(puesto)
|
|
50
|
+
return cities.actual(os.environ.get('AGENTS_CITY_USER') or cities.usuario_actual())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def token(dado=''):
|
|
54
|
+
"""The bus token: given, from the environment, from .env, or the keychain."""
|
|
55
|
+
if dado:
|
|
56
|
+
return dado
|
|
57
|
+
v = ajuste('CITY_BUS_TOKEN')
|
|
58
|
+
if v:
|
|
59
|
+
return v
|
|
60
|
+
try:
|
|
61
|
+
r = subprocess.run(['security', 'find-generic-password', '-s', LLAVERO, '-w'],
|
|
62
|
+
capture_output=True, text=True)
|
|
63
|
+
if r.returncode == 0:
|
|
64
|
+
return r.stdout.strip()
|
|
65
|
+
except (OSError, FileNotFoundError):
|
|
66
|
+
pass
|
|
67
|
+
return ''
|