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,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reset one managed city to first-run state, recoverably.
|
|
3
|
+
|
|
4
|
+
The target is always explicit. Repos are references and are never touched. The
|
|
5
|
+
city folder and its local map state move to a timestamped backup, incident local
|
|
6
|
+
roads are removed at both ends, and an empty city with the same stable identity
|
|
7
|
+
is created in its place. The next ``agents-city seat --city <name>`` runs the
|
|
8
|
+
five onboarding questions again.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
import cities
|
|
19
|
+
import domains
|
|
20
|
+
import roads
|
|
21
|
+
import runtime_processes
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _backup_path(usuario, datos):
|
|
25
|
+
sello = time.strftime("%Y%m%d-%H%M%S")
|
|
26
|
+
base = os.path.join(cities.carpeta_usuario(usuario), ".backups")
|
|
27
|
+
nombre = f"{cities.slug_ciudad(datos)}-{sello}"
|
|
28
|
+
ruta = os.path.join(base, nombre)
|
|
29
|
+
n = 2
|
|
30
|
+
while os.path.exists(ruta):
|
|
31
|
+
ruta = os.path.join(base, f"{nombre}-{n}")
|
|
32
|
+
n += 1
|
|
33
|
+
return ruta
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _deten(datos, dry_run=False):
|
|
37
|
+
guion = os.path.join(os.path.dirname(__file__), "apaga.py")
|
|
38
|
+
args = [sys.executable, guion, datos]
|
|
39
|
+
if dry_run:
|
|
40
|
+
args.append("--dry-run")
|
|
41
|
+
return subprocess.run(args, capture_output=True, text=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _valida(datos, usuario):
|
|
45
|
+
usuario = usuario or cities.usuario_actual()
|
|
46
|
+
datos = os.path.realpath(datos)
|
|
47
|
+
if not cities.gestionada(datos, usuario):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"reset only accepts a managed city under "
|
|
50
|
+
f"{cities.carpeta_usuario(usuario)}; external/shared "
|
|
51
|
+
"folders are never erased"
|
|
52
|
+
)
|
|
53
|
+
if not cities.es_ciudad(datos):
|
|
54
|
+
raise ValueError(f"{datos} is not a city")
|
|
55
|
+
owner_en_disco = cities.lee_clave(datos, "owner")
|
|
56
|
+
if owner_en_disco and owner_en_disco != usuario:
|
|
57
|
+
raise ValueError(f"this city belongs to {owner_en_disco!r}, not {usuario!r}")
|
|
58
|
+
return datos, usuario
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _contexto(datos, usuario):
|
|
62
|
+
return {
|
|
63
|
+
"datos": datos,
|
|
64
|
+
"usuario": usuario,
|
|
65
|
+
"ident": cities.identidad(datos),
|
|
66
|
+
"nombre": cities.nombre(datos),
|
|
67
|
+
"slug": cities.slug_ciudad(datos),
|
|
68
|
+
"owner": cities.lee_clave(datos, "owner") or usuario,
|
|
69
|
+
"domain": domains.de_ciudad(datos),
|
|
70
|
+
"grow": cities.lee_clave(
|
|
71
|
+
datos, "grow_command", "git log --since=30.days --oneline | wc -l"
|
|
72
|
+
),
|
|
73
|
+
"backup": _backup_path(usuario, datos),
|
|
74
|
+
"estado": os.path.join(cities.raiz(), "state", cities.slug(datos)),
|
|
75
|
+
"runtime": runtime_processes.ruta(datos),
|
|
76
|
+
"roads": roads.lee(datos),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _preview(ctx):
|
|
81
|
+
print(f" Would reset city `{ctx['slug']}` at {ctx['datos']}")
|
|
82
|
+
print(f" Would preserve it at {ctx['backup']}")
|
|
83
|
+
print(" Would leave every referenced repo untouched")
|
|
84
|
+
if ctx["roads"]:
|
|
85
|
+
print(f" Would close {len(ctx['roads'])} road(s) at both ends")
|
|
86
|
+
parada = _deten(ctx["datos"], dry_run=True)
|
|
87
|
+
if parada.stdout.strip():
|
|
88
|
+
print(parada.stdout.rstrip())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _para(datos):
|
|
92
|
+
parada = _deten(datos)
|
|
93
|
+
if parada.returncode != 0:
|
|
94
|
+
detalle = (parada.stderr or parada.stdout or "unknown stop error").strip()
|
|
95
|
+
raise ValueError(f"could not stop the city before reset: {detalle}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cierra_carreteras(ctx):
|
|
99
|
+
"""Back up and remove the other half of every incident local road."""
|
|
100
|
+
backlinks = os.path.join(ctx["backup"], "road-backlinks")
|
|
101
|
+
for camino in ctx["roads"]:
|
|
102
|
+
otro = roads.destino_local(camino, ctx["usuario"])
|
|
103
|
+
if not otro:
|
|
104
|
+
continue
|
|
105
|
+
os.makedirs(backlinks, exist_ok=True)
|
|
106
|
+
if os.path.isfile(roads.ruta(otro)):
|
|
107
|
+
destino = os.path.join(backlinks, f"{cities.slug_ciudad(otro)}.roads.json")
|
|
108
|
+
shutil.copy2(roads.ruta(otro), destino)
|
|
109
|
+
roads.desconecta(otro, ctx["ident"])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _mueve_si_existe(origen, destino):
|
|
113
|
+
if os.path.exists(origen):
|
|
114
|
+
shutil.move(origen, destino)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _respalda(ctx):
|
|
118
|
+
os.makedirs(ctx["backup"], exist_ok=False)
|
|
119
|
+
_cierra_carreteras(ctx)
|
|
120
|
+
shutil.move(ctx["datos"], os.path.join(ctx["backup"], "city"))
|
|
121
|
+
_mueve_si_existe(ctx["estado"], os.path.join(ctx["backup"], "state"))
|
|
122
|
+
_mueve_si_existe(ctx["runtime"], os.path.join(ctx["backup"], "runtime"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _crea_vacia(ctx):
|
|
126
|
+
datos = ctx["datos"]
|
|
127
|
+
os.makedirs(datos, exist_ok=True)
|
|
128
|
+
cities.asegura_metadata(datos, ctx["owner"], ctx["nombre"], ctx["ident"])
|
|
129
|
+
# Keep stable identity and growth measure, but no seat, agents, repos or roads.
|
|
130
|
+
city_yml = os.path.join(datos, "city.yml")
|
|
131
|
+
texto = open(city_yml, encoding="utf-8").read()
|
|
132
|
+
if "\ndomain:" not in "\n" + texto:
|
|
133
|
+
texto += f"domain: {ctx['domain']}\n"
|
|
134
|
+
if "\ngrow_command:" not in "\n" + texto:
|
|
135
|
+
texto += f"grow_command: {ctx['grow']}\n"
|
|
136
|
+
cities.escribe_atomico(city_yml, texto)
|
|
137
|
+
roads.escribe(datos, [])
|
|
138
|
+
cities.selecciona(ctx["usuario"], datos)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def reinicia(datos, usuario="", dry_run=False):
|
|
142
|
+
datos, usuario = _valida(datos, usuario)
|
|
143
|
+
ctx = _contexto(datos, usuario)
|
|
144
|
+
if dry_run:
|
|
145
|
+
_preview(ctx)
|
|
146
|
+
return ctx["backup"]
|
|
147
|
+
|
|
148
|
+
_para(datos)
|
|
149
|
+
_respalda(ctx)
|
|
150
|
+
_crea_vacia(ctx)
|
|
151
|
+
return ctx["backup"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main():
|
|
155
|
+
ap = argparse.ArgumentParser(
|
|
156
|
+
description="Reset one city to onboarding; repos stay untouched and data is backed up"
|
|
157
|
+
)
|
|
158
|
+
ap.add_argument("city", help="city name or path (required)")
|
|
159
|
+
ap.add_argument("--dry-run", action="store_true", help="show every effect, change nothing")
|
|
160
|
+
a = ap.parse_args()
|
|
161
|
+
usuario = cities.usuario_actual()
|
|
162
|
+
datos = cities.resuelve(a.city, usuario)
|
|
163
|
+
if not datos:
|
|
164
|
+
conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
|
|
165
|
+
print(f" No city called {a.city!r}. Known here: {conocidas}.", file=sys.stderr)
|
|
166
|
+
return 1
|
|
167
|
+
try:
|
|
168
|
+
backup = reinicia(datos, usuario, a.dry_run)
|
|
169
|
+
except ValueError as e:
|
|
170
|
+
print(f" {e}", file=sys.stderr)
|
|
171
|
+
return 1
|
|
172
|
+
if not a.dry_run:
|
|
173
|
+
corto = backup.replace(os.path.expanduser("~"), "~")
|
|
174
|
+
print(f" City `{cities.slug_ciudad(datos)}` reset. Recovery copy: {corto}")
|
|
175
|
+
print(f" Run `agents-city seat --city {cities.slug_ciudad(datos)}` for onboarding.")
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
sys.exit(main())
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explicit roads between autonomous cities.
|
|
3
|
+
|
|
4
|
+
Roads connect city seats, never users and never repo windows. A local road is
|
|
5
|
+
written at both ends. A remote road is added from a small public invitation;
|
|
6
|
+
the other end accepts this city's invitation independently. No token or skill
|
|
7
|
+
content is ever stored here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import card
|
|
16
|
+
import cities
|
|
17
|
+
import domains
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ADDRESS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}/[a-z0-9][a-z0-9_-]{0,63}$")
|
|
21
|
+
ID_RE = re.compile(r"^[A-Za-z0-9_.:-]{1,128}$")
|
|
22
|
+
INVITATION_KEYS = {"version", "id", "name", "owner", "address", "domain", "role"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _limpia(camino):
|
|
26
|
+
"""Validate untrusted road metadata and return its storage shape."""
|
|
27
|
+
if not isinstance(camino, dict):
|
|
28
|
+
return None
|
|
29
|
+
ident = str(camino.get("id", "")).strip()
|
|
30
|
+
address = str(camino.get("address", "")).strip().lower()
|
|
31
|
+
if not ID_RE.fullmatch(ident) or not ADDRESS_RE.fullmatch(address):
|
|
32
|
+
return None
|
|
33
|
+
dueno, slug = address.split("/", 1)
|
|
34
|
+
owner = str(camino.get("owner") or dueno).strip().lower()
|
|
35
|
+
if owner != dueno:
|
|
36
|
+
return None
|
|
37
|
+
name = " ".join(str(camino.get("name") or slug).split())[:80] or slug
|
|
38
|
+
limpio = {"id": ident, "name": name, "owner": owner, "address": address}
|
|
39
|
+
dominio = domains.canonico(camino.get("domain"))
|
|
40
|
+
rol = str(camino.get("role") or "").strip().lower()
|
|
41
|
+
if dominio and re.fullmatch(r"[a-z0-9-]{1,80}", dominio):
|
|
42
|
+
limpio["domain"] = dominio
|
|
43
|
+
if rol and re.fullmatch(r"[a-z0-9-]{1,80}", rol):
|
|
44
|
+
limpio["role"] = rol
|
|
45
|
+
if camino.get("local"):
|
|
46
|
+
limpio["local"] = True
|
|
47
|
+
return limpio
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ruta(datos):
|
|
51
|
+
return os.path.join(os.path.realpath(datos), "roads.json")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _atomico(ruta_, objeto):
|
|
55
|
+
os.makedirs(os.path.dirname(ruta_), exist_ok=True)
|
|
56
|
+
temporal = ruta_ + f".tmp-{os.getpid()}"
|
|
57
|
+
with open(temporal, "w", encoding="utf-8") as f:
|
|
58
|
+
json.dump(objeto, f, indent=2, ensure_ascii=False, sort_keys=True)
|
|
59
|
+
f.write("\n")
|
|
60
|
+
os.replace(temporal, ruta_)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def lee(datos):
|
|
64
|
+
try:
|
|
65
|
+
objeto = json.load(open(ruta(datos), encoding="utf-8"))
|
|
66
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
67
|
+
return []
|
|
68
|
+
caminos = objeto.get("roads", []) if isinstance(objeto, dict) else []
|
|
69
|
+
return [limpio for r in caminos if (limpio := _limpia(r)) is not None]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def escribe(datos, caminos):
|
|
73
|
+
limpios, ids, direcciones = [], set(), set()
|
|
74
|
+
for camino in caminos:
|
|
75
|
+
limpio = _limpia(camino)
|
|
76
|
+
if not limpio or limpio["id"] in ids or limpio["address"] in direcciones:
|
|
77
|
+
continue
|
|
78
|
+
ids.add(limpio["id"])
|
|
79
|
+
direcciones.add(limpio["address"])
|
|
80
|
+
limpios.append(limpio)
|
|
81
|
+
limpios.sort(key=lambda r: (r["owner"], r["name"].lower(), r["id"]))
|
|
82
|
+
_atomico(ruta(datos), {"version": 1, "roads": limpios})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def invitacion(datos, usuario=""):
|
|
86
|
+
usuario = usuario or cities.usuario_actual()
|
|
87
|
+
owner = cities.lee_clave(datos, "owner") or usuario
|
|
88
|
+
ficha = card.lee(os.path.join(datos, f"{owner}.md"))
|
|
89
|
+
invitacion_ = {
|
|
90
|
+
"version": 1,
|
|
91
|
+
"id": cities.identidad(datos),
|
|
92
|
+
"name": cities.nombre(datos),
|
|
93
|
+
"owner": owner,
|
|
94
|
+
"address": cities.direccion(usuario, datos),
|
|
95
|
+
"domain": domains.de_ciudad(datos),
|
|
96
|
+
}
|
|
97
|
+
if ficha.get("role"):
|
|
98
|
+
invitacion_["role"] = ficha["role"]
|
|
99
|
+
return invitacion_
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _pon(datos, destino, local=False):
|
|
103
|
+
caminos = [r for r in lee(datos) if r["id"] != destino["id"]]
|
|
104
|
+
nuevo = dict(destino)
|
|
105
|
+
if local:
|
|
106
|
+
nuevo["local"] = True
|
|
107
|
+
caminos.append(nuevo)
|
|
108
|
+
escribe(datos, caminos)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def conecta(origen, destino, usuario=""):
|
|
112
|
+
"""Connect two cities on this machine, symmetrically."""
|
|
113
|
+
usuario = usuario or cities.usuario_actual()
|
|
114
|
+
if cities.identidad(origen) == cities.identidad(destino):
|
|
115
|
+
raise ValueError("a city cannot build a road to itself")
|
|
116
|
+
_pon(origen, invitacion(destino, usuario), local=True)
|
|
117
|
+
_pon(destino, invitacion(origen, usuario), local=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def conecta_invitacion(origen, destino):
|
|
121
|
+
"""Add the local half of a road to a remote city invitation."""
|
|
122
|
+
necesarios = {"id", "name", "owner", "address"}
|
|
123
|
+
if not isinstance(destino, dict) or not necesarios <= set(destino):
|
|
124
|
+
raise ValueError("the invitation is missing id, name, owner or address")
|
|
125
|
+
if not set(destino) <= INVITATION_KEYS:
|
|
126
|
+
raise ValueError("the invitation contains fields other than public city identity")
|
|
127
|
+
limpia = _limpia(destino)
|
|
128
|
+
if not limpia:
|
|
129
|
+
raise ValueError("the invitation has invalid or inconsistent identity")
|
|
130
|
+
origen_publico = invitacion(origen)
|
|
131
|
+
if limpia["id"] == origen_publico["id"] or limpia["address"] == origen_publico["address"]:
|
|
132
|
+
raise ValueError("a city cannot build a road to itself")
|
|
133
|
+
_pon(origen, limpia, local=False)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def desconecta(origen, destino_id):
|
|
137
|
+
antes = lee(origen)
|
|
138
|
+
despues = [r for r in antes if r["id"] != destino_id]
|
|
139
|
+
escribe(origen, despues)
|
|
140
|
+
return len(antes) != len(despues)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def desconecta_local(origen, destino):
|
|
144
|
+
a = desconecta(origen, cities.identidad(destino))
|
|
145
|
+
b = desconecta(destino, cities.identidad(origen))
|
|
146
|
+
return a or b
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def quita_incidentes(datos, usuario=""):
|
|
150
|
+
"""Remove every local edge touching a city; used by a scoped reset."""
|
|
151
|
+
usuario = usuario or cities.usuario_actual()
|
|
152
|
+
ident = cities.identidad(datos)
|
|
153
|
+
for otra in cities.lista(usuario):
|
|
154
|
+
if os.path.realpath(otra["ruta"]) != os.path.realpath(datos):
|
|
155
|
+
desconecta(otra["ruta"], ident)
|
|
156
|
+
escribe(datos, [])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def destino_local(camino, usuario=""):
|
|
160
|
+
"""Resolve a road record to a city currently present on this machine."""
|
|
161
|
+
if not camino.get("local"):
|
|
162
|
+
return ""
|
|
163
|
+
usuario = usuario or cities.usuario_actual()
|
|
164
|
+
return next((c["ruta"] for c in cities.lista(usuario) if c["id"] == camino.get("id")), "")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resuelve(nombre, usuario):
|
|
168
|
+
datos = cities.resuelve(nombre, usuario)
|
|
169
|
+
if not datos:
|
|
170
|
+
conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
|
|
171
|
+
raise ValueError(f"no city called {nombre!r}; known here: {conocidas}")
|
|
172
|
+
return datos
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _ayuda():
|
|
176
|
+
return """usage:
|
|
177
|
+
agents-city road list <city>
|
|
178
|
+
agents-city road connect <city> <local-city|invitation.json>
|
|
179
|
+
agents-city road disconnect <city> <local-city|city-id>
|
|
180
|
+
agents-city road invite <city>
|
|
181
|
+
|
|
182
|
+
Roads are explicit. Connecting two local cities writes both ends. For a remote
|
|
183
|
+
city, exchange the public JSON printed by `road invite`; it contains no token."""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main():
|
|
187
|
+
args = sys.argv[1:]
|
|
188
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
189
|
+
print(_ayuda())
|
|
190
|
+
return 0
|
|
191
|
+
usuario = cities.usuario_actual()
|
|
192
|
+
try:
|
|
193
|
+
if args[0] == "list" and len(args) == 2:
|
|
194
|
+
origen = _resuelve(args[1], usuario)
|
|
195
|
+
caminos = lee(origen)
|
|
196
|
+
if not caminos:
|
|
197
|
+
print(f" {cities.nombre(origen)} has no roads.")
|
|
198
|
+
for r in caminos:
|
|
199
|
+
local = "local" if destino_local(r, usuario) else "remote"
|
|
200
|
+
print(f" {r['name']} {r['address']} [{local}]")
|
|
201
|
+
return 0
|
|
202
|
+
if args[0] == "invite" and len(args) == 2:
|
|
203
|
+
print(json.dumps(invitacion(_resuelve(args[1], usuario)), indent=2))
|
|
204
|
+
return 0
|
|
205
|
+
if args[0] == "connect" and len(args) == 3:
|
|
206
|
+
origen = _resuelve(args[1], usuario)
|
|
207
|
+
destino = cities.resuelve(args[2], usuario)
|
|
208
|
+
if destino:
|
|
209
|
+
conecta(origen, destino, usuario)
|
|
210
|
+
print(f" Road open: {cities.nombre(origen)} <-> {cities.nombre(destino)}")
|
|
211
|
+
else:
|
|
212
|
+
with open(os.path.expanduser(args[2]), encoding="utf-8") as f:
|
|
213
|
+
conecta_invitacion(origen, json.load(f))
|
|
214
|
+
print(f" Remote road added to {cities.nombre(origen)}.")
|
|
215
|
+
return 0
|
|
216
|
+
if args[0] == "disconnect" and len(args) == 3:
|
|
217
|
+
origen = _resuelve(args[1], usuario)
|
|
218
|
+
destino = cities.resuelve(args[2], usuario)
|
|
219
|
+
cambiado = desconecta_local(origen, destino) if destino else desconecta(origen, args[2])
|
|
220
|
+
print(" Road closed." if cambiado else " No such road.")
|
|
221
|
+
return 0
|
|
222
|
+
except (OSError, ValueError, json.JSONDecodeError) as e:
|
|
223
|
+
print(f" {e}", file=sys.stderr)
|
|
224
|
+
return 1
|
|
225
|
+
print(_ayuda(), file=sys.stderr)
|
|
226
|
+
return 1
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
sys.exit(main())
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The roles offered to an owner seat, across the built-in work domains.
|
|
3
|
+
|
|
4
|
+
The v2 road address is stable ``owner/city`` and never depends on role. The short
|
|
5
|
+
role suffixes remain only to read and import v1 cards such as ``alice/lead``.
|
|
6
|
+
|
|
7
|
+
The catalogue is shared by the Hall and seat so they cannot label the same role
|
|
8
|
+
differently. Which subset is relevant is owned by ``domains.py``: a city first
|
|
9
|
+
chooses its work domain, then one role inside it. Role descriptions may still be
|
|
10
|
+
overridden by a city's own role files.
|
|
11
|
+
|
|
12
|
+
Only picker labels and one-line summaries live here. The operating knowledge and
|
|
13
|
+
trade come from each role's Markdown file, looked up in your city first and the
|
|
14
|
+
plugin's examples second — duplicating those in code is how a managing partner
|
|
15
|
+
once got labelled a master builder.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
+
|
|
23
|
+
# (id, what the city calls it, its one line, whether it owns a property of every
|
|
24
|
+
# folder rather than folders of its own).
|
|
25
|
+
CATALOGO = [
|
|
26
|
+
# Deliberately no role pack. ``blank`` is an explicit choice to start with
|
|
27
|
+
# no predefined responsibility or operating knowledge; it is offered in
|
|
28
|
+
# every domain and can also be assigned to an individual repo agent.
|
|
29
|
+
("blank", "Blank role", "no predefined responsibility or role knowledge", True),
|
|
30
|
+
# Software development
|
|
31
|
+
("cpto", "Product & technology lead", "direction, priorities and product outcomes", True),
|
|
32
|
+
("dev", "Software engineer", "owns repositories and answers for their code", False),
|
|
33
|
+
(
|
|
34
|
+
"data-engineer",
|
|
35
|
+
"Data engineer",
|
|
36
|
+
"pipelines, transformations, storage contracts and data reliability",
|
|
37
|
+
False,
|
|
38
|
+
),
|
|
39
|
+
("devops", "Platform / DevOps lead", "security, reliability, performance, cost and CI", True),
|
|
40
|
+
("data", "Data & analytics lead", "events, schemas and comparable measurement", True),
|
|
41
|
+
("product-design", "Product designer", "design system, accessibility and user flows", True),
|
|
42
|
+
("po", "Product manager", "scope, priority and delivery coherence", True),
|
|
43
|
+
("llm-engineer", "AI / LLM engineer", "model calls, prompts, evaluations and token cost", True),
|
|
44
|
+
("ai-manager", "AI portfolio lead", "initiative overlap, adoption and portfolio value", True),
|
|
45
|
+
# Marketing, legal and finance
|
|
46
|
+
("brand-lead", "Brand lead", "positioning, priorities and brand coherence", True),
|
|
47
|
+
("content", "Content lead", "owns pieces, channels and editorial intent", False),
|
|
48
|
+
("performance", "Performance marketing lead", "paid acquisition, spend and conversion", True),
|
|
49
|
+
("seo", "SEO lead", "organic structure, indexing, links and durable URLs", True),
|
|
50
|
+
(
|
|
51
|
+
"lifecycle",
|
|
52
|
+
"Lifecycle / CRM lead",
|
|
53
|
+
"segments, triggers, sequences and contact pressure",
|
|
54
|
+
True,
|
|
55
|
+
),
|
|
56
|
+
("managing-partner", "Managing partner", "practice direction, matters and staffing", True),
|
|
57
|
+
("associate", "Lawyer / associate", "owns matters and their legal context", False),
|
|
58
|
+
("compliance", "Compliance lead", "obligations, deadlines, conflicts and records", True),
|
|
59
|
+
("knowledge", "Knowledge lead", "precedents, reusable work and institutional memory", True),
|
|
60
|
+
("cfo", "CFO / finance lead", "financial direction, priorities and resource choices", True),
|
|
61
|
+
("controller", "Controller", "close, reconciliations, controls and audit trail", True),
|
|
62
|
+
(
|
|
63
|
+
"fin-analytics",
|
|
64
|
+
"Financial analyst",
|
|
65
|
+
"definitions, comparability, forecasts and scenarios",
|
|
66
|
+
True,
|
|
67
|
+
),
|
|
68
|
+
("ops", "Operations specialist", "owns operational processes and their exceptions", False),
|
|
69
|
+
# Healthcare
|
|
70
|
+
(
|
|
71
|
+
"clinical-director",
|
|
72
|
+
"Clinical director",
|
|
73
|
+
"clinical direction, accountability and care priorities",
|
|
74
|
+
True,
|
|
75
|
+
),
|
|
76
|
+
("clinician", "Clinician", "owns patient or pathway context and clinical judgement", False),
|
|
77
|
+
(
|
|
78
|
+
"patient-safety",
|
|
79
|
+
"Patient safety & quality lead",
|
|
80
|
+
"harm prevention, incidents and safeguards",
|
|
81
|
+
True,
|
|
82
|
+
),
|
|
83
|
+
(
|
|
84
|
+
"clinical-ops",
|
|
85
|
+
"Clinical operations lead",
|
|
86
|
+
"capacity, pathways, handoffs and service delivery",
|
|
87
|
+
True,
|
|
88
|
+
),
|
|
89
|
+
("health-data", "Clinical data lead", "health measures, provenance and data quality", True),
|
|
90
|
+
(
|
|
91
|
+
"health-compliance",
|
|
92
|
+
"Health privacy & compliance lead",
|
|
93
|
+
"privacy, consent, access and regulation",
|
|
94
|
+
True,
|
|
95
|
+
),
|
|
96
|
+
# Revenue
|
|
97
|
+
("revenue-lead", "Revenue leader", "commercial direction, forecast and commitments", True),
|
|
98
|
+
("account-executive", "Account executive", "owns opportunities and buyer context", False),
|
|
99
|
+
(
|
|
100
|
+
"revops",
|
|
101
|
+
"Revenue operations lead",
|
|
102
|
+
"pipeline definitions, routing, systems and forecast hygiene",
|
|
103
|
+
True,
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
"customer-success",
|
|
107
|
+
"Customer success lead",
|
|
108
|
+
"adoption, outcomes, renewal and churn risk",
|
|
109
|
+
True,
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
"enablement",
|
|
113
|
+
"Sales enablement lead",
|
|
114
|
+
"playbooks, evidence and reusable field knowledge",
|
|
115
|
+
True,
|
|
116
|
+
),
|
|
117
|
+
# Research and general operations
|
|
118
|
+
(
|
|
119
|
+
"research-director",
|
|
120
|
+
"Research director",
|
|
121
|
+
"questions, portfolio direction and research decisions",
|
|
122
|
+
True,
|
|
123
|
+
),
|
|
124
|
+
("researcher", "Researcher", "owns studies, sources and interpretation", False),
|
|
125
|
+
(
|
|
126
|
+
"methods",
|
|
127
|
+
"Methods / statistics lead",
|
|
128
|
+
"study design, validity, uncertainty and analysis",
|
|
129
|
+
True,
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
"research-ops",
|
|
133
|
+
"Research operations lead",
|
|
134
|
+
"participants, tooling, data and study delivery",
|
|
135
|
+
True,
|
|
136
|
+
),
|
|
137
|
+
("ethics", "Ethics lead", "consent, proportionality, vulnerable groups and review", True),
|
|
138
|
+
("operations-lead", "Operations lead", "operating direction, outcomes and trade-offs", True),
|
|
139
|
+
("program-manager", "Programme manager", "dependencies, scope, milestones and delivery", True),
|
|
140
|
+
("process-owner", "Process owner", "owns a process, its exceptions and its outcomes", False),
|
|
141
|
+
(
|
|
142
|
+
"quality",
|
|
143
|
+
"Quality & risk lead",
|
|
144
|
+
"acceptance, failure modes, controls and verification",
|
|
145
|
+
True,
|
|
146
|
+
),
|
|
147
|
+
("city-lead", "City lead", "sets direction and chairs decisions in a custom domain", True),
|
|
148
|
+
("specialist", "Domain specialist", "owns a body of work and answers from its evidence", False),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# Short, lowercase and stable. A role nobody anticipated still gets a sensible name
|
|
152
|
+
# from `sufijo()` rather than nothing.
|
|
153
|
+
SUFIJOS = {
|
|
154
|
+
"blank": "blank",
|
|
155
|
+
"cpto": "lead",
|
|
156
|
+
"devops": "ops",
|
|
157
|
+
"data": "data",
|
|
158
|
+
"product-design": "design",
|
|
159
|
+
"po": "po",
|
|
160
|
+
"llm-engineer": "llm",
|
|
161
|
+
"ai-manager": "ai",
|
|
162
|
+
"dev": "dev",
|
|
163
|
+
"data-engineer": "dataeng",
|
|
164
|
+
"brand-lead": "brand",
|
|
165
|
+
"content": "content",
|
|
166
|
+
"performance": "ads",
|
|
167
|
+
"seo": "seo",
|
|
168
|
+
"lifecycle": "crm",
|
|
169
|
+
"managing-partner": "partner",
|
|
170
|
+
"associate": "assoc",
|
|
171
|
+
"compliance": "comp",
|
|
172
|
+
"knowledge": "know",
|
|
173
|
+
"cfo": "cfo",
|
|
174
|
+
"controller": "ctrl",
|
|
175
|
+
"fin-analytics": "fin",
|
|
176
|
+
"ops": "ops",
|
|
177
|
+
"clinical-director": "clinical",
|
|
178
|
+
"clinician": "clinician",
|
|
179
|
+
"patient-safety": "safety",
|
|
180
|
+
"clinical-ops": "careops",
|
|
181
|
+
"health-data": "healthdata",
|
|
182
|
+
"health-compliance": "privacy",
|
|
183
|
+
"revenue-lead": "revenue",
|
|
184
|
+
"account-executive": "account",
|
|
185
|
+
"revops": "revops",
|
|
186
|
+
"customer-success": "success",
|
|
187
|
+
"enablement": "enable",
|
|
188
|
+
"research-director": "research",
|
|
189
|
+
"researcher": "researcher",
|
|
190
|
+
"methods": "methods",
|
|
191
|
+
"research-ops": "resops",
|
|
192
|
+
"ethics": "ethics",
|
|
193
|
+
"operations-lead": "operations",
|
|
194
|
+
"program-manager": "program",
|
|
195
|
+
"process-owner": "process",
|
|
196
|
+
"quality": "quality",
|
|
197
|
+
"city-lead": "lead",
|
|
198
|
+
"specialist": "specialist",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Whoever sets the goals. Not a style rule: goals are the one thing this system keeps
|
|
202
|
+
# whatever kind of city you build, and they are agreed between the architect and each
|
|
203
|
+
# city seat. Without that seat there is nobody to agree them with, and every round
|
|
204
|
+
# degrades into a status report.
|
|
205
|
+
ARQUITECTOS = {
|
|
206
|
+
"cpto",
|
|
207
|
+
"brand-lead",
|
|
208
|
+
"managing-partner",
|
|
209
|
+
"cfo",
|
|
210
|
+
"clinical-director",
|
|
211
|
+
"revenue-lead",
|
|
212
|
+
"research-director",
|
|
213
|
+
"operations-lead",
|
|
214
|
+
"city-lead",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def sufijo(rol):
|
|
219
|
+
"""The agent-name suffix for a role."""
|
|
220
|
+
return SUFIJOS.get(rol, rol.split("-")[0][:8])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def agente(usuario, rol, ciudad=""):
|
|
224
|
+
"""A stable owner/city address, or the legacy user/role address.
|
|
225
|
+
|
|
226
|
+
Repo agents stay private behind the seat. Once a city is known, the public
|
|
227
|
+
endpoint is ``user/city`` so changing its role cannot break every road.
|
|
228
|
+
"""
|
|
229
|
+
if ciudad:
|
|
230
|
+
segura = re.sub(r"[^a-z0-9-]+", "-", ciudad.lower()).strip("-") or "city"
|
|
231
|
+
return f"{usuario}/{segura}"
|
|
232
|
+
return f"{usuario}/{sufijo(rol)}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def nombre(rol):
|
|
236
|
+
"""What the city calls this role, or a readable version of its id."""
|
|
237
|
+
for r, n, _, _ in CATALOGO:
|
|
238
|
+
if r == rol:
|
|
239
|
+
return n
|
|
240
|
+
return rol.replace("-", " ").title()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def oficio(rol, datos=""):
|
|
244
|
+
"""The trade, read from the role's own file — never from a table in code.
|
|
245
|
+
|
|
246
|
+
A hardcoded map labels every unknown role as the same thing, which is exactly
|
|
247
|
+
what once turned a managing partner into a master builder. Looked up in the data
|
|
248
|
+
repo first (roles are data, and a team renames them), then in the examples this
|
|
249
|
+
plugin ships. `../roles/examples` and not a path through the clone, because the
|
|
250
|
+
installed plugin has no clone — this is the one lookup that works at both doors.
|
|
251
|
+
|
|
252
|
+
There were two copies of this: the wizard's skipped the data repo, so a re-run
|
|
253
|
+
against renamed roles showed the stale trade. One copy, the complete one.
|
|
254
|
+
"""
|
|
255
|
+
if rol == "blank":
|
|
256
|
+
return "No preset"
|
|
257
|
+
bases = [os.path.join(datos, "roles")] if datos else []
|
|
258
|
+
bases.append(os.path.join(AQUI, "..", "roles", "examples"))
|
|
259
|
+
for base in bases:
|
|
260
|
+
f = os.path.join(base, f"{rol}.md")
|
|
261
|
+
if os.path.exists(f):
|
|
262
|
+
m = re.search(r"^trade:\s*(.+)$", open(f).read(), re.M)
|
|
263
|
+
if m:
|
|
264
|
+
return m.group(1).strip()
|
|
265
|
+
# No role file: say what the role is called rather than inventing a trade.
|
|
266
|
+
return rol.replace("-", " ").title()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def transversal(rol):
|
|
270
|
+
"""Whether this role owns a property of every folder rather than folders."""
|
|
271
|
+
for r, _, _, t in CATALOGO:
|
|
272
|
+
if r == rol:
|
|
273
|
+
return t
|
|
274
|
+
return True
|