agents-city 0.1.0
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 +2226 -0
- package/README.md +2196 -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 +119 -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/doctor +3 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +498 -0
- package/bin/logs +3 -0
- package/bin/navegador.mjs +243 -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 +1579 -0
- package/bin/setup.py +753 -0
- package/bin/shortcut +3 -0
- package/bin/skills +3 -0
- package/bin/test +133 -0
- package/bin/test-actualiza.py +130 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-atajos.py +301 -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 +374 -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 +551 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +139 -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-navegador.py +140 -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 +1598 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1251 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +251 -0
- package/bin/testlib.py +118 -0
- package/bin/tokens.py +14 -0
- package/bin/update +3 -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 +1363 -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 +1959 -0
- package/city/web/src/main.ts +2252 -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 +106 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +265 -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/actualiza.py +198 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +492 -0
- package/plugin/scripts/capabilities.py +210 -0
- package/plugin/scripts/card.py +322 -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 +552 -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 +294 -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 +55 -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 +1696 -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 +561 -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,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create a short, auditable launcher so tmux never has to paste long commands."""
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import runtime_processes
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def safe(value):
|
|
14
|
+
cleaned = ''.join(c if c.isalnum() or c in '-_' else '-' for c in value.lower())
|
|
15
|
+
return cleaned.strip('-')[:80] or 'actor'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make(args):
|
|
19
|
+
directory = os.path.join(runtime_processes.ruta(args.data), 'launchers')
|
|
20
|
+
app_home = os.path.realpath(os.environ.get('AGENTS_CITY_HOME') or
|
|
21
|
+
os.path.join(args.data, '..', '..'))
|
|
22
|
+
os.makedirs(directory, mode=0o700, exist_ok=True)
|
|
23
|
+
os.chmod(directory, 0o700)
|
|
24
|
+
path = os.path.join(directory, f'{safe(args.actor)}-{os.getpid()}-{time.time_ns()}.sh')
|
|
25
|
+
command = base64.b64encode(args.command.encode('utf-8')).decode('ascii')
|
|
26
|
+
failure = base64.b64encode(json.dumps({
|
|
27
|
+
'sourceId': f'launcher:{args.actor}:{time.time_ns()}:failed',
|
|
28
|
+
'kind': 'runtime.launch.failed',
|
|
29
|
+
'phase': 'failed',
|
|
30
|
+
'tone': 'error',
|
|
31
|
+
'title': f'{args.actor} failed to start',
|
|
32
|
+
'summary': 'The agent command exited before a usable session was ready.',
|
|
33
|
+
}).encode('utf-8')).decode('ascii')
|
|
34
|
+
log = os.path.realpath(os.path.join(os.path.dirname(__file__), 'runtime_log.py'))
|
|
35
|
+
client = os.path.realpath(args.client)
|
|
36
|
+
lines = [
|
|
37
|
+
'#!/usr/bin/env bash',
|
|
38
|
+
'set +e',
|
|
39
|
+
f'DATA={shlex.quote(os.path.realpath(args.data))}',
|
|
40
|
+
f'APP_HOME={shlex.quote(app_home)}',
|
|
41
|
+
f'ACTOR={shlex.quote(args.actor)}',
|
|
42
|
+
f'WORKDIR={shlex.quote(os.path.realpath(args.cwd))}',
|
|
43
|
+
f'RUNTIME_DIR={shlex.quote(os.path.dirname(directory))}',
|
|
44
|
+
f'CLIENT={shlex.quote(client)}',
|
|
45
|
+
f'RUNTIME_LOG={shlex.quote(log)}',
|
|
46
|
+
f'COMMAND_B64={shlex.quote(command)}',
|
|
47
|
+
f'FAILURE_B64={shlex.quote(failure)}',
|
|
48
|
+
'export AGENTS_CITY_HOME="$APP_HOME" AGENTS_CITY_DATA="$DATA" CITY_BUS_ACTOR="$ACTOR"',
|
|
49
|
+
'python3 "$RUNTIME_LOG" launch.started --data "$DATA" --component launcher '
|
|
50
|
+
' --actor "$ACTOR" --outcome starting',
|
|
51
|
+
'cd "$WORKDIR"',
|
|
52
|
+
'cd_status=$?',
|
|
53
|
+
'if [ "$cd_status" -eq 0 ]; then',
|
|
54
|
+
' command_text="$(printf %s "$COMMAND_B64" | base64 --decode)"',
|
|
55
|
+
' eval "$command_text"',
|
|
56
|
+
' status=$?',
|
|
57
|
+
'else',
|
|
58
|
+
' status=$cd_status',
|
|
59
|
+
'fi',
|
|
60
|
+
'if [ "$status" -eq 0 ]; then',
|
|
61
|
+
' python3 "$RUNTIME_LOG" launch.exited --data "$DATA" --component launcher '
|
|
62
|
+
' --actor "$ACTOR" --outcome ok --message "exit 0"',
|
|
63
|
+
'else',
|
|
64
|
+
' python3 "$RUNTIME_LOG" launch.failed --data "$DATA" --component launcher '
|
|
65
|
+
' --actor "$ACTOR" --outcome failed --message "exit $status"',
|
|
66
|
+
' printf %s "$FAILURE_B64" | base64 --decode | env AGENTS_CITY_DATA="$DATA" '
|
|
67
|
+
' CITY_BUS_ACTOR="$ACTOR" node "$CLIENT" activity publish '
|
|
68
|
+
' --input - >/dev/null 2>&1 || true',
|
|
69
|
+
' printf "\\n Agents City: %s failed to start (exit %s).\\n" "$ACTOR" "$status" >&2',
|
|
70
|
+
' printf " Diagnostics: %s/diagnostics.jsonl\\n\\n" "$RUNTIME_DIR" >&2',
|
|
71
|
+
'fi',
|
|
72
|
+
'exit "$status"',
|
|
73
|
+
'',
|
|
74
|
+
]
|
|
75
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o700)
|
|
76
|
+
with os.fdopen(fd, 'w', encoding='utf-8') as stream:
|
|
77
|
+
stream.write('\n'.join(lines))
|
|
78
|
+
print(path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
parser = argparse.ArgumentParser()
|
|
83
|
+
parser.add_argument('verb', choices=['create'])
|
|
84
|
+
parser.add_argument('--data', required=True)
|
|
85
|
+
parser.add_argument('--actor', required=True)
|
|
86
|
+
parser.add_argument('--cwd', required=True)
|
|
87
|
+
parser.add_argument('--client', required=True)
|
|
88
|
+
parser.add_argument('--command', required=True)
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
make(args)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == '__main__':
|
|
94
|
+
main()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read or follow one city's durable activity and diagnostic logs."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import city_env
|
|
10
|
+
import runtime_processes
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read(path):
|
|
14
|
+
try:
|
|
15
|
+
with open(path, encoding='utf-8') as stream:
|
|
16
|
+
return [json.loads(line) for line in stream if line.strip()]
|
|
17
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def display(record, raw=False):
|
|
22
|
+
if raw:
|
|
23
|
+
print(json.dumps(record, ensure_ascii=False), flush=True)
|
|
24
|
+
return
|
|
25
|
+
at = str(record.get('at', ''))[11:19] or '--:--:--'
|
|
26
|
+
source = 'activity' if record.get('protocol') == 'agents-city-activity/1' else 'diagnostic'
|
|
27
|
+
actor = record.get('actor') or record.get('component') or 'city'
|
|
28
|
+
event = record.get('kind') or record.get('event') or 'event'
|
|
29
|
+
message = record.get('summary') or record.get('message') or record.get('outcome') or ''
|
|
30
|
+
print(f'{at} {source:<10} {str(actor):<18} {event} {message}', flush=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(): # noqa: C901 - CLI selection and follow mode share one stream lifecycle
|
|
34
|
+
parser = argparse.ArgumentParser(
|
|
35
|
+
description='show persistent city conversations and operational diagnostics')
|
|
36
|
+
group = parser.add_mutually_exclusive_group()
|
|
37
|
+
group.add_argument(
|
|
38
|
+
'--activity', action='store_true', help='only visible conversation/work events')
|
|
39
|
+
group.add_argument(
|
|
40
|
+
'--diagnostics', action='store_true', help='only operational diagnostics')
|
|
41
|
+
parser.add_argument('-f', '--follow', action='store_true', help='keep following new events')
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
'-n', '--lines', type=int, default=100, help='initial records (default: 100)')
|
|
44
|
+
parser.add_argument('--json', action='store_true', help='emit JSONL unchanged')
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
|
|
47
|
+
data = city_env.datos()
|
|
48
|
+
if not data:
|
|
49
|
+
print('agents-city: no selected city', file=sys.stderr)
|
|
50
|
+
return 2
|
|
51
|
+
directory = runtime_processes.ruta(data)
|
|
52
|
+
selected = []
|
|
53
|
+
if not args.diagnostics:
|
|
54
|
+
selected.append(os.path.join(directory, 'activity.jsonl'))
|
|
55
|
+
if not args.activity:
|
|
56
|
+
selected.append(os.path.join(directory, 'diagnostics.jsonl'))
|
|
57
|
+
|
|
58
|
+
rows = []
|
|
59
|
+
positions = {}
|
|
60
|
+
for path in selected:
|
|
61
|
+
values = read(path)
|
|
62
|
+
rows.extend(values)
|
|
63
|
+
try:
|
|
64
|
+
positions[path] = os.path.getsize(path)
|
|
65
|
+
except OSError:
|
|
66
|
+
positions[path] = 0
|
|
67
|
+
rows.sort(key=lambda row: str(row.get('at', '')))
|
|
68
|
+
for row in rows[-max(0, args.lines):]:
|
|
69
|
+
display(row, args.json)
|
|
70
|
+
if not args.follow:
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
while True:
|
|
75
|
+
for path in selected:
|
|
76
|
+
try:
|
|
77
|
+
size = os.path.getsize(path)
|
|
78
|
+
if size < positions[path]:
|
|
79
|
+
positions[path] = 0
|
|
80
|
+
if size == positions[path]:
|
|
81
|
+
continue
|
|
82
|
+
with open(path, encoding='utf-8') as stream:
|
|
83
|
+
stream.seek(positions[path])
|
|
84
|
+
for line in stream:
|
|
85
|
+
try:
|
|
86
|
+
display(json.loads(line), args.json)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
pass
|
|
89
|
+
positions[path] = stream.tell()
|
|
90
|
+
except OSError:
|
|
91
|
+
continue
|
|
92
|
+
time.sleep(.2)
|
|
93
|
+
except KeyboardInterrupt:
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == '__main__':
|
|
98
|
+
sys.exit(main())
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Road pairing: how an unknown city earns the right to write to a seat.
|
|
3
|
+
|
|
4
|
+
Exchanging invitation files works, but it is friction, and friction is why
|
|
5
|
+
nobody connects two cities. Pairing is the low-friction path: an unknown seat's
|
|
6
|
+
first message yields a short code the owner approves once. Approving grants one
|
|
7
|
+
thing only — permission for that address to send to this seat — never road
|
|
8
|
+
membership, never chair authority, never anything the code's holder did not
|
|
9
|
+
already have.
|
|
10
|
+
|
|
11
|
+
The safety comes from four constants and two rules. A code is short and
|
|
12
|
+
unambiguous (no 0/O/1/I) so it can be read aloud; it expires; only a bounded
|
|
13
|
+
number can be pending per city, so a flood cannot mint an unbounded backlog;
|
|
14
|
+
and the human code is never echoed by tooling — approvals happen by an opaque
|
|
15
|
+
id derived from it. The two rules: a pending request is created (and its code
|
|
16
|
+
revealed) at most once per sender per window, and an approval is only ever
|
|
17
|
+
consulted under the pairing policy, never to widen an allowlist.
|
|
18
|
+
|
|
19
|
+
State is one JSON file per city under its runtime dir. No network, no daemon:
|
|
20
|
+
the seat reads and writes it, the tests drive it on any platform.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import secrets
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
CODIGO_LARGO = 8
|
|
30
|
+
CODIGO_ALFABETO = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' # no 0/O/1/I, read-aloud safe
|
|
31
|
+
PENDIENTE_TTL_MS = 60 * 60 * 1000 # a code is good for one hour
|
|
32
|
+
PENDIENTE_MAX = 3 # at most this many pending per city
|
|
33
|
+
CONCEDIDO_MAX = 500 # a generous cap on approved senders
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ahora_ms():
|
|
37
|
+
return int(time.time() * 1000)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _estado(datos):
|
|
41
|
+
real = os.path.realpath(os.path.expanduser(datos))
|
|
42
|
+
ruta = os.path.join(real, '.runtime')
|
|
43
|
+
os.makedirs(ruta, mode=0o700, exist_ok=True)
|
|
44
|
+
return os.path.join(ruta, 'pairing.json')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _lee(datos):
|
|
48
|
+
try:
|
|
49
|
+
with open(_estado(datos), encoding='utf-8') as f:
|
|
50
|
+
objeto = json.load(f)
|
|
51
|
+
except (OSError, json.JSONDecodeError):
|
|
52
|
+
objeto = {}
|
|
53
|
+
objeto.setdefault('pending', {})
|
|
54
|
+
objeto.setdefault('granted', {})
|
|
55
|
+
return objeto
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _escribe(datos, objeto):
|
|
59
|
+
ruta = _estado(datos)
|
|
60
|
+
tmp = f'{ruta}.tmp'
|
|
61
|
+
with open(tmp, 'w', encoding='utf-8') as f:
|
|
62
|
+
json.dump(objeto, f, indent=2, sort_keys=True)
|
|
63
|
+
os.chmod(tmp, 0o600)
|
|
64
|
+
os.replace(tmp, ruta)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _codigo():
|
|
68
|
+
return ''.join(secrets.choice(CODIGO_ALFABETO) for _ in range(CODIGO_LARGO))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def id_solicitud(direccion, codigo):
|
|
72
|
+
"""The opaque id a tool uses to approve, so the human code never travels."""
|
|
73
|
+
crudo = f'{direccion}\0{codigo}'.encode()
|
|
74
|
+
return 'pr_' + hashlib.sha256(crudo).hexdigest()[:20]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _purga(objeto, ahora):
|
|
78
|
+
objeto['pending'] = {
|
|
79
|
+
d: p for d, p in objeto['pending'].items()
|
|
80
|
+
if ahora - int(p.get('ts', 0)) < PENDIENTE_TTL_MS
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def concedido(datos, direccion):
|
|
85
|
+
"""True when this seat address has already been approved to write here."""
|
|
86
|
+
objeto = _lee(datos)
|
|
87
|
+
return direccion in objeto['granted']
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def solicita(datos, direccion):
|
|
91
|
+
"""Record a pending pairing for `direccion`, returning the code exactly once.
|
|
92
|
+
|
|
93
|
+
Returns `{"code", "id", "created": True}` the first time within the TTL, and
|
|
94
|
+
`{"id", "created": False}` on any repeat — the code is revealed once, so
|
|
95
|
+
re-messaging cannot re-spam it, and a flood cannot exceed the pending cap.
|
|
96
|
+
"""
|
|
97
|
+
objeto = _lee(datos)
|
|
98
|
+
if direccion in objeto['granted']:
|
|
99
|
+
return {'created': False, 'already': True}
|
|
100
|
+
ahora = _ahora_ms()
|
|
101
|
+
_purga(objeto, ahora)
|
|
102
|
+
existente = objeto['pending'].get(direccion)
|
|
103
|
+
if existente:
|
|
104
|
+
return {'created': False, 'id': existente['id']}
|
|
105
|
+
if len(objeto['pending']) >= PENDIENTE_MAX:
|
|
106
|
+
return {'created': False, 'full': True}
|
|
107
|
+
codigo = _codigo()
|
|
108
|
+
entrada = {'code': codigo, 'id': id_solicitud(direccion, codigo), 'ts': ahora}
|
|
109
|
+
objeto['pending'][direccion] = entrada
|
|
110
|
+
_escribe(datos, objeto)
|
|
111
|
+
return {'created': True, 'code': codigo, 'id': entrada['id']}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _concede(datos, seleccion):
|
|
115
|
+
"""Grant one pending request in a single read/write, given a picker.
|
|
116
|
+
|
|
117
|
+
`seleccion(pending) -> direccion|None` is the only thing that differs
|
|
118
|
+
between approving by code and approving by opaque id, so both entry points
|
|
119
|
+
share the read, purge, cap check, grant and write exactly once.
|
|
120
|
+
"""
|
|
121
|
+
objeto = _lee(datos)
|
|
122
|
+
ahora = _ahora_ms()
|
|
123
|
+
_purga(objeto, ahora)
|
|
124
|
+
encontrado = seleccion(objeto['pending'])
|
|
125
|
+
if not encontrado:
|
|
126
|
+
_escribe(datos, objeto)
|
|
127
|
+
return None
|
|
128
|
+
if len(objeto['granted']) >= CONCEDIDO_MAX:
|
|
129
|
+
raise ValueError('the pairing grant table is full; revoke unused senders first')
|
|
130
|
+
objeto['granted'][encontrado] = {'ts': ahora}
|
|
131
|
+
del objeto['pending'][encontrado]
|
|
132
|
+
_escribe(datos, objeto)
|
|
133
|
+
return encontrado
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _por_campo(campo, objetivo):
|
|
137
|
+
"""A picker matching one pending field in constant time — no timing signal.
|
|
138
|
+
|
|
139
|
+
The target is compared as UTF-8 bytes so a non-ASCII or non-string input
|
|
140
|
+
(e.g. a client-supplied opaque id) yields a clean no-match instead of the
|
|
141
|
+
TypeError `compare_digest` raises on non-ASCII strings.
|
|
142
|
+
"""
|
|
143
|
+
objetivo_b = str(objetivo or '').encode('utf-8')
|
|
144
|
+
|
|
145
|
+
def elige(pending):
|
|
146
|
+
hallado = None
|
|
147
|
+
for direccion, p in pending.items():
|
|
148
|
+
if secrets.compare_digest(str(p.get(campo, '')).encode('utf-8'), objetivo_b):
|
|
149
|
+
hallado = direccion
|
|
150
|
+
return hallado
|
|
151
|
+
return elige
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def aprueba(datos, codigo):
|
|
155
|
+
"""Approve a pending request by its human code. Returns the address or None.
|
|
156
|
+
|
|
157
|
+
A non-string or empty code matches nothing and returns None — the
|
|
158
|
+
documented 'a wrong code approves nothing', never a crash.
|
|
159
|
+
"""
|
|
160
|
+
return _concede(datos, _por_campo('code', str(codigo or '').strip().upper()))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def aprueba_por_id(datos, id_):
|
|
164
|
+
"""Approve by opaque id (what tooling holds), never echoing the code."""
|
|
165
|
+
return _concede(datos, _por_campo('id', id_))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def revoca(datos, direccion):
|
|
169
|
+
"""Withdraw a previously granted sender. Returns True if one was removed."""
|
|
170
|
+
objeto = _lee(datos)
|
|
171
|
+
quitado = objeto['granted'].pop(direccion, None) is not None
|
|
172
|
+
objeto['pending'].pop(direccion, None)
|
|
173
|
+
_escribe(datos, objeto)
|
|
174
|
+
return quitado
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def pendientes(datos):
|
|
178
|
+
"""Opaque view of pending requests: id + age, never the code."""
|
|
179
|
+
objeto = _lee(datos)
|
|
180
|
+
ahora = _ahora_ms()
|
|
181
|
+
_purga(objeto, ahora)
|
|
182
|
+
return [
|
|
183
|
+
{'address': d, 'id': p['id'], 'ageMinutes': (ahora - int(p['ts'])) // 60000}
|
|
184
|
+
for d, p in sorted(objeto['pending'].items())
|
|
185
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The one reader of `parcels.yml`.
|
|
3
|
+
|
|
4
|
+
There were two, and they were not the same reader. Both walked the format with the
|
|
5
|
+
same byte-identical regex, and then they disagreed: the seeder handled the `lab:`
|
|
6
|
+
section and complained about lines it could not parse; the reporter did neither,
|
|
7
|
+
and read the folder out of a key called `path` that this format has never had. So
|
|
8
|
+
the map and the growth reporter had different ideas about the same file, and the
|
|
9
|
+
reporter's idea was wrong in a way that stopped it working at all.
|
|
10
|
+
|
|
11
|
+
Two readers of one format is a bug waiting for someone to edit only one of them.
|
|
12
|
+
|
|
13
|
+
It lives here, in `plugin/scripts/`, and not next to the seeder, because this
|
|
14
|
+
directory is what gets installed on everybody's machine — the plugin ships without
|
|
15
|
+
`city/`, so the plugin cannot import from there. The seeder runs from a clone and
|
|
16
|
+
can import from here, so this is the only direction that works.
|
|
17
|
+
|
|
18
|
+
Deliberately not YAML: the format is a line at a time on purpose, so this runs
|
|
19
|
+
anywhere with no dependency to install.
|
|
20
|
+
|
|
21
|
+
repos:
|
|
22
|
+
main-site:
|
|
23
|
+
- ruta: "content/**/deposits/**" ; unidad: banking ; nombre: "main · deposits"
|
|
24
|
+
- ruta: "" ; unidad: none ; nombre: "main · platform"
|
|
25
|
+
lab: []
|
|
26
|
+
"""
|
|
27
|
+
import re
|
|
28
|
+
|
|
29
|
+
# A parcel line. Values may be quoted, fields are separated by `;`, and a missing
|
|
30
|
+
# field is a default rather than an error.
|
|
31
|
+
CAMPO = re.compile(r'(\w+):\s*"?([^";\n]*?)"?\s*(?:;|$)')
|
|
32
|
+
CABECERA = re.compile(r'^[a-zA-Z0-9_.\-]+:\s*$') # `repos:` / `lab:`
|
|
33
|
+
REPO = re.compile(r'^ [a-zA-Z0-9_.\-]+:\s*$') # ` main-site:`
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def campos(linea):
|
|
37
|
+
"""`- ruta: "x" ; unidad: y ; nombre: "z"` -> {'ruta': 'x', …}"""
|
|
38
|
+
return dict(CAMPO.findall(linea.strip().lstrip('- ')))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def identidad(repo, ruta):
|
|
42
|
+
"""A parcel's id, which has to be computed the same way everywhere.
|
|
43
|
+
|
|
44
|
+
The reporter pushes growth keyed on this and the map stores rows keyed on this.
|
|
45
|
+
If the two ever disagree, growth lands on nothing and neither side says so.
|
|
46
|
+
"""
|
|
47
|
+
return f'{repo}:{ruta}' if ruta else repo
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def lee(ruta_fichero):
|
|
51
|
+
"""Walk the file once. Returns (parcelas, lab, raras).
|
|
52
|
+
|
|
53
|
+
parcelas: [{'id', 'repo', 'ruta', 'unidad', 'nombre'}] in file order
|
|
54
|
+
lab: {repo, …} listed under `lab:`
|
|
55
|
+
raras: lines under a repo that are not parcels — the caller decides whether
|
|
56
|
+
to complain, because a silent one is how a repo ends up in the wrong
|
|
57
|
+
district for a reason nobody can find
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
with open(ruta_fichero, encoding='utf-8') as f:
|
|
61
|
+
txt = f.read()
|
|
62
|
+
except (FileNotFoundError, IsADirectoryError):
|
|
63
|
+
return [], set(), []
|
|
64
|
+
|
|
65
|
+
fuera, lab, raras = [], set(), []
|
|
66
|
+
repo, en_lab = None, False
|
|
67
|
+
for linea in txt.splitlines():
|
|
68
|
+
if re.match(r'^lab:\s*$', linea):
|
|
69
|
+
en_lab, repo = True, None
|
|
70
|
+
continue
|
|
71
|
+
if CABECERA.match(linea):
|
|
72
|
+
en_lab = False
|
|
73
|
+
continue
|
|
74
|
+
if REPO.match(linea):
|
|
75
|
+
repo = linea.strip().rstrip(':')
|
|
76
|
+
elif linea.strip().startswith('- ruta:') and repo:
|
|
77
|
+
c = campos(linea)
|
|
78
|
+
r = c.get('ruta', '')
|
|
79
|
+
fuera.append({'id': identidad(repo, r), 'repo': repo, 'ruta': r,
|
|
80
|
+
'unidad': c.get('unidad', 'none'),
|
|
81
|
+
'nombre': c.get('nombre', repo)})
|
|
82
|
+
elif linea.strip().startswith('- '):
|
|
83
|
+
suelto = linea.strip().lstrip('- ').strip()
|
|
84
|
+
if en_lab:
|
|
85
|
+
if suelto:
|
|
86
|
+
lab.add(suelto)
|
|
87
|
+
elif repo:
|
|
88
|
+
raras.append(linea.strip()[:70])
|
|
89
|
+
return fuera, lab, raras
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def escribe(ruta_fichero, porRepo, lab=()):
|
|
93
|
+
"""Write the whole file back, in the shape `lee` reads.
|
|
94
|
+
|
|
95
|
+
`porRepo` is {repo: [{'ruta', 'unidad', 'nombre'}, …]}. This exists so the
|
|
96
|
+
modelling — the one thing no tool can do for you — can be edited with a form
|
|
97
|
+
instead of by hand in a format nobody remembers. Round-tripping through `lee`
|
|
98
|
+
is pinned by a test, because a writer that drifts from its reader is exactly
|
|
99
|
+
the bug this module was created to end.
|
|
100
|
+
"""
|
|
101
|
+
lineas = ['# Which slice of which repo serves which unit.',
|
|
102
|
+
'#',
|
|
103
|
+
'# A house is not a repo: it is a **parcel**, a slice of a repo serving one',
|
|
104
|
+
'# unit. One repo can hold several, and that is the central fact of most',
|
|
105
|
+
'# codebases.',
|
|
106
|
+
'',
|
|
107
|
+
'repos:']
|
|
108
|
+
for repo in sorted(porRepo):
|
|
109
|
+
filas = porRepo[repo] or [{'ruta': '', 'unidad': 'none', 'nombre': repo}]
|
|
110
|
+
lineas.append(f' {repo}:')
|
|
111
|
+
for f in filas:
|
|
112
|
+
ruta = str(f.get('ruta', '')).replace(';', '').replace('"', '')
|
|
113
|
+
unidad = str(f.get('unidad', 'none')).replace(';', '') or 'none'
|
|
114
|
+
nombre = str(f.get('nombre') or repo).replace(';', ',').replace('"', "'")
|
|
115
|
+
lineas.append(f' - ruta: "{ruta}" ; unidad: {unidad} ; nombre: "{nombre}"')
|
|
116
|
+
lineas += ['', '# Research that does not ship yet gets its own district instead of',
|
|
117
|
+
'# reading as waste among the rest.']
|
|
118
|
+
lineas.append('lab:' if lab else 'lab: []')
|
|
119
|
+
for r in sorted(lab):
|
|
120
|
+
lineas.append(f' - {r}')
|
|
121
|
+
with open(ruta_fichero, 'w', encoding='utf-8') as f:
|
|
122
|
+
f.write('\n'.join(lineas) + '\n')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def aviso(raras):
|
|
126
|
+
"""What to say about lines that could not be read, or '' if there were none."""
|
|
127
|
+
if not raras:
|
|
128
|
+
return ''
|
|
129
|
+
lineas = [f'parcels.yml: {len(raras)} line(s) I could not read — they are being '
|
|
130
|
+
f'ignored. Use: - ruta: "<glob>" ; unidad: <unit> ; nombre: "<name>"']
|
|
131
|
+
lineas += [f' {r}' for r in raras[:5]]
|
|
132
|
+
return '\n'.join(lineas)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read one field out of a card's frontmatter, for the shell scripts.
|
|
3
|
+
|
|
4
|
+
read-card.py <card.md> agent
|
|
5
|
+
read-card.py <card.md> repos -> comma-separated list
|
|
6
|
+
read-card.py --window <repo> -> canonical tmux/bus actor slug
|
|
7
|
+
read-card.py --actor-role <card.md> <actor> -> safe operating role or blank
|
|
8
|
+
|
|
9
|
+
A thin front on card.py, which is where the parsing lives. It used to hold its own
|
|
10
|
+
copy, and that copy is why the tmux session asked for `agente` — a field no card in
|
|
11
|
+
this repo has ever carried — and got nothing every single time.
|
|
12
|
+
|
|
13
|
+
`agente` still answers, for a card somebody wrote by hand.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
19
|
+
import card # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
if len(sys.argv) == 3 and sys.argv[1] == '--window':
|
|
24
|
+
print(card.ventana(sys.argv[2]))
|
|
25
|
+
return
|
|
26
|
+
if len(sys.argv) == 4 and sys.argv[1] == '--actor-role':
|
|
27
|
+
try:
|
|
28
|
+
texto = open(sys.argv[2], encoding='utf-8').read()
|
|
29
|
+
except OSError as e:
|
|
30
|
+
sys.exit(f'cannot read {sys.argv[2]}: {e}')
|
|
31
|
+
actor = card.ventana(sys.argv[3])
|
|
32
|
+
print(card.rol_seguro(card.campo(texto, f'role.{actor}')))
|
|
33
|
+
return
|
|
34
|
+
if len(sys.argv) < 3:
|
|
35
|
+
sys.exit(
|
|
36
|
+
'usage: read-card.py <card.md> <field> | --window <repo> | '
|
|
37
|
+
'--actor-role <card.md> <actor>'
|
|
38
|
+
)
|
|
39
|
+
ruta, campo = sys.argv[1], sys.argv[2]
|
|
40
|
+
try:
|
|
41
|
+
with open(ruta, encoding='utf-8') as f:
|
|
42
|
+
texto = f.read()
|
|
43
|
+
except OSError as e:
|
|
44
|
+
sys.exit(f'cannot read {ruta}: {e}')
|
|
45
|
+
valor = card.campo(texto, campo)
|
|
46
|
+
# A bracketed value is a list everywhere else in this product — `repos:`,
|
|
47
|
+
# `agents:`, `mounts.<agent>:` — and the shell wants one line. Deciding by
|
|
48
|
+
# SHAPE rather than by a list of blessed field names is what stopped this
|
|
49
|
+
# from having to be edited every time the card grew another list.
|
|
50
|
+
esLista = valor.strip().startswith('[') and valor.strip().endswith(']')
|
|
51
|
+
print(','.join(card.lista(valor)) if esLista else valor)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__':
|
|
55
|
+
main()
|