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,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rebuild the city's history from the merge date of every pull request.
|
|
3
|
+
|
|
4
|
+
You do not have to wait for time to pass to have a past: a merged PR carries its
|
|
5
|
+
date, so a house's capital in March 2024 is however many PRs had landed by March
|
|
6
|
+
2024. That is what the replay plays.
|
|
7
|
+
|
|
8
|
+
Run from a laptop with that person's own `gh`, so the token never leaves the
|
|
9
|
+
machine and never ends up inside a Worker.
|
|
10
|
+
|
|
11
|
+
./city/scripts/history.py writes /tmp/history.sql
|
|
12
|
+
./city/scripts/history.py --few only 8 repos, to try it
|
|
13
|
+
./city/scripts/history.py --org acme the organisation to ask about
|
|
14
|
+
./city/scripts/history.py --out h.sql somewhere else
|
|
15
|
+
|
|
16
|
+
Then load it:
|
|
17
|
+
npx wrangler@4 d1 execute city --remote --file /tmp/history.sql
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
|
|
25
|
+
RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def org_por_defecto():
|
|
29
|
+
"""The organisation, from the settings or from `gh`'s own idea of who you are."""
|
|
30
|
+
for v in (os.environ.get('AGENTS_CITY_ORG'), os.environ.get('GITHUB_ORG')):
|
|
31
|
+
if v:
|
|
32
|
+
return v
|
|
33
|
+
r = subprocess.run(['gh', 'api', 'user', '--jq', '.login'],
|
|
34
|
+
capture_output=True, text=True)
|
|
35
|
+
return r.stdout.strip() if r.returncode == 0 else ''
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parcelas_por_repo():
|
|
39
|
+
"""repo -> [parcel ids], read from the same place the seeder reads."""
|
|
40
|
+
salida = subprocess.run([os.path.join(RAIZ, 'city', 'scripts', 'seed.py')],
|
|
41
|
+
capture_output=True, text=True).stdout
|
|
42
|
+
m = defaultdict(list)
|
|
43
|
+
for linea in salida.splitlines():
|
|
44
|
+
g = re.match(r"INSERT INTO parcela \(id,repo,ruta,unidad,nombre,dueno\) "
|
|
45
|
+
r"VALUES \('(.+?)','(.+?)',", linea)
|
|
46
|
+
if g:
|
|
47
|
+
m[g.group(2)].append(g.group(1))
|
|
48
|
+
return m
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def dias_de(org, repo):
|
|
52
|
+
"""How many PRs were merged each DAY in this repo.
|
|
53
|
+
|
|
54
|
+
Per day and not per month because a month is too coarse to watch anything
|
|
55
|
+
grow: the interesting film is the last ninety days.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
r = subprocess.run(
|
|
59
|
+
['gh', 'api', f'repos/{org}/{repo}/pulls?state=closed&per_page=100',
|
|
60
|
+
'--paginate', '--jq', '.[] | select(.merged_at) | .merged_at'],
|
|
61
|
+
capture_output=True, text=True, timeout=180)
|
|
62
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
63
|
+
return {}
|
|
64
|
+
cuenta = defaultdict(int)
|
|
65
|
+
for linea in r.stdout.splitlines():
|
|
66
|
+
if len(linea) >= 10:
|
|
67
|
+
cuenta[linea[:10]] += 1
|
|
68
|
+
return cuenta
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
org = ''
|
|
73
|
+
salida = '/tmp/history.sql'
|
|
74
|
+
args = sys.argv[1:]
|
|
75
|
+
for i, a in enumerate(args):
|
|
76
|
+
if a == '--org' and i + 1 < len(args):
|
|
77
|
+
org = args[i + 1]
|
|
78
|
+
if a == '--out' and i + 1 < len(args):
|
|
79
|
+
salida = args[i + 1]
|
|
80
|
+
org = org or org_por_defecto()
|
|
81
|
+
if not org:
|
|
82
|
+
print('I need a GitHub organisation: --org <name>, or AGENTS_CITY_ORG.',
|
|
83
|
+
file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
por_repo = parcelas_por_repo()
|
|
87
|
+
repos = sorted(por_repo)
|
|
88
|
+
if not repos:
|
|
89
|
+
print('No parcels. Is AGENTS_CITY_DATA pointing at your data repo?',
|
|
90
|
+
file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
if '--few' in args or '--pocos' in args:
|
|
93
|
+
repos = repos[:8]
|
|
94
|
+
|
|
95
|
+
filas = []
|
|
96
|
+
for i, repo in enumerate(repos, 1):
|
|
97
|
+
dias = dias_de(org, repo)
|
|
98
|
+
# Every parcel of a repo inherits the repo's dates: counting per path
|
|
99
|
+
# needs one API call per PR. The map says so where it shows the number,
|
|
100
|
+
# rather than presenting a whole-repo figure as if it were the parcel's.
|
|
101
|
+
for parcela in por_repo[repo]:
|
|
102
|
+
for dia, n in dias.items():
|
|
103
|
+
filas.append((parcela, dia, n))
|
|
104
|
+
print(f' [{i}/{len(repos)}] {repo}: {sum(dias.values())} PRs '
|
|
105
|
+
f'across {len(dias)} days', file=sys.stderr, flush=True)
|
|
106
|
+
|
|
107
|
+
def q(s):
|
|
108
|
+
return "'" + str(s).replace("'", "''") + "'"
|
|
109
|
+
with open(salida, 'w') as f:
|
|
110
|
+
f.write('DELETE FROM hito;\n')
|
|
111
|
+
for parcela, dia, n in filas:
|
|
112
|
+
f.write('INSERT INTO hito (parcela_id,dia,n) VALUES ('
|
|
113
|
+
f'{q(parcela)},{q(dia)},{n}) '
|
|
114
|
+
'ON CONFLICT(parcela_id,dia) DO UPDATE SET n=excluded.n;\n')
|
|
115
|
+
print(f'{len(filas)} rows -> {salida}', file=sys.stderr)
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
sys.exit(main())
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Load your city into D1: units, parcels, people — and optionally a history.
|
|
3
|
+
|
|
4
|
+
Reads the data repo, which is the same thing the plugin reads: units.yml,
|
|
5
|
+
parcels.yml, roles/ and one card per person. Nothing about any particular
|
|
6
|
+
organisation lives in here.
|
|
7
|
+
|
|
8
|
+
./city/scripts/seed.py print the SQL, change nothing
|
|
9
|
+
./city/scripts/seed.py --local load it into the local D1
|
|
10
|
+
./city/scripts/seed.py --remote load it into the deployed one
|
|
11
|
+
./city/scripts/seed.py --data ../mine read a different data repo
|
|
12
|
+
./city/scripts/seed.py --fake-history invent a plausible past (demo only)
|
|
13
|
+
|
|
14
|
+
Where the data comes from, in order: --data, $AGENTS_CITY_DATA, ~/agents-city-data.
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import shutil
|
|
18
|
+
import glob
|
|
19
|
+
import os
|
|
20
|
+
import random
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
from datetime import date, timedelta
|
|
26
|
+
|
|
27
|
+
# The repo root, three levels up from city/scripts/seed.py. Worth being explicit:
|
|
28
|
+
# computing it wrong sends wrangler looking for city/city/worker.
|
|
29
|
+
RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
30
|
+
# The parcels.yml reader lives in plugin/scripts because that directory is what
|
|
31
|
+
# gets installed on everybody's machine. The plugin cannot import from city/, so
|
|
32
|
+
# this is the only direction that works — and there is one reader, not two.
|
|
33
|
+
sys.path.insert(0, os.path.join(RAIZ, 'plugin', 'scripts'))
|
|
34
|
+
import parcels # noqa: E402
|
|
35
|
+
|
|
36
|
+
import roles # noqa: E402
|
|
37
|
+
import units # noqa: E402
|
|
38
|
+
oficio = roles.oficio
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def lanzador():
|
|
43
|
+
"""How to run wrangler on this machine.
|
|
44
|
+
|
|
45
|
+
Not a detail: hardcoding one runner is what made the first from-scratch run
|
|
46
|
+
fail on a machine that had npm but not bun. npx ships with npm, so it is the
|
|
47
|
+
one almost everybody already has; bunx is faster when it is there.
|
|
48
|
+
"""
|
|
49
|
+
for cand in (['bunx'], ['bun', 'x'], ['npx', '--yes']):
|
|
50
|
+
if shutil.which(cand[0]):
|
|
51
|
+
return cand
|
|
52
|
+
raise SystemExit(' I need npx (comes with Node) or bun. Install Node from nodejs.org.')
|
|
53
|
+
|
|
54
|
+
def q(s):
|
|
55
|
+
return "'" + str(s).replace("'", "''") + "'"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def unidades(datos):
|
|
61
|
+
"""The districts, read by units.py like everywhere else. The column widths are
|
|
62
|
+
the map's own idea and stay here."""
|
|
63
|
+
fuera = units.lee(f'{datos}/units.yml')
|
|
64
|
+
for u in fuera:
|
|
65
|
+
u['cols'] = 16 if u['id'] == 'none' else 7 if u['id'] == 'lab' else 3
|
|
66
|
+
return fuera
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parcelas(datos):
|
|
70
|
+
"""Declared parcels, and which repos are research that does not ship yet.
|
|
71
|
+
|
|
72
|
+
The format is read by plugin/scripts/parcels.py — the same reader the growth
|
|
73
|
+
reporter uses. It used to be read here by a second copy of the same walk and the
|
|
74
|
+
same regex, and the two drifted: this one handled the `lab:` section and warned
|
|
75
|
+
about lines it could not parse, the reporter's did neither and looked for the
|
|
76
|
+
folder under a key that does not exist. Two readers of one format is a bug
|
|
77
|
+
waiting for somebody to edit one of them.
|
|
78
|
+
"""
|
|
79
|
+
declaradas, lab, raras = parcels.lee(f'{datos}/parcels.yml')
|
|
80
|
+
if raras:
|
|
81
|
+
print(parcels.aviso(raras), file=sys.stderr)
|
|
82
|
+
return ([(p['repo'], p['ruta'], p['unidad'], p['nombre']) for p in declaradas],
|
|
83
|
+
lab)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def objetivos(fichero):
|
|
87
|
+
"""The goals written on one card.
|
|
88
|
+
|
|
89
|
+
Parsed from the card rather than from a second file, because the card is
|
|
90
|
+
where they are agreed and reviewed. A goal with no measure is still read and
|
|
91
|
+
still shown — saying "this one has no measure yet" is more useful than
|
|
92
|
+
pretending it does not exist.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
texto = open(fichero, encoding='utf-8').read()
|
|
96
|
+
except OSError:
|
|
97
|
+
return []
|
|
98
|
+
# Only above the format comment: the template below it is not somebody's goal.
|
|
99
|
+
texto = texto.split('<!-- Format', 1)[0]
|
|
100
|
+
fuera = []
|
|
101
|
+
for i, bloque in enumerate(re.split(r'^### O\d+ — ', texto, flags=re.M)[1:], 1):
|
|
102
|
+
lineas = bloque.strip().splitlines()
|
|
103
|
+
titulo = lineas[0].strip() if lineas else ''
|
|
104
|
+
if not titulo or titulo.startswith('<'):
|
|
105
|
+
continue
|
|
106
|
+
def campo(k, _b=bloque):
|
|
107
|
+
m = re.search(rf'^- \*\*{k}\*\*:\s*(.+)$', _b, re.M)
|
|
108
|
+
return (m.group(1).strip().strip('`') if m else '')
|
|
109
|
+
fuera.append({
|
|
110
|
+
'n': i, 'titulo': titulo,
|
|
111
|
+
'como': campo('How it is measured'), 'medida': campo('Measure'),
|
|
112
|
+
'partida': campo('Baseline'), 'meta': campo('Target'),
|
|
113
|
+
'cuando': campo('By when'), 'estado': campo('State') or 'not started',
|
|
114
|
+
})
|
|
115
|
+
return fuera
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def gente(datos):
|
|
119
|
+
"""One card per person. They live at the root of the data repo — that is what
|
|
120
|
+
the wizard writes and what the plugin reads — but a `team/` subfolder is
|
|
121
|
+
accepted too, because plenty of people will reach for one."""
|
|
122
|
+
fuera = []
|
|
123
|
+
for f in sorted(glob.glob(f'{datos}/*.md') + glob.glob(f'{datos}/team/*.md')):
|
|
124
|
+
t = open(f).read()
|
|
125
|
+
def g(k, d='', _t=t):
|
|
126
|
+
m = re.search(rf'^{k}:\s*(.+)$', _t, re.M)
|
|
127
|
+
return m.group(1).strip() if m else d
|
|
128
|
+
|
|
129
|
+
def lista(k, _t=t):
|
|
130
|
+
m = re.search(rf'^{k}:\s*\[(.*?)\]', _t, re.M)
|
|
131
|
+
return [x.strip() for x in (m.group(1) if m else '').split(',') if x.strip()]
|
|
132
|
+
u = g('user')
|
|
133
|
+
if u:
|
|
134
|
+
# The path travels with the person: the goals are read from the same
|
|
135
|
+
# card, and hunting for it again by name would be a second guess about
|
|
136
|
+
# where cards live.
|
|
137
|
+
fuera.append({'user': u, 'name': g('name', u), 'role': g('role', 'dev'),
|
|
138
|
+
'agent': g('agent', f'{u}/dev'), 'repos': lista('repos'),
|
|
139
|
+
'ficha': f})
|
|
140
|
+
return fuera
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def dueno(repo, personas):
|
|
144
|
+
"""Who answers for a repo. A dev wins over a cross-cutting role: the map asks
|
|
145
|
+
'who knows this code', not 'who has an opinion about it'."""
|
|
146
|
+
for p in personas:
|
|
147
|
+
if p['role'] == 'dev' and repo in p['repos']:
|
|
148
|
+
return p['user']
|
|
149
|
+
for p in personas:
|
|
150
|
+
if repo in p['repos']:
|
|
151
|
+
return p['user']
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def historia_inventada(ids, semilla=1312):
|
|
156
|
+
"""A plausible past, for demos and for trying the replay before you have one.
|
|
157
|
+
|
|
158
|
+
Invented numbers, but not an invented shape: growth is lumpy, each parcel
|
|
159
|
+
starts on a different day, most are quiet most of the time, and a few carry
|
|
160
|
+
the bulk. A flat random history looks obviously fake the moment you play it.
|
|
161
|
+
"""
|
|
162
|
+
random.seed(semilla)
|
|
163
|
+
hoy = date.today()
|
|
164
|
+
inicio = hoy - timedelta(days=3 * 365)
|
|
165
|
+
filas = []
|
|
166
|
+
for pid in sorted(ids):
|
|
167
|
+
# A long tail and a couple of monsters, which is what a real portfolio
|
|
168
|
+
# looks like: most parcels land a handful of changes a month and two or
|
|
169
|
+
# three carry the company. A flat distribution made every house the same
|
|
170
|
+
# height, and a city where everything is the same height says nothing.
|
|
171
|
+
ritmo = random.choice([0.01, 0.02, 0.03, 0.05, 0.08, 0.12, 0.2, 0.45, 0.9])
|
|
172
|
+
d = inicio + timedelta(days=random.randint(0, 1000))
|
|
173
|
+
while d <= hoy:
|
|
174
|
+
if d.weekday() < 5 and random.random() < min(0.85, ritmo * 1.6):
|
|
175
|
+
filas.append((pid, d.isoformat(), max(1, int(random.gauss(ritmo * 3, ritmo * 2)))))
|
|
176
|
+
d += timedelta(days=1)
|
|
177
|
+
return filas
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
ETIQUETAS = ['ux', 'data', 'security', 'cost', 'product', 'llm']
|
|
181
|
+
|
|
182
|
+
# What a notice actually says, per property. Invented, but the shape is the point:
|
|
183
|
+
# a notice names the thing that moved, not "something changed".
|
|
184
|
+
TEXTOS = {
|
|
185
|
+
'ux': ['the empty state now shows a spinner in {p}', 'the primary button moved in {p}'],
|
|
186
|
+
'data': ['the event schema changed in {p}', 'a series was renamed in {p}'],
|
|
187
|
+
'security': ['a new public endpoint in {p}', 'a dependency with a CVE in {p}'],
|
|
188
|
+
'cost': ['the batch size doubled in {p}', 'a nightly job now runs hourly in {p}'],
|
|
189
|
+
'product': ['a flag defaults to on in {p}', 'the onboarding order changed in {p}'],
|
|
190
|
+
'llm': ['the system prompt changed in {p}', 'the model was swapped in {p}'],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def avisos_inventados(ids, personas, hitos, semilla=99):
|
|
195
|
+
"""Notices with dates, so the replay has letters flying and not just floors.
|
|
196
|
+
|
|
197
|
+
Derived from the history rather than sprinkled at random: a notice happens on
|
|
198
|
+
a day when something actually landed, and it goes from the parcel that changed
|
|
199
|
+
to a person who is not its owner — which is the whole point of a notice.
|
|
200
|
+
"""
|
|
201
|
+
if not hitos or not personas:
|
|
202
|
+
return []
|
|
203
|
+
random.seed(semilla)
|
|
204
|
+
dueno_de = {}
|
|
205
|
+
fuera, vistos = [], set()
|
|
206
|
+
for pid, dia, n in hitos:
|
|
207
|
+
# Roughly one notice per twelve landings: often enough to see, rare enough
|
|
208
|
+
# that a day with one means something.
|
|
209
|
+
if random.random() > min(0.35, n / 12):
|
|
210
|
+
continue
|
|
211
|
+
emisor = dueno_de.get(pid) or random.choice(personas)['user']
|
|
212
|
+
dueno_de[pid] = emisor
|
|
213
|
+
otros = [p for p in personas if p['user'] != emisor]
|
|
214
|
+
if not otros:
|
|
215
|
+
continue
|
|
216
|
+
receptor = random.choice(otros)
|
|
217
|
+
etq = random.choice(ETIQUETAS)
|
|
218
|
+
# One notice per (day, sender, recipient, parcel, property): the same
|
|
219
|
+
# person telling the same person the same thing twice on the same day is
|
|
220
|
+
# noise, and on screen it reads as a bug in the ticker.
|
|
221
|
+
clave = (dia, emisor, receptor['user'], pid, etq)
|
|
222
|
+
if clave in vistos:
|
|
223
|
+
continue
|
|
224
|
+
vistos.add(clave)
|
|
225
|
+
texto = random.choice(TEXTOS[etq]).format(p=pid)
|
|
226
|
+
fuera.append((dia, emisor, receptor['user'], etq, texto))
|
|
227
|
+
return fuera
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
MODELOS = ['claude-opus-5', 'claude-sonnet-5', 'claude-haiku-4-5']
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def gasto_inventado(hitos, personas, semilla=404):
|
|
234
|
+
"""Token spend for the demo, so the global counter is not an empty box.
|
|
235
|
+
|
|
236
|
+
Tied to the invented history rather than random: the days the city built are
|
|
237
|
+
the days it spent. Cache reads dominate the numbers, which is what a real
|
|
238
|
+
report looks like — a long session re-reads its context far more than it
|
|
239
|
+
writes anything new, and a counter that hides that misleads about the cost.
|
|
240
|
+
"""
|
|
241
|
+
if not personas:
|
|
242
|
+
return []
|
|
243
|
+
random.seed(semilla)
|
|
244
|
+
desde = date.today() - timedelta(days=45)
|
|
245
|
+
por_dia = {}
|
|
246
|
+
for _, dia, n in hitos:
|
|
247
|
+
if dia >= desde.isoformat():
|
|
248
|
+
por_dia[dia] = por_dia.get(dia, 0) + n
|
|
249
|
+
filas = []
|
|
250
|
+
for dia in sorted(por_dia) or [date.today().isoformat()]:
|
|
251
|
+
trabajando = random.sample(personas, k=min(len(personas),
|
|
252
|
+
max(1, min(len(personas),
|
|
253
|
+
por_dia.get(dia, 1) + 1))))
|
|
254
|
+
for p in trabajando:
|
|
255
|
+
for modelo in random.sample(MODELOS, k=random.randint(1, 2)):
|
|
256
|
+
salida = random.randint(4_000, 60_000)
|
|
257
|
+
filas.append((dia, p['user'], modelo,
|
|
258
|
+
random.randint(2_000, 30_000), salida,
|
|
259
|
+
salida * random.randint(20, 90),
|
|
260
|
+
salida * random.randint(1, 6)))
|
|
261
|
+
return filas
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def sql_personas(sql, personas, datos):
|
|
265
|
+
"""People and their goals, straight off the cards."""
|
|
266
|
+
for p in personas:
|
|
267
|
+
for o in objetivos(p['ficha']):
|
|
268
|
+
sql.append('INSERT INTO objetivo '
|
|
269
|
+
'(usuario,n,titulo,como,medida,partida,meta,cuando,estado) '
|
|
270
|
+
f'VALUES ({q(p["user"])},{o["n"]},{q(o["titulo"])},{q(o["como"])},'
|
|
271
|
+
f'{q(o["medida"])},{q(o["partida"])},{q(o["meta"])},{q(o["cuando"])},'
|
|
272
|
+
f'{q(o["estado"])}) ON CONFLICT(usuario,n) DO UPDATE SET '
|
|
273
|
+
'titulo=excluded.titulo, como=excluded.como, medida=excluded.medida, '
|
|
274
|
+
'partida=excluded.partida, meta=excluded.meta, cuando=excluded.cuando, '
|
|
275
|
+
'estado=excluded.estado;')
|
|
276
|
+
sql.append('INSERT INTO persona (usuario,nombre,rol,oficio,agente) VALUES ('
|
|
277
|
+
f'{q(p["user"])},{q(p["name"])},{q(p["role"])},'
|
|
278
|
+
f'{q(oficio(p["role"], datos))},{q(p["agent"])}) '
|
|
279
|
+
'ON CONFLICT(usuario) DO UPDATE SET nombre=excluded.nombre, rol=excluded.rol, '
|
|
280
|
+
'oficio=excluded.oficio, agente=excluded.agente;')
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def sql_casas(sql, ids, acumulado, hitos):
|
|
284
|
+
"""Houses start empty unless there is a history: the cron fills them from the
|
|
285
|
+
real repos, and an invented number in a real city is worse than a gap."""
|
|
286
|
+
hoy = date.today().isoformat()
|
|
287
|
+
if not hitos:
|
|
288
|
+
return
|
|
289
|
+
random.seed(7)
|
|
290
|
+
for pid in ids:
|
|
291
|
+
cap = acumulado.get(pid, 0)
|
|
292
|
+
abiertos = random.randint(0, 4) if cap else 0
|
|
293
|
+
sql.append('INSERT INTO casa (parcela_id,pisos,ladrillos,andamios,andamio_viejo,'
|
|
294
|
+
f'grieta,actividad30,actualizado) VALUES ({q(pid)},{cap},'
|
|
295
|
+
f'{random.randint(0, 9)},{abiertos},{random.randint(0, abiertos)},'
|
|
296
|
+
f'{1 if random.random() < 0.06 else 0},'
|
|
297
|
+
f'{random.randint(0, 40) if cap else 0},{q(hoy)}) '
|
|
298
|
+
'ON CONFLICT(parcela_id) DO UPDATE SET pisos=excluded.pisos, '
|
|
299
|
+
'ladrillos=excluded.ladrillos, andamios=excluded.andamios, '
|
|
300
|
+
'andamio_viejo=excluded.andamio_viejo, grieta=excluded.grieta, '
|
|
301
|
+
'actividad30=excluded.actividad30, actualizado=excluded.actualizado;')
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def main():
|
|
305
|
+
ap = argparse.ArgumentParser()
|
|
306
|
+
ap.add_argument('--data', default=os.environ.get('AGENTS_CITY_DATA',
|
|
307
|
+
os.path.expanduser('~/agents-city-data')))
|
|
308
|
+
ap.add_argument('--local', action='store_true')
|
|
309
|
+
# Where the local D1 lives. bin/demo and bin/city point at different folders so
|
|
310
|
+
# the demo's live state never shows up on your own bus, and the seeder has to
|
|
311
|
+
# write into the same one the worker will read — otherwise it loads a city
|
|
312
|
+
# nobody serves.
|
|
313
|
+
ap.add_argument('--persist-to', default=os.environ.get('CITY_PERSIST', ''))
|
|
314
|
+
ap.add_argument('--remote', action='store_true')
|
|
315
|
+
ap.add_argument('--fake-history', action='store_true')
|
|
316
|
+
a = ap.parse_args()
|
|
317
|
+
datos = os.path.expanduser(a.data)
|
|
318
|
+
|
|
319
|
+
if not os.path.isdir(datos):
|
|
320
|
+
print(f'No data repo at {datos}.\nRun ./bin/setup.py first, or pass --data.',
|
|
321
|
+
file=sys.stderr)
|
|
322
|
+
return 1
|
|
323
|
+
|
|
324
|
+
unis, personas = unidades(datos), gente(datos)
|
|
325
|
+
declaradas, lab = parcelas(datos)
|
|
326
|
+
ya = {r for r, *_ in declaradas}
|
|
327
|
+
todos = sorted({r for p in personas for r in p['repos']} | ya | lab)
|
|
328
|
+
|
|
329
|
+
filas = list(declaradas)
|
|
330
|
+
for r in todos:
|
|
331
|
+
if r in ya:
|
|
332
|
+
continue
|
|
333
|
+
filas.append((r, '', 'lab' if r in lab else 'none', r))
|
|
334
|
+
|
|
335
|
+
sql, ids = [], []
|
|
336
|
+
for u in unis:
|
|
337
|
+
sql.append('INSERT INTO unidad (id,nombre,color,orden,nota,cols) VALUES ('
|
|
338
|
+
f'{q(u["id"])},{q(u["name"])},{q(u["color"])},{u["order"]},'
|
|
339
|
+
f'{q(u["note"]) if u["note"] else "NULL"},{u["cols"]}) '
|
|
340
|
+
'ON CONFLICT(id) DO UPDATE SET nombre=excluded.nombre, color=excluded.color, '
|
|
341
|
+
'orden=excluded.orden, nota=excluded.nota, cols=excluded.cols;')
|
|
342
|
+
|
|
343
|
+
sql_personas(sql, personas, datos)
|
|
344
|
+
|
|
345
|
+
for repo, ruta, unidad, nombre in filas:
|
|
346
|
+
# Shared, not repeated: the reporter pushes growth keyed on this id and
|
|
347
|
+
# the map stores rows keyed on it. If the two ever drift, growth lands on
|
|
348
|
+
# nothing and neither side notices.
|
|
349
|
+
pid = parcels.identidad(repo, ruta)
|
|
350
|
+
ids.append(pid)
|
|
351
|
+
d = dueno(repo, personas)
|
|
352
|
+
sql.append('INSERT INTO parcela (id,repo,ruta,unidad,nombre,dueno) VALUES ('
|
|
353
|
+
f'{q(pid)},{q(repo)},{q(ruta)},{q(unidad)},{q(nombre)},{q(d) if d else "NULL"}) '
|
|
354
|
+
'ON CONFLICT(id) DO UPDATE SET repo=excluded.repo, ruta=excluded.ruta, '
|
|
355
|
+
'unidad=excluded.unidad, nombre=excluded.nombre, dueno=excluded.dueno;')
|
|
356
|
+
|
|
357
|
+
hitos = historia_inventada(ids) if a.fake_history else []
|
|
358
|
+
acumulado = {}
|
|
359
|
+
for pid, dia, n in hitos:
|
|
360
|
+
acumulado[pid] = acumulado.get(pid, 0) + n
|
|
361
|
+
sql.append(f'INSERT INTO hito (parcela_id,dia,n) VALUES ({q(pid)},{q(dia)},{n}) '
|
|
362
|
+
'ON CONFLICT(parcela_id,dia) DO UPDATE SET n=excluded.n;')
|
|
363
|
+
|
|
364
|
+
sql_casas(sql, ids, acumulado, hitos)
|
|
365
|
+
|
|
366
|
+
# Token spend. Stored per person because that is how the reports arrive and
|
|
367
|
+
# the only way to deduplicate them; shown as one global number and never as
|
|
368
|
+
# a ranking.
|
|
369
|
+
gastos = gasto_inventado(hitos, personas) if a.fake_history else []
|
|
370
|
+
for dia, usuario, modelo, ent, sal, cr, cw in gastos:
|
|
371
|
+
sql.append('INSERT INTO gasto (dia,usuario,modelo,entrada,salida,cache_r,cache_w) VALUES ('
|
|
372
|
+
f'{q(dia)},{q(usuario)},{q(modelo)},{ent},{sal},{cr},{cw}) '
|
|
373
|
+
'ON CONFLICT(dia,usuario,modelo) DO UPDATE SET entrada=excluded.entrada, '
|
|
374
|
+
'salida=excluded.salida, cache_r=excluded.cache_r, cache_w=excluded.cache_w;')
|
|
375
|
+
|
|
376
|
+
# Notices, with their day, so the replay can fly them.
|
|
377
|
+
#
|
|
378
|
+
# Cleared first when the history is invented: `evento` has an autoincrementing
|
|
379
|
+
# id and no natural key, so re-seeding a demo used to add a second copy of
|
|
380
|
+
# every notice — and a third, and a fourth. Everything else here upserts and
|
|
381
|
+
# is therefore safe to re-run; this is the one table that was not.
|
|
382
|
+
if a.fake_history:
|
|
383
|
+
sql.append("DELETE FROM evento WHERE tipo='notice';")
|
|
384
|
+
avisos = avisos_inventados(ids, personas, hitos) if a.fake_history else []
|
|
385
|
+
for dia, de, para, etq, texto in avisos:
|
|
386
|
+
sql.append('INSERT INTO evento (ts,tipo,origen,destino,etiqueta,texto) VALUES ('
|
|
387
|
+
f"{q(dia + 'T12:00:00Z')},'notice',{q(de)},{q(para)},{q(etq)},"
|
|
388
|
+
f'{q(texto)});')
|
|
389
|
+
|
|
390
|
+
# The data repo is the source of truth, so anything no longer in it goes.
|
|
391
|
+
#
|
|
392
|
+
# Without this, renaming a unit left its old district on the map for ever —
|
|
393
|
+
# an empty platform with a banner and nothing on it — and a parcel that was
|
|
394
|
+
# split differently left a house standing on the old plot. A seeder that only
|
|
395
|
+
# ever inserts cannot express "this is gone", and "this is gone" happens
|
|
396
|
+
# every time an org changes shape.
|
|
397
|
+
lista_u = ','.join(q(u['id']) for u in unis) or "''"
|
|
398
|
+
lista_p = ','.join(q(i) for i in ids) or "''"
|
|
399
|
+
sql.append(f'DELETE FROM unidad WHERE id NOT IN ({lista_u});')
|
|
400
|
+
sql.append(f'DELETE FROM casa WHERE parcela_id NOT IN ({lista_p});')
|
|
401
|
+
sql.append(f'DELETE FROM hito WHERE parcela_id NOT IN ({lista_p});')
|
|
402
|
+
sql.append(f'DELETE FROM parcela WHERE id NOT IN ({lista_p});')
|
|
403
|
+
# People too: someone who left the company should not keep a light on the map.
|
|
404
|
+
lista_g = ','.join(q(p['user']) for p in personas) or "''"
|
|
405
|
+
sql.append(f'DELETE FROM persona WHERE usuario NOT IN ({lista_g});')
|
|
406
|
+
# And their spend with them. Without this, seeding a brand new city over a
|
|
407
|
+
# database that once held another one shows the other one's token counter —
|
|
408
|
+
# which is how this was found: a fresh marketing city claiming 3.5M tokens
|
|
409
|
+
# spent today by thirteen people who were not in it.
|
|
410
|
+
sql.append(f'DELETE FROM gasto WHERE usuario NOT IN ({lista_g});')
|
|
411
|
+
sql.append(f'DELETE FROM objetivo WHERE usuario NOT IN ({lista_g});')
|
|
412
|
+
|
|
413
|
+
salida = '\n'.join(sql)
|
|
414
|
+
resumen = (f'{len(unis)} units, {len(personas)} people, {len(ids)} parcels'
|
|
415
|
+
+ (f', {len(hitos)} history rows' if hitos else '')
|
|
416
|
+
+ (f', {len(avisos)} notices' if avisos else '')
|
|
417
|
+
+ (f', {len(gastos)} spend rows' if gastos else ''))
|
|
418
|
+
if a.local or a.remote:
|
|
419
|
+
modo = '--local' if a.local else '--remote'
|
|
420
|
+
# A unique file per run, not a fixed /tmp name: two cities seeding at
|
|
421
|
+
# once (the multi-domain demos do exactly this) used to overwrite each
|
|
422
|
+
# other's SQL mid-flight and load somebody else's city.
|
|
423
|
+
descriptor, ruta = tempfile.mkstemp(prefix='agents-city-seed-', suffix='.sql')
|
|
424
|
+
try:
|
|
425
|
+
with os.fdopen(descriptor, 'w') as archivo:
|
|
426
|
+
archivo.write(salida)
|
|
427
|
+
orden = [*lanzador(), 'wrangler@4', 'd1', 'execute', 'city', modo]
|
|
428
|
+
if a.local and a.persist_to:
|
|
429
|
+
orden += ['--persist-to', os.path.abspath(os.path.expanduser(a.persist_to))]
|
|
430
|
+
subprocess.run(orden + ['--file', ruta, '-y'],
|
|
431
|
+
cwd=f'{RAIZ}/city/worker', check=True)
|
|
432
|
+
finally:
|
|
433
|
+
os.unlink(ruta)
|
|
434
|
+
print(f'loaded: {resumen}')
|
|
435
|
+
else:
|
|
436
|
+
print(salida)
|
|
437
|
+
print(f'-- {resumen}', file=sys.stderr)
|
|
438
|
+
return 0
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if __name__ == '__main__':
|
|
442
|
+
sys.exit(main())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
City Kit Commercial (2.1)
|
|
4
|
+
|
|
5
|
+
Created/distributed by Kenney (www.kenney.nl)
|
|
6
|
+
Creation date: 21-07-2025 18:16
|
|
7
|
+
|
|
8
|
+
------------------------------
|
|
9
|
+
|
|
10
|
+
License: (Creative Commons Zero, CC0)
|
|
11
|
+
http://creativecommons.org/publicdomain/zero/1.0/
|
|
12
|
+
|
|
13
|
+
You can use this content for personal, educational, and commercial purposes.
|
|
14
|
+
|
|
15
|
+
Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
|
|
16
|
+
|
|
17
|
+
------------------------------
|
|
18
|
+
|
|
19
|
+
• Website : www.kenney.nl
|
|
20
|
+
• Donate : www.kenney.nl/donate
|
|
21
|
+
|
|
22
|
+
• Patreon : patreon.com/kenney
|
|
23
|
+
|
|
24
|
+
Follow on social media for updates:
|
|
25
|
+
|
|
26
|
+
• Twitter: twitter.com/KenneyNL
|
|
27
|
+
• BlueSky: kenney.bsky.social
|
|
28
|
+
• Instagram: instagram.com/kenney_nl
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|