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,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""What makes a house grow depends on who lives in it.
|
|
3
|
+
|
|
4
|
+
The map grew every building from merged pull requests. That is right for a code
|
|
5
|
+
agent and meaningless for everyone else — a knowledge agent whose work is a
|
|
6
|
+
folder of documents, or a coordinator whose work is decisions, would forever
|
|
7
|
+
show an empty lot no matter how much they did. So growth is polymorphic: each
|
|
8
|
+
agent kind declares how its floors are counted, over its own workspace, with no
|
|
9
|
+
assumption of git.
|
|
10
|
+
|
|
11
|
+
- **code**: floors = merged PRs (the existing signal), bricks = commits with no
|
|
12
|
+
PR. Counted from the mounted repos, exactly as before.
|
|
13
|
+
- **knowledge**: floors = documents in the workspace, bricks = recent edits.
|
|
14
|
+
A person with no git at all now has a house that grows as their knowledge does.
|
|
15
|
+
- **coordinator**: floors = recorded decisions (deliberations), bricks = notices.
|
|
16
|
+
|
|
17
|
+
Every counter takes the same `(agente, data)` and returns the same
|
|
18
|
+
`{floors, bricks, activity30, signal}` shape, so the map renders one model and
|
|
19
|
+
the reporter pushes one row regardless of kind. Pure counting over the
|
|
20
|
+
filesystem; the git-backed `code` counter is injected so this module needs no
|
|
21
|
+
network and stays trivially testable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
import deliberations
|
|
28
|
+
import workspace
|
|
29
|
+
|
|
30
|
+
#: Extensions that count as a knowledge document.
|
|
31
|
+
DOC_EXT = ('.md', '.txt', '.rst', '.org', '.pdf', '.doc', '.docx', '.adoc')
|
|
32
|
+
DIAS_RECIENTE = 30
|
|
33
|
+
_SEG_RECIENTE = DIAS_RECIENTE * 24 * 3600
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ahora():
|
|
37
|
+
# Injected via env for deterministic tests; falls back to wall clock.
|
|
38
|
+
forzado = os.environ.get('CITY_GROWTH_NOW')
|
|
39
|
+
return float(forzado) if forzado else time.time()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cuenta_docs(raiz):
|
|
43
|
+
"""(total docs, docs edited within DIAS_RECIENTE) under a folder tree."""
|
|
44
|
+
total = recientes = 0
|
|
45
|
+
ahora = _ahora()
|
|
46
|
+
for base, dirs, files in os.walk(raiz):
|
|
47
|
+
dirs[:] = [d for d in dirs if d not in ('.git', 'node_modules', '.cache')]
|
|
48
|
+
for f in files:
|
|
49
|
+
if f.lower().endswith(DOC_EXT):
|
|
50
|
+
total += 1
|
|
51
|
+
try:
|
|
52
|
+
if ahora - os.path.getmtime(os.path.join(base, f)) < _SEG_RECIENTE:
|
|
53
|
+
recientes += 1
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
return total, recientes
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resultado(floors, bricks, actividad, signal):
|
|
60
|
+
return {'floors': floors, 'bricks': bricks, 'activity30': actividad, 'signal': signal}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _knowledge(agente, data, contador_git=None):
|
|
64
|
+
total = recientes = 0
|
|
65
|
+
vistos = set()
|
|
66
|
+
# The workspace always counts and mounts add to it — `or`-falling-back made
|
|
67
|
+
# the agent's own documents vanish the moment it mounted anything.
|
|
68
|
+
for destino in [agente.workspace] + list(workspace.mount_targets(agente, data) or []):
|
|
69
|
+
real = os.path.realpath(destino) if destino else ''
|
|
70
|
+
if not real or real in vistos or not os.path.isdir(real):
|
|
71
|
+
continue
|
|
72
|
+
vistos.add(real)
|
|
73
|
+
t, r = _cuenta_docs(real)
|
|
74
|
+
total += t
|
|
75
|
+
recientes += r
|
|
76
|
+
return _resultado(total, recientes, recientes, 'documents')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _coordinator(agente, data, contador_git=None):
|
|
80
|
+
# Count decisions through the canonical deliberation reader, not a re-glob:
|
|
81
|
+
# the store is a tree of `deliberations/<id>/` dirs, so listing `*.json`
|
|
82
|
+
# would always find nothing.
|
|
83
|
+
registros = deliberations.lista(data)
|
|
84
|
+
ahora = _ahora()
|
|
85
|
+
recientes = 0
|
|
86
|
+
for d in registros:
|
|
87
|
+
cuando = d.get('updated_at') or d.get('created_at')
|
|
88
|
+
try:
|
|
89
|
+
if cuando and ahora - _epoch(cuando) < _SEG_RECIENTE:
|
|
90
|
+
recientes += 1
|
|
91
|
+
except (ValueError, TypeError):
|
|
92
|
+
pass
|
|
93
|
+
return _resultado(len(registros), 0, recientes, 'decisions')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _code(agente, data, contador_git=None):
|
|
97
|
+
"""Floors from merged PRs via an injected git counter; 0 without one.
|
|
98
|
+
|
|
99
|
+
Kept injectable so this module needs no git and no network: the map/reporter
|
|
100
|
+
passes the existing PR counter, the tests pass a stub.
|
|
101
|
+
"""
|
|
102
|
+
if contador_git is None:
|
|
103
|
+
return _resultado(0, 0, 0, 'pull-requests')
|
|
104
|
+
prs, commits, act = contador_git(agente, data)
|
|
105
|
+
return _resultado(prs, commits, act, 'pull-requests')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
#: One counter per kind. Every counter has the same (agente, data, contador_git)
|
|
109
|
+
#: signature — the non-code ones ignore contador_git — so dispatch is uniform,
|
|
110
|
+
#: no special case. Keyed off workspace.CLASES so the kind vocabulary lives once;
|
|
111
|
+
#: `crece` degrades an unknown kind to _knowledge rather than crashing, so a new
|
|
112
|
+
#: kind added to workspace.CLASES stays safe here until a counter is written.
|
|
113
|
+
_CONTADORES = {
|
|
114
|
+
'code': _code,
|
|
115
|
+
'knowledge': _knowledge,
|
|
116
|
+
'coordinator': _coordinator,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _epoch(valor):
|
|
121
|
+
"""An ISO-8601 or epoch-ish timestamp -> epoch seconds. Raises on garbage.
|
|
122
|
+
|
|
123
|
+
A timezone-naive timestamp is read as UTC (not local time), so a coordinator's
|
|
124
|
+
recency window does not drift by the machine's UTC offset.
|
|
125
|
+
"""
|
|
126
|
+
import datetime
|
|
127
|
+
texto = str(valor)
|
|
128
|
+
try:
|
|
129
|
+
return float(texto)
|
|
130
|
+
except ValueError:
|
|
131
|
+
dt = datetime.datetime.fromisoformat(texto.replace('Z', '+00:00'))
|
|
132
|
+
if dt.tzinfo is None:
|
|
133
|
+
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
134
|
+
return dt.timestamp()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def crece(agente, data, contador_git=None):
|
|
138
|
+
"""The growth of one agent's house, dispatched uniformly on its kind."""
|
|
139
|
+
contador = _CONTADORES.get(agente.clase, _knowledge)
|
|
140
|
+
return contador(agente, data, contador_git)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only summaries of durable committee acts for the local Hall."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
ID_RE = re.compile(r'^delib_[a-z0-9][a-z0-9_-]{5,79}$')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def lista(datos):
|
|
11
|
+
root = os.path.join(os.path.realpath(datos), 'deliberations')
|
|
12
|
+
try:
|
|
13
|
+
nombres = sorted(os.listdir(root), reverse=True)
|
|
14
|
+
except OSError:
|
|
15
|
+
return []
|
|
16
|
+
fuera = []
|
|
17
|
+
for ident in nombres:
|
|
18
|
+
if not ID_RE.match(ident):
|
|
19
|
+
continue
|
|
20
|
+
try:
|
|
21
|
+
state = json.load(open(os.path.join(root, ident, 'state.json'),
|
|
22
|
+
encoding='utf-8'))
|
|
23
|
+
if state.get('schema') != 'agents-city/deliberation@1':
|
|
24
|
+
continue
|
|
25
|
+
brief = state['brief']
|
|
26
|
+
decisions = state.get('decisions') or []
|
|
27
|
+
decision = decisions[-1] if decisions else {}
|
|
28
|
+
verification = decision.get('verification') or {}
|
|
29
|
+
fuera.append({
|
|
30
|
+
'id': ident,
|
|
31
|
+
'status': state.get('status', 'unknown'),
|
|
32
|
+
'question': brief.get('question', ''),
|
|
33
|
+
'desired_outcome': brief.get('desiredOutcome', ''),
|
|
34
|
+
'participants': brief.get('participants', []),
|
|
35
|
+
'participant_roles': state.get('participantRoles', {}),
|
|
36
|
+
'received': len(state.get('positions') or {}),
|
|
37
|
+
'total': len(brief.get('participants') or []),
|
|
38
|
+
'revision': (state.get('progress') or {}).get('revision', 1),
|
|
39
|
+
'decision': decision.get('outcome', ''),
|
|
40
|
+
'contributors': decision.get('decisiveContributors', []),
|
|
41
|
+
'verifier': decision.get('verifier', ''),
|
|
42
|
+
'verification': verification.get('result', ''),
|
|
43
|
+
'updated_at': state.get('updatedAt', ''),
|
|
44
|
+
'act': os.path.join(root, ident, 'ACT.md'),
|
|
45
|
+
})
|
|
46
|
+
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
|
|
47
|
+
continue
|
|
48
|
+
return fuera
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""doctor: detect an old config shape, explain it, back it up, rewrite it.
|
|
3
|
+
|
|
4
|
+
The runtime reads one config schema — the current one. It keeps no long-lived
|
|
5
|
+
aliases that silently accept renamed or malformed keys, because a silent alias
|
|
6
|
+
is how two subtly different shapes drift apart. Instead, every change that can
|
|
7
|
+
invalidate an existing config ships a migration here: `doctor` finds the old
|
|
8
|
+
shape, says in one line what it is doing, writes a timestamped backup, and
|
|
9
|
+
rewrites the file to canonical form.
|
|
10
|
+
|
|
11
|
+
Each migration is a small object with three parts — does it apply, what does it
|
|
12
|
+
change, and the rewrite — so adding one is additive and never touches the
|
|
13
|
+
others (open for extension). `--dry-run` reports without writing; the default
|
|
14
|
+
writes a `.bak-<stamp>` next to the file first. Pure enough to test against a
|
|
15
|
+
temp file with no real config in sight.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Callable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Migracion:
|
|
28
|
+
id: str
|
|
29
|
+
explica: str
|
|
30
|
+
aplica: Callable[[dict], bool]
|
|
31
|
+
reescribe: Callable[[dict], dict]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _renombra_bus_url(cfg):
|
|
35
|
+
cfg = dict(cfg)
|
|
36
|
+
cfg['roadsUrl'] = cfg.pop('busUrl')
|
|
37
|
+
return cfg
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sella_bind(cfg):
|
|
41
|
+
cfg = dict(cfg)
|
|
42
|
+
gw = dict(cfg.get('gateway', {}))
|
|
43
|
+
gw.setdefault('bind', 'loopback')
|
|
44
|
+
cfg['gateway'] = gw
|
|
45
|
+
return cfg
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _cage_por_defecto(cfg):
|
|
49
|
+
cfg = dict(cfg)
|
|
50
|
+
seg = dict(cfg.get('security', {}))
|
|
51
|
+
if 'cage' not in seg:
|
|
52
|
+
seg['cage'] = True
|
|
53
|
+
cfg['security'] = seg
|
|
54
|
+
return cfg
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
#: Ordered so a file needing several migrations converges in one pass. Each is
|
|
58
|
+
#: idempotent: re-running doctor on a canonical file changes nothing.
|
|
59
|
+
MIGRACIONES = [
|
|
60
|
+
Migracion(
|
|
61
|
+
'bus-url-renamed-to-roads-url',
|
|
62
|
+
'the transport key `busUrl` is now `roadsUrl`',
|
|
63
|
+
lambda c: 'busUrl' in c,
|
|
64
|
+
_renombra_bus_url,
|
|
65
|
+
),
|
|
66
|
+
Migracion(
|
|
67
|
+
'gateway-bind-defaults-to-loopback',
|
|
68
|
+
'a gateway block without an explicit `bind` now pins `loopback`',
|
|
69
|
+
lambda c: isinstance(c.get('gateway'), dict) and 'bind' not in c['gateway'],
|
|
70
|
+
_sella_bind,
|
|
71
|
+
),
|
|
72
|
+
Migracion(
|
|
73
|
+
'cage-on-by-default',
|
|
74
|
+
'the per-window cage is recorded as on unless the owner set it',
|
|
75
|
+
# Guard the type like migration 2: a `security` that is present but not
|
|
76
|
+
# a dict (null/str/list) must not crash the tool that exists to fix it.
|
|
77
|
+
# Absent security counts as an empty dict, so the default is still added.
|
|
78
|
+
lambda c: isinstance(c.get('security', {}), dict) and 'cage' not in c.get('security', {}),
|
|
79
|
+
_cage_por_defecto,
|
|
80
|
+
),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _aplica(cfg):
|
|
85
|
+
"""The single migration walk: return (nuevo, [Migracion aplicadas]).
|
|
86
|
+
|
|
87
|
+
Everything else is a view of this — `diagnostica` wants the objects,
|
|
88
|
+
`cura` wants their ids, `cura_fichero` wants both — so the apply/rewrite
|
|
89
|
+
logic lives here once and never runs twice per file.
|
|
90
|
+
"""
|
|
91
|
+
aplicadas = []
|
|
92
|
+
trabajo = dict(cfg)
|
|
93
|
+
for m in MIGRACIONES:
|
|
94
|
+
if m.aplica(trabajo):
|
|
95
|
+
trabajo = m.reescribe(trabajo)
|
|
96
|
+
aplicadas.append(m)
|
|
97
|
+
return trabajo, aplicadas
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def diagnostica(cfg):
|
|
101
|
+
"""Return the ordered migrations that still apply to `cfg`."""
|
|
102
|
+
return _aplica(cfg)[1]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cura(cfg):
|
|
106
|
+
"""Apply every applicable migration in order; return (nuevo, ids_aplicadas)."""
|
|
107
|
+
nuevo, aplicadas = _aplica(cfg)
|
|
108
|
+
return nuevo, [m.id for m in aplicadas]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _stamp_backup(ruta, marca):
|
|
112
|
+
"""Copy the file to a timestamped sibling before rewriting. Marca is passed
|
|
113
|
+
in (never read from the clock here) so a caller controls naming and tests
|
|
114
|
+
stay deterministic."""
|
|
115
|
+
destino = f'{ruta}.bak-{marca}'
|
|
116
|
+
with open(ruta, encoding='utf-8') as f:
|
|
117
|
+
contenido = f.read()
|
|
118
|
+
with open(destino, 'w', encoding='utf-8') as f:
|
|
119
|
+
f.write(contenido)
|
|
120
|
+
os.chmod(destino, 0o600)
|
|
121
|
+
return destino
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def cura_fichero(ruta, marca, dry_run=False):
|
|
125
|
+
"""Diagnose and (unless dry-run) rewrite a config file. Returns a report."""
|
|
126
|
+
with open(ruta, encoding='utf-8') as f:
|
|
127
|
+
cfg = json.load(f)
|
|
128
|
+
nuevo, pendientes = _aplica(cfg) # one walk: report and rewrite come from it
|
|
129
|
+
reporte = {'file': ruta, 'migrations': [m.id for m in pendientes],
|
|
130
|
+
'explains': [m.explica for m in pendientes], 'wrote': False, 'backup': None}
|
|
131
|
+
if not pendientes or dry_run:
|
|
132
|
+
return reporte
|
|
133
|
+
reporte['backup'] = _stamp_backup(ruta, marca)
|
|
134
|
+
tmp = f'{ruta}.tmp'
|
|
135
|
+
with open(tmp, 'w', encoding='utf-8') as f:
|
|
136
|
+
json.dump(nuevo, f, indent=2, sort_keys=True)
|
|
137
|
+
# Preserve the original file's mode rather than inheriting the umask default
|
|
138
|
+
# (0644): a config that was 0600 must not be silently widened by a migration.
|
|
139
|
+
try:
|
|
140
|
+
os.chmod(tmp, os.stat(ruta).st_mode & 0o777)
|
|
141
|
+
except OSError:
|
|
142
|
+
os.chmod(tmp, 0o600)
|
|
143
|
+
os.replace(tmp, ruta)
|
|
144
|
+
reporte['wrote'] = True
|
|
145
|
+
return reporte
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
p = argparse.ArgumentParser(description='Detect, explain and migrate an old config shape.')
|
|
150
|
+
p.add_argument('fichero', help='the config JSON file to check')
|
|
151
|
+
p.add_argument('--fix', action='store_true', help='rewrite (default is dry-run report)')
|
|
152
|
+
p.add_argument('--stamp', default='manual', help='backup suffix (tests pass a fixed value)')
|
|
153
|
+
args = p.parse_args()
|
|
154
|
+
try:
|
|
155
|
+
reporte = cura_fichero(args.fichero, args.stamp, dry_run=not args.fix)
|
|
156
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
157
|
+
print(f'{e}', file=sys.stderr)
|
|
158
|
+
return 1
|
|
159
|
+
if not reporte['migrations']:
|
|
160
|
+
print('config is already canonical')
|
|
161
|
+
return 0
|
|
162
|
+
for ident, porque in zip(reporte['migrations'], reporte['explains'], strict=True):
|
|
163
|
+
print(f' {ident}: {porque}')
|
|
164
|
+
if reporte['wrote']:
|
|
165
|
+
print(f'rewritten; backup at {reporte["backup"]}')
|
|
166
|
+
else:
|
|
167
|
+
print('dry-run: pass --fix to apply')
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == '__main__':
|
|
172
|
+
sys.exit(main())
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Built-in work domains and the knowledge packs installed into one city.
|
|
3
|
+
|
|
4
|
+
A domain is the context of the city (software, healthcare, legal, finance...).
|
|
5
|
+
A role is the responsibility of its one owner seat inside that domain. Keeping
|
|
6
|
+
those two choices separate is important: ``data`` means something different in a
|
|
7
|
+
software product from a clinical-data responsibility, while neither is the city
|
|
8
|
+
itself.
|
|
9
|
+
|
|
10
|
+
The catalogue is plain Markdown under ``plugin/domains``. On selection, the
|
|
11
|
+
chosen domain pack and every non-blank seat/repo role pack are copied into the
|
|
12
|
+
city's own ``domains/`` and ``roles/`` folders. They are ordinary, editable files
|
|
13
|
+
after that. This is city knowledge, not a repo skill: Agents City never installs
|
|
14
|
+
or changes skills in a referenced repo.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import glob
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
|
|
22
|
+
import cities
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
PACKS = os.path.realpath(os.path.join(AQUI, "..", "domains"))
|
|
27
|
+
ROLE_PACKS = os.path.realpath(os.path.join(AQUI, "..", "roles", "examples"))
|
|
28
|
+
|
|
29
|
+
# Old cities called the domain ``kind`` and used these ids. Read them forever,
|
|
30
|
+
# but every new write uses the explicit ``domain`` field and canonical id.
|
|
31
|
+
ALIASES = {"product": "software", "blank": "custom"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def canonico(valor):
|
|
35
|
+
return ALIASES.get(str(valor or "").strip().lower(), str(valor or "").strip().lower())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _frontmatter(texto):
|
|
39
|
+
if not texto.startswith("---\n"):
|
|
40
|
+
return ""
|
|
41
|
+
partes = texto.split("---", 2)
|
|
42
|
+
return partes[1] if len(partes) == 3 else ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _escalar(fm, clave, defecto=""):
|
|
46
|
+
m = re.search(rf"^{re.escape(clave)}:[ \t]*(.*)$", fm, re.M)
|
|
47
|
+
return (m.group(1).strip() if m else "") or defecto
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _lista(fm, clave):
|
|
51
|
+
"""Indented ``- value`` rows below one frontmatter key."""
|
|
52
|
+
m = re.search(rf"^{re.escape(clave)}:[ \t]*\n((?:[ \t]+-[^\n]*\n?)*)", fm, re.M)
|
|
53
|
+
if not m:
|
|
54
|
+
return []
|
|
55
|
+
fuera = []
|
|
56
|
+
for linea in m.group(1).splitlines():
|
|
57
|
+
valor = re.sub(r"^[ \t]+-[ \t]*", "", linea).strip()
|
|
58
|
+
if not valor:
|
|
59
|
+
continue
|
|
60
|
+
base, _, nota = valor.partition("#")
|
|
61
|
+
fuera.append((base.strip().rstrip(), "on by default" in nota.lower()))
|
|
62
|
+
return fuera
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _unidades(fm):
|
|
66
|
+
fuera = []
|
|
67
|
+
for valor, _ in _lista(fm, "units"):
|
|
68
|
+
nombre, sep, color = valor.rpartition(";")
|
|
69
|
+
if sep and re.fullmatch(r"[0-9a-fA-F]{6}", color.strip()):
|
|
70
|
+
fuera.append((nombre.strip(), color.strip().lower()))
|
|
71
|
+
return fuera
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def lee(ruta):
|
|
75
|
+
try:
|
|
76
|
+
texto = open(ruta, encoding="utf-8").read()
|
|
77
|
+
except OSError:
|
|
78
|
+
return {}
|
|
79
|
+
fm = _frontmatter(texto)
|
|
80
|
+
ident = canonico(_escalar(fm, "id", os.path.basename(ruta)[:-3]))
|
|
81
|
+
roles = _lista(fm, "roles")
|
|
82
|
+
return {
|
|
83
|
+
"id": ident,
|
|
84
|
+
"order": int(_escalar(fm, "order", "999")),
|
|
85
|
+
"name": _escalar(fm, "name", ident.replace("-", " ").title()),
|
|
86
|
+
"summary": _escalar(fm, "summary"),
|
|
87
|
+
"parcel": _escalar(fm, "parcel", "a working repository"),
|
|
88
|
+
"source": _escalar(fm, "source", "github|disk"),
|
|
89
|
+
"grows": _escalar(fm, "grows_with"),
|
|
90
|
+
"grow_cmd": _escalar(fm, "grow_command"),
|
|
91
|
+
"roles": roles,
|
|
92
|
+
"units": _unidades(fm),
|
|
93
|
+
"path": ruta,
|
|
94
|
+
"text": texto,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def catalogo():
|
|
99
|
+
fuera = [lee(ruta) for ruta in glob.glob(os.path.join(PACKS, "*.md"))]
|
|
100
|
+
return sorted((p for p in fuera if p.get("id")), key=lambda p: (p["order"], p["name"].lower()))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def obtiene(ident):
|
|
104
|
+
buscado = canonico(ident)
|
|
105
|
+
return next((p for p in catalogo() if p["id"] == buscado), None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def roles_de(ident):
|
|
109
|
+
pack = obtiene(ident)
|
|
110
|
+
return list(pack["roles"]) if pack else []
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def dominio_de_rol(rol, defecto="software"):
|
|
114
|
+
for pack in catalogo():
|
|
115
|
+
if any(r == rol for r, _ in pack["roles"]):
|
|
116
|
+
return pack["id"]
|
|
117
|
+
return defecto
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def de_ciudad(datos, defecto="software"):
|
|
121
|
+
explicito = cities.lee_clave(datos, "domain")
|
|
122
|
+
legado = cities.lee_clave(datos, "kind")
|
|
123
|
+
elegido = canonico(explicito or legado or defecto)
|
|
124
|
+
return elegido if obtiene(elegido) else canonico(defecto)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def selecciona(datos, ident):
|
|
128
|
+
"""Persist the canonical domain without rewriting city identity metadata."""
|
|
129
|
+
ident = canonico(ident)
|
|
130
|
+
if not obtiene(ident):
|
|
131
|
+
raise ValueError(f"unknown city domain: {ident}")
|
|
132
|
+
ruta = os.path.join(datos, "city.yml")
|
|
133
|
+
try:
|
|
134
|
+
texto = open(ruta, encoding="utf-8").read()
|
|
135
|
+
except OSError:
|
|
136
|
+
texto = ""
|
|
137
|
+
if re.search(r"^domain:", texto, re.M):
|
|
138
|
+
texto = re.sub(r"^domain:.*$", f"domain: {ident}", texto, count=1, flags=re.M)
|
|
139
|
+
else:
|
|
140
|
+
if texto and not texto.endswith("\n"):
|
|
141
|
+
texto += "\n"
|
|
142
|
+
texto += f"domain: {ident}\n"
|
|
143
|
+
cities.escribe_atomico(ruta, texto)
|
|
144
|
+
return ident
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
INSTRUCCIONES = """# Agents City seat
|
|
148
|
+
|
|
149
|
+
This folder is one autonomous city, not a source-code repository and not a team
|
|
150
|
+
of people. Its only public actor is the owner seat; referenced repos are private
|
|
151
|
+
support agents behind that chair.
|
|
152
|
+
|
|
153
|
+
Before acting for this city:
|
|
154
|
+
|
|
155
|
+
1. Read `city.yml` for the selected `domain` and owner.
|
|
156
|
+
2. Read the owner card `<owner>.md` for the role, goal and support repos.
|
|
157
|
+
3. Read `domains/<domain>.md` and `roles/<role>.md` when that role file exists.
|
|
158
|
+
`blank` deliberately has no role file. These are transparent, editable
|
|
159
|
+
operating knowledge for this city, not hidden prompts.
|
|
160
|
+
4. Read each support agent's `role.<repo>` assignment from the owner card before
|
|
161
|
+
asking it for evidence. Use a chaired committee when several matter.
|
|
162
|
+
Repo agents do not talk directly to each other.
|
|
163
|
+
|
|
164
|
+
Skills remain installed in the referenced repos. Agents City only recognises
|
|
165
|
+
them; it never copies, installs or enables a repo skill from this folder.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def materializa(datos, ident, rol):
|
|
170
|
+
"""Copy selected built-in knowledge into a city, never over user edits.
|
|
171
|
+
|
|
172
|
+
A later role/domain switch gets its own file, so changing back restores the
|
|
173
|
+
city's edited copy. Existing files are deliberately not refreshed in place.
|
|
174
|
+
"""
|
|
175
|
+
pack = obtiene(ident)
|
|
176
|
+
if not pack:
|
|
177
|
+
return []
|
|
178
|
+
hechos = []
|
|
179
|
+
carpeta_dominios = os.path.join(datos, "domains")
|
|
180
|
+
carpeta_roles = os.path.join(datos, "roles")
|
|
181
|
+
os.makedirs(carpeta_dominios, exist_ok=True)
|
|
182
|
+
os.makedirs(carpeta_roles, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
destino_dominio = os.path.join(carpeta_dominios, f"{pack['id']}.md")
|
|
185
|
+
if not os.path.exists(destino_dominio):
|
|
186
|
+
shutil.copy2(pack["path"], destino_dominio)
|
|
187
|
+
hechos.append(f"domains/{pack['id']}.md")
|
|
188
|
+
|
|
189
|
+
rol_seguro = str(rol or "")
|
|
190
|
+
if re.fullmatch(r"[a-z0-9-]+", rol_seguro):
|
|
191
|
+
fuente_rol = os.path.join(ROLE_PACKS, f"{rol_seguro}.md")
|
|
192
|
+
destino_rol = os.path.join(carpeta_roles, f"{rol_seguro}.md")
|
|
193
|
+
if os.path.isfile(fuente_rol) and not os.path.exists(destino_rol):
|
|
194
|
+
shutil.copy2(fuente_rol, destino_rol)
|
|
195
|
+
hechos.append(f"roles/{rol_seguro}.md")
|
|
196
|
+
|
|
197
|
+
instrucciones = os.path.join(datos, "AGENTS.md")
|
|
198
|
+
if not os.path.exists(instrucciones):
|
|
199
|
+
cities.escribe_atomico(instrucciones, INSTRUCCIONES)
|
|
200
|
+
hechos.append("AGENTS.md")
|
|
201
|
+
return hechos
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The evidence vocabulary: what an audit line actually proves.
|
|
3
|
+
|
|
4
|
+
A hash chain gives tamper-evidence — that a line was not changed after the
|
|
5
|
+
fact. It says nothing about whether the thing the line describes was truly
|
|
6
|
+
authorised, merely observed, or never established at all. Those are different
|
|
7
|
+
claims, and collapsing them into a boolean `ok` is how a missing check comes to
|
|
8
|
+
read as a pass.
|
|
9
|
+
|
|
10
|
+
So every consequential record carries one of these states, and two rules hold
|
|
11
|
+
everywhere they are read:
|
|
12
|
+
|
|
13
|
+
- `DESCONOCIDO` never means allowed. A missing, stale, reused or unverifiable
|
|
14
|
+
binding is unknown, and unknown is not consent.
|
|
15
|
+
- `AMBIGUO` is never resolved by picking the first or latest candidate. If the
|
|
16
|
+
producer cannot say which decision governed, the record says so.
|
|
17
|
+
|
|
18
|
+
Only `IMPUESTO` — a decision that actually changed the outcome — is proof that
|
|
19
|
+
a control fired. `ATRIBUIDO` means the actor is known but no control was
|
|
20
|
+
exercised; `NO_IMPUESTO` means a control could have fired and did not.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from enum import Enum
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Evidencia(str, Enum):
|
|
27
|
+
IMPUESTO = 'enforced' # a decision that changed the outcome
|
|
28
|
+
ATRIBUIDO = 'attribution-only' # actor known, no control exercised
|
|
29
|
+
NO_IMPUESTO = 'unattributed' # a control could apply, none did
|
|
30
|
+
DESCONOCIDO = 'unknown' # missing/stale/reused/unverifiable — never allowed
|
|
31
|
+
AMBIGUO = 'ambiguous' # producer cannot say which decision governed
|
|
32
|
+
NO_SOPORTADO = 'unsupported' # this path has no authoritative integration
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
return self.value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def autoriza(estado):
|
|
39
|
+
"""True only for an evidence state that positively authorises the action.
|
|
40
|
+
|
|
41
|
+
A strict whitelist, so a new evidence state authorises nothing until it is
|
|
42
|
+
deliberately added here: only `IMPUESTO` passes. `ATRIBUIDO` deliberately
|
|
43
|
+
does NOT — knowing who asked is not the same as a control having permitted
|
|
44
|
+
it, so the caller must pair attribution with an explicit enforced decision.
|
|
45
|
+
|
|
46
|
+
Fails closed on unrecognised input via `normaliza`: an unknown wire string
|
|
47
|
+
becomes `DESCONOCIDO` (which does not authorise) rather than raising, so a
|
|
48
|
+
caller that skipped normalisation gets `False`, never an exception.
|
|
49
|
+
"""
|
|
50
|
+
return normaliza(estado) is Evidencia.IMPUESTO
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normaliza(valor, defecto=Evidencia.DESCONOCIDO):
|
|
54
|
+
"""Coerce arbitrary input to an Evidencia, failing safe to DESCONOCIDO.
|
|
55
|
+
|
|
56
|
+
Unrecognised input is never guessed toward a permissive state: an unknown
|
|
57
|
+
string becomes `DESCONOCIDO`, which authorises nothing.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(valor, Evidencia):
|
|
60
|
+
return valor
|
|
61
|
+
try:
|
|
62
|
+
return Evidencia(str(valor))
|
|
63
|
+
except ValueError:
|
|
64
|
+
return defecto
|