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,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live, read-only recognition of skills available to a city's repo agents.
|
|
3
|
+
|
|
4
|
+
Agents City never installs, copies, enables or edits a skill. The runtime in a
|
|
5
|
+
repo remains the source of truth and decides whether a skill applies to a task.
|
|
6
|
+
This module only discovers conventional repo-local ``SKILL.md`` files so a seat
|
|
7
|
+
can see which agent is likely to know how to help.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import card
|
|
16
|
+
import cities
|
|
17
|
+
import workspace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RAICES = (".claude/skills", ".codex/skills", ".agents/skills", "skills")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _campo(texto, clave):
|
|
24
|
+
if not texto.startswith("---"):
|
|
25
|
+
return ""
|
|
26
|
+
partes = texto.split("---", 2)
|
|
27
|
+
if len(partes) < 3:
|
|
28
|
+
return ""
|
|
29
|
+
m = re.search(rf"^{re.escape(clave)}:[ \t]*(.+)$", partes[1], re.M)
|
|
30
|
+
return m.group(1).strip().strip("\"'") if m else ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def descubre_repo(ruta_repo):
|
|
34
|
+
"""Skills declared by this repo, with no cache and no writes."""
|
|
35
|
+
fuera, vistos = [], set()
|
|
36
|
+
raiz_manifest = os.path.join(ruta_repo, "SKILL.md")
|
|
37
|
+
if os.path.isfile(raiz_manifest):
|
|
38
|
+
try:
|
|
39
|
+
texto = open(raiz_manifest, encoding="utf-8").read()
|
|
40
|
+
except OSError:
|
|
41
|
+
texto = ""
|
|
42
|
+
if texto:
|
|
43
|
+
real = os.path.realpath(raiz_manifest)
|
|
44
|
+
vistos.add(real)
|
|
45
|
+
fuera.append(
|
|
46
|
+
{
|
|
47
|
+
"name": _campo(texto, "name") or os.path.basename(ruta_repo),
|
|
48
|
+
"description": _campo(texto, "description"),
|
|
49
|
+
"manifest": real,
|
|
50
|
+
"provider": "repo",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
for relativa in RAICES:
|
|
54
|
+
base = os.path.join(ruta_repo, relativa)
|
|
55
|
+
if not os.path.isdir(base):
|
|
56
|
+
continue
|
|
57
|
+
for nombre in sorted(os.listdir(base)):
|
|
58
|
+
manifiesto = os.path.join(base, nombre, "SKILL.md")
|
|
59
|
+
if not os.path.isfile(manifiesto):
|
|
60
|
+
continue
|
|
61
|
+
real = os.path.realpath(manifiesto)
|
|
62
|
+
if real in vistos:
|
|
63
|
+
continue
|
|
64
|
+
vistos.add(real)
|
|
65
|
+
try:
|
|
66
|
+
texto = open(manifiesto, encoding="utf-8").read()
|
|
67
|
+
except OSError:
|
|
68
|
+
continue
|
|
69
|
+
fuera.append(
|
|
70
|
+
{
|
|
71
|
+
"name": _campo(texto, "name") or nombre,
|
|
72
|
+
"description": _campo(texto, "description"),
|
|
73
|
+
"manifest": real,
|
|
74
|
+
"provider": relativa.split("/", 1)[0].lstrip(".") or "repo",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return fuera
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
#: The disk index, remembered for a moment. `find-repos.sh` keeps its own
|
|
81
|
+
#: day-long cache, but SPAWNING it and re-parsing the TSV is ~15 ms, and one
|
|
82
|
+
#: Hall request resolves it once per legacy agent — N+1 processes for one
|
|
83
|
+
#: answer. A short memory collapses that to one while staying young enough to
|
|
84
|
+
#: notice a repo cloned a minute ago.
|
|
85
|
+
_INDICE = {"cuando": 0.0, "valor": None}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _indice_repos(vida=90):
|
|
89
|
+
import time
|
|
90
|
+
|
|
91
|
+
if _INDICE["valor"] is not None and time.monotonic() - _INDICE["cuando"] < vida:
|
|
92
|
+
return _INDICE["valor"]
|
|
93
|
+
guion = os.path.join(os.path.dirname(__file__), "find-repos.sh")
|
|
94
|
+
try:
|
|
95
|
+
salida = subprocess.run([guion], capture_output=True, text=True, timeout=300).stdout
|
|
96
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
97
|
+
return {}
|
|
98
|
+
fuera = {}
|
|
99
|
+
for linea in salida.splitlines():
|
|
100
|
+
if "\t" in linea:
|
|
101
|
+
nombre, ruta = linea.split("\t", 1)
|
|
102
|
+
fuera[nombre.strip()] = ruta.strip()
|
|
103
|
+
_INDICE["cuando"], _INDICE["valor"] = time.monotonic(), fuera
|
|
104
|
+
return fuera
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def ficha_de_ciudad(datos):
|
|
108
|
+
owner = cities.lee_clave(datos, "owner")
|
|
109
|
+
preferida = os.path.join(datos, f"{owner}.md") if owner else ""
|
|
110
|
+
if preferida and os.path.isfile(preferida):
|
|
111
|
+
return preferida
|
|
112
|
+
try:
|
|
113
|
+
fichas = sorted(
|
|
114
|
+
os.path.join(datos, f)
|
|
115
|
+
for f in os.listdir(datos)
|
|
116
|
+
if f.endswith(".md") and card.lee(os.path.join(datos, f)).get("user")
|
|
117
|
+
)
|
|
118
|
+
except OSError:
|
|
119
|
+
fichas = []
|
|
120
|
+
return fichas[0] if fichas else ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def ruta_de(nombre, rutas=None):
|
|
124
|
+
"""One legacy repo's folder on this disk, or ''. The single mechanism both
|
|
125
|
+
full discovery and one-agent lookups share — resolving one path must never
|
|
126
|
+
cost a whole-city skill scan."""
|
|
127
|
+
rutas = _indice_repos() if rutas is None else rutas
|
|
128
|
+
code = os.path.expanduser(os.environ.get("CITY_CODE_DIR", "~/codigo"))
|
|
129
|
+
ruta = rutas.get(nombre, os.path.join(code, nombre))
|
|
130
|
+
return os.path.realpath(ruta) if os.path.isdir(ruta) else ""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def descubre_ciudad(datos, rutas=None):
|
|
134
|
+
"""``{agent: {path, role, skills}}`` for the city's support agents.
|
|
135
|
+
|
|
136
|
+
Both card shapes, through the one normaliser (`workspace.agentes`): a
|
|
137
|
+
legacy repo agent looks for its repo on disk exactly as before, and an
|
|
138
|
+
agents-first agent looks in its own workspace and in every mount it
|
|
139
|
+
declares. Before this, an agents-first city honestly full of skills showed
|
|
140
|
+
"Agents & skills 0" — the discovery only knew `repos:`.
|
|
141
|
+
"""
|
|
142
|
+
ficha = ficha_de_ciudad(datos)
|
|
143
|
+
if not ficha:
|
|
144
|
+
return {}
|
|
145
|
+
try:
|
|
146
|
+
texto = open(ficha, encoding="utf-8").read()
|
|
147
|
+
except OSError:
|
|
148
|
+
return {}
|
|
149
|
+
try:
|
|
150
|
+
agentes = workspace.agentes(texto, datos)
|
|
151
|
+
except ValueError:
|
|
152
|
+
# A malformed card (two names that slug to one identity) is the card
|
|
153
|
+
# tools' error to raise loudly; read-only discovery just has nothing
|
|
154
|
+
# safe to list for it.
|
|
155
|
+
return {}
|
|
156
|
+
if not agentes:
|
|
157
|
+
return {}
|
|
158
|
+
# The disk index is a find-repos scan: paid lazily, and only when a legacy
|
|
159
|
+
# agent actually needs it — a pure agents-first city never touches it.
|
|
160
|
+
fuera = {}
|
|
161
|
+
for a in agentes:
|
|
162
|
+
if a.legacy:
|
|
163
|
+
rutas = _indice_repos() if rutas is None else rutas
|
|
164
|
+
candidatos = [ruta_de(a.nombre, rutas)]
|
|
165
|
+
else:
|
|
166
|
+
candidatos = [a.workspace] + [os.path.expanduser(m) for m in a.mounts]
|
|
167
|
+
existentes = [c for c in candidatos if c and os.path.isdir(c)]
|
|
168
|
+
skills = []
|
|
169
|
+
vistos = set()
|
|
170
|
+
for c in existentes:
|
|
171
|
+
for s in descubre_repo(c):
|
|
172
|
+
if s["name"] in vistos:
|
|
173
|
+
continue
|
|
174
|
+
vistos.add(s["name"])
|
|
175
|
+
skills.append(s)
|
|
176
|
+
fuera[a.nombre] = {
|
|
177
|
+
"path": os.path.realpath(existentes[0]) if existentes else "",
|
|
178
|
+
"role": a.rol,
|
|
179
|
+
"skills": skills,
|
|
180
|
+
}
|
|
181
|
+
return fuera
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main():
|
|
185
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help", "help"):
|
|
186
|
+
print(
|
|
187
|
+
"usage: agents-city skills [city]\n\n"
|
|
188
|
+
"Read-only discovery of skills already installed in each repo."
|
|
189
|
+
)
|
|
190
|
+
return 0
|
|
191
|
+
usuario = cities.usuario_actual()
|
|
192
|
+
pedida = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
193
|
+
datos = cities.resuelve(pedida, usuario) if pedida else cities.actual(usuario)
|
|
194
|
+
if not datos:
|
|
195
|
+
print(f" No city called {pedida!r}.", file=sys.stderr)
|
|
196
|
+
return 1
|
|
197
|
+
capacidades = descubre_ciudad(datos)
|
|
198
|
+
print(f" Skills visible to {cities.direccion(usuario, datos)}")
|
|
199
|
+
if not capacidades:
|
|
200
|
+
print(" No repo agents in this city.")
|
|
201
|
+
for repo, info in capacidades.items():
|
|
202
|
+
nombres = ", ".join(s["name"] for s in info["skills"]) or "none discovered"
|
|
203
|
+
estado = info["path"] or "repo not found on this machine"
|
|
204
|
+
print(f" {repo} [{info['role']}]: {nombres}\n {estado}")
|
|
205
|
+
print("\n Read-only discovery: Agents City installed and changed nothing.")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
sys.exit(main())
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The one reader and writer of a person's card.
|
|
3
|
+
|
|
4
|
+
A card is markdown with frontmatter. It is the only thing that says who somebody
|
|
5
|
+
is, what they answer for and what they are aiming at, and it is read by the tmux
|
|
6
|
+
session, the seat, the wizard, the repo picker, the seeder and every round.
|
|
7
|
+
|
|
8
|
+
There were five ways to read it and three to write it, in one repo:
|
|
9
|
+
|
|
10
|
+
* `read-card.py`, a CLI, one field at a time, by subprocess
|
|
11
|
+
* a repo picker with its own frontmatter parse, for `repos` only — which nothing
|
|
12
|
+
ever called, and which is gone
|
|
13
|
+
* `bin/seat`, all three of the above plus two loose regexes of its own
|
|
14
|
+
* `bin/setup.py` and `bin/seat` each writing the whole card their own way
|
|
15
|
+
|
|
16
|
+
That is how the seat window ended up reading a field called `agente` while every
|
|
17
|
+
card in the repo carries `agent`, and how a card written by one door came out
|
|
18
|
+
different from the same card written by the other. One file, so there is one answer.
|
|
19
|
+
|
|
20
|
+
Here, and not next to the wizard, for the same reason as `parcels.py`: this
|
|
21
|
+
directory is what gets installed on everybody's machine.
|
|
22
|
+
"""
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
# The frontmatter fields, in the order they are written. Order is not cosmetic — a
|
|
26
|
+
# card is read by people, and a stable order means a diff shows what changed rather
|
|
27
|
+
# than that something moved.
|
|
28
|
+
CAMPOS = ('user', 'name', 'role', 'agent', 'repos', 'goals_defined')
|
|
29
|
+
ROLE_ID = re.compile(r'^[a-z0-9][a-z0-9-]{0,63}$')
|
|
30
|
+
#: What `ventana()` can produce, as a shape callers can check without rebuilding
|
|
31
|
+
#: it. A window slug is longer than a role id (80 vs 64), and every door that
|
|
32
|
+
#: resolves an agent by slug has to agree on that or a long-named agent renders
|
|
33
|
+
#: everywhere and answers nowhere.
|
|
34
|
+
VENTANA_ID = re.compile(r'^[a-z0-9][a-z0-9-]{0,79}$')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ventana_valida(valor):
|
|
38
|
+
"""Whether this is a window slug the rest of the product will accept."""
|
|
39
|
+
return bool(VENTANA_ID.fullmatch(str(valor or '')))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def frontmatter(texto):
|
|
43
|
+
"""The frontmatter block of a card, or '' if it has none."""
|
|
44
|
+
if not texto.startswith('---'):
|
|
45
|
+
return ''
|
|
46
|
+
partes = texto.split('---')
|
|
47
|
+
return partes[1] if len(partes) > 2 else ''
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def lista(valor):
|
|
51
|
+
"""`[a, "b", 'c']` -> ['a', 'b', 'c']. Quotes are optional in these files."""
|
|
52
|
+
return [x.strip().strip('"\'') for x in valor.strip().strip('[]').split(',') if x.strip()]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def campo(texto, clave, defecto=''):
|
|
56
|
+
"""One frontmatter field, by name.
|
|
57
|
+
|
|
58
|
+
`agent` also answers to `agente`. Every card in this repo says `agent`, but a
|
|
59
|
+
hand-written one may not, and the version of this that only accepted `agente`
|
|
60
|
+
silently returned nothing for all twelve demo cards.
|
|
61
|
+
"""
|
|
62
|
+
fm = frontmatter(texto)
|
|
63
|
+
if not fm:
|
|
64
|
+
return defecto
|
|
65
|
+
for nombre in ((clave, 'agente') if clave == 'agent' else (clave,)):
|
|
66
|
+
m = re.search(rf'^{re.escape(nombre)}:[ \t]*(.*)$', fm, re.M)
|
|
67
|
+
if m and m.group(1).strip():
|
|
68
|
+
return m.group(1).strip()
|
|
69
|
+
return defecto
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lee(ruta):
|
|
73
|
+
"""A whole card as a dict, including its goal. Missing file reads as empty."""
|
|
74
|
+
try:
|
|
75
|
+
with open(ruta, encoding='utf-8') as f:
|
|
76
|
+
texto = f.read()
|
|
77
|
+
except (OSError, IsADirectoryError):
|
|
78
|
+
return {}
|
|
79
|
+
usuario = campo(texto, 'user')
|
|
80
|
+
repos = lista(campo(texto, 'repos'))
|
|
81
|
+
return {
|
|
82
|
+
'user': usuario,
|
|
83
|
+
'name': campo(texto, 'name', usuario),
|
|
84
|
+
'role': campo(texto, 'role', 'dev'),
|
|
85
|
+
'agent': campo(texto, 'agent'),
|
|
86
|
+
'repos': repos,
|
|
87
|
+
'repo_roles': roles_repos(texto, repos),
|
|
88
|
+
'goals_defined': campo(texto, 'goals_defined') == 'true',
|
|
89
|
+
'objetivo': objetivo(texto, usuario),
|
|
90
|
+
'texto': texto,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def rol_seguro(valor, defecto='blank'):
|
|
95
|
+
"""One inert role id suitable for a card key, environment and prompt.
|
|
96
|
+
|
|
97
|
+
Role text is configuration, never a shell fragment. A hand-edited malformed
|
|
98
|
+
value therefore degrades to the explicit blank role rather than crossing the
|
|
99
|
+
runtime boundary verbatim.
|
|
100
|
+
"""
|
|
101
|
+
valor = str(valor or '').strip().lower()
|
|
102
|
+
return valor if ROLE_ID.fullmatch(valor) else defecto
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def roles_repos(texto, repos=None):
|
|
106
|
+
"""``{original repo name: operating role}`` from dynamic ``role.<actor>`` keys.
|
|
107
|
+
|
|
108
|
+
Old cards have no such keys. Returning ``blank`` is intentional: it preserves
|
|
109
|
+
the old behaviour without pretending that Agents City knew why that repo was
|
|
110
|
+
present. The key suffix uses the same canonical actor/window slug as tmux and
|
|
111
|
+
the bus.
|
|
112
|
+
"""
|
|
113
|
+
repos = list(repos if repos is not None else lista(campo(texto, 'repos')))
|
|
114
|
+
return {repo: rol_seguro(campo(texto, f'role.{ventana(repo)}')) for repo in repos}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def normaliza_roles_repos(repos, asignados=None):
|
|
118
|
+
"""Validate one explicit role per selected repo and reject actor collisions."""
|
|
119
|
+
asignados = dict(asignados or {})
|
|
120
|
+
fuera, actores = {}, {}
|
|
121
|
+
limpios = [str(r).strip() for r in repos if str(r).strip()]
|
|
122
|
+
for repo in sorted(dict.fromkeys(limpios)):
|
|
123
|
+
actor = ventana(repo)
|
|
124
|
+
if actor in actores and actores[actor] != repo:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"repo names {actores[actor]!r} and {repo!r} collide as agent {actor!r}"
|
|
127
|
+
)
|
|
128
|
+
actores[actor] = repo
|
|
129
|
+
crudo = asignados.get(repo, asignados.get(actor, 'blank'))
|
|
130
|
+
rol = str(crudo or 'blank').strip().lower()
|
|
131
|
+
if not ROLE_ID.fullmatch(rol):
|
|
132
|
+
raise ValueError(f"invalid role {crudo!r} for repo {repo!r}")
|
|
133
|
+
fuera[repo] = rol
|
|
134
|
+
return fuera
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def linea_rol(rol, oficio=''):
|
|
138
|
+
"""The one human-readable role line shared by new cards and surgery."""
|
|
139
|
+
if rol == 'blank':
|
|
140
|
+
return 'Role: **blank** — no predefined seat responsibility or role knowledge.'
|
|
141
|
+
nombre = (oficio or rol.replace('-', ' ')).lower()
|
|
142
|
+
return (
|
|
143
|
+
f'Role: **{rol}** — in the city, the **{nombre}**. '
|
|
144
|
+
f'Role knowledge, when present: `roles/{rol}.md`.'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def objetivo(texto, usuario=''):
|
|
149
|
+
"""The first goal off a card, in the shape both writers emit."""
|
|
150
|
+
if '### O1 —' not in texto:
|
|
151
|
+
return None
|
|
152
|
+
m = re.search(r'^### O1 — (.+)$', texto, re.M)
|
|
153
|
+
titulo = m.group(1).strip() if m else ''
|
|
154
|
+
if not titulo:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def linea(etiqueta, defecto=''):
|
|
158
|
+
m = re.search(rf'^- \*\*{etiqueta}\*\*:\s*`?(.+?)`?\.?$', texto, re.M)
|
|
159
|
+
return m.group(1).strip() if m else defecto
|
|
160
|
+
|
|
161
|
+
medida = linea('Measure')
|
|
162
|
+
# A measure is one of two honest things: a command that returns a number, or a
|
|
163
|
+
# person's judgement written down — "the architect reads the AGENTS.md files on
|
|
164
|
+
# Fridays". The second is not a lesser goal; plenty of quality is prose. It
|
|
165
|
+
# round-trips as `manual`, never as a command a round might try to execute.
|
|
166
|
+
manual = ''
|
|
167
|
+
if medida.startswith('manual —'):
|
|
168
|
+
manual = medida[len('manual —'):].strip()
|
|
169
|
+
if manual == 'who looks at what, and how often':
|
|
170
|
+
manual = ''
|
|
171
|
+
medida = ''
|
|
172
|
+
return {'user': usuario, 'title': titulo,
|
|
173
|
+
'signal': linea('How it is measured'),
|
|
174
|
+
'command': medida, 'manual': manual,
|
|
175
|
+
'baseline': linea('Baseline'), 'target': linea('Target'),
|
|
176
|
+
'by': linea('By when', 'this quarter')}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def bloque_objetivo(obj):
|
|
180
|
+
"""The goal, as the lines that go on a card. One emitter, so a round reads the
|
|
181
|
+
same shape whichever door wrote it."""
|
|
182
|
+
if not obj:
|
|
183
|
+
return ['> **Pending.** A round works without them, but it cannot contrast '
|
|
184
|
+
'the work against anything.', '',
|
|
185
|
+
'> To set one: `./bin/seat --goal`, or `/city:goals`.', '']
|
|
186
|
+
medida = obj.get('command') or ('manual — ' + (
|
|
187
|
+
obj.get('manual') or 'who looks at what, and how often'))
|
|
188
|
+
return [f"### O1 — {obj['title']}",
|
|
189
|
+
f"- **What**: {obj['title']}.",
|
|
190
|
+
f"- **How it is measured**: {obj.get('signal') or 'to be defined'}.",
|
|
191
|
+
f"- **Measure**: `{medida}`",
|
|
192
|
+
f"- **Baseline**: {obj.get('baseline') or 'not measured yet'}",
|
|
193
|
+
f"- **Target**: {obj.get('target') or 'to be defined'}",
|
|
194
|
+
f"- **By when**: {obj.get('by') or 'this quarter'}",
|
|
195
|
+
'- **State**: in progress', '']
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _cambia_seccion(texto, titulo, lineas_nuevas):
|
|
199
|
+
"""Replace one `##` section's body, touching nothing else on the card."""
|
|
200
|
+
patron = re.compile(rf'(^## {re.escape(titulo)}\n)(.*?)(?=^## |\Z)', re.M | re.S)
|
|
201
|
+
if not patron.search(texto):
|
|
202
|
+
return None
|
|
203
|
+
cuerpo = '\n' + '\n'.join(lineas_nuevas).rstrip('\n') + '\n\n'
|
|
204
|
+
return patron.sub(lambda m: m.group(1) + cuerpo, texto, count=1)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def cambia_objetivo(ruta, obj):
|
|
208
|
+
"""Rewrite only the goals section — and the `goals_defined` flag with it.
|
|
209
|
+
|
|
210
|
+
Surgical on purpose: the card also carries the round history, and the first
|
|
211
|
+
version of "change your goal" rewrote the whole card from a template, wiping
|
|
212
|
+
every round summary anyone had left there. Changing what you aim at must not
|
|
213
|
+
erase what already happened.
|
|
214
|
+
|
|
215
|
+
`obj` of None clears the goal back to pending. Returns False when the card has
|
|
216
|
+
no goals section to replace.
|
|
217
|
+
"""
|
|
218
|
+
with open(ruta, encoding='utf-8') as f:
|
|
219
|
+
texto = f.read()
|
|
220
|
+
nuevo = _cambia_seccion(texto, 'Current goals', bloque_objetivo(obj))
|
|
221
|
+
if nuevo is None:
|
|
222
|
+
return False
|
|
223
|
+
bandera = 'true' if obj else 'false'
|
|
224
|
+
nuevo = re.sub(r'^goals_defined:.*$', f'goals_defined: {bandera}', nuevo,
|
|
225
|
+
count=1, flags=re.M)
|
|
226
|
+
with open(ruta, 'w', encoding='utf-8') as f:
|
|
227
|
+
f.write(nuevo)
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cambia_rol(ruta, rol, agente, oficio=''):
|
|
232
|
+
"""Rewrite the role — frontmatter, agent name, and the one body line that says
|
|
233
|
+
it out loud, so the card does not end up claiming two different roles."""
|
|
234
|
+
with open(ruta, encoding='utf-8') as f:
|
|
235
|
+
texto = f.read()
|
|
236
|
+
nuevo, n = re.subn(r'^role:.*$', f'role: {rol}', texto, count=1, flags=re.M)
|
|
237
|
+
if n == 0:
|
|
238
|
+
return False
|
|
239
|
+
nuevo = re.sub(r'^agent:.*$', f'agent: {agente}', nuevo, count=1, flags=re.M)
|
|
240
|
+
if oficio or rol == 'blank':
|
|
241
|
+
nuevo = re.sub(r'^Role:.*$', linea_rol(rol, oficio), nuevo, count=1, flags=re.M)
|
|
242
|
+
with open(ruta, 'w', encoding='utf-8') as f:
|
|
243
|
+
f.write(nuevo)
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def ventana(repo):
|
|
248
|
+
"""The tmux window a repo becomes — which doubles as the engine-key suffix
|
|
249
|
+
on the card (`model.<window>`, `runs.<window>`), so the charset stays
|
|
250
|
+
key-safe and valid as a bus actor. A worktree `MiApp@feature/X` becomes
|
|
251
|
+
`miapp-feature-x`. Shell callers delegate here through read-card.py."""
|
|
252
|
+
limpio = re.sub(r'[^a-z0-9-]+', '-', str(repo).lower()).strip('-')
|
|
253
|
+
return (limpio or 'repo')[:80]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def pon_campo(ruta, clave, valor):
|
|
257
|
+
"""Set one frontmatter field, surgically: replace it if it exists, add it just
|
|
258
|
+
before the closing --- if not, remove it when the value is empty.
|
|
259
|
+
|
|
260
|
+
This is how a card carries the engine its agents start with — `model:` and
|
|
261
|
+
`effort:` as the person's default, `model.<window>:` / `effort.<window>:` to
|
|
262
|
+
tune one window ("the docs repo runs on haiku, the monorepo on opus"). The
|
|
263
|
+
session script resolves specific-then-default, and an absent key means
|
|
264
|
+
"whatever this person's Claude defaults to", which is the right silence.
|
|
265
|
+
"""
|
|
266
|
+
if not re.match(r'^[a-z][a-z0-9._-]*$', clave):
|
|
267
|
+
return False
|
|
268
|
+
with open(ruta, encoding='utf-8') as f:
|
|
269
|
+
texto = f.read()
|
|
270
|
+
if not texto.startswith('---'):
|
|
271
|
+
return False
|
|
272
|
+
patron = re.compile(rf'^{re.escape(clave)}:.*\n', re.M)
|
|
273
|
+
fin = texto.index('---', 3)
|
|
274
|
+
if valor == '' or valor is None:
|
|
275
|
+
nuevo = patron.sub('', texto[:fin], count=1) + texto[fin:]
|
|
276
|
+
elif patron.search(texto[:fin]):
|
|
277
|
+
nuevo = patron.sub(f'{clave}: {valor}\n', texto[:fin], count=1) + texto[fin:]
|
|
278
|
+
else:
|
|
279
|
+
nuevo = texto[:fin] + f'{clave}: {valor}\n' + texto[fin:]
|
|
280
|
+
with open(ruta, 'w', encoding='utf-8') as f:
|
|
281
|
+
f.write(nuevo)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cambia_repos(ruta, elegidos):
|
|
286
|
+
"""Rewrite only the `repos` field of an existing card, leaving the rest alone.
|
|
287
|
+
|
|
288
|
+
Used by the repo picker, which has no business rewriting somebody's goals to
|
|
289
|
+
change which folders they answer for.
|
|
290
|
+
"""
|
|
291
|
+
with open(ruta, encoding='utf-8') as f:
|
|
292
|
+
texto = f.read()
|
|
293
|
+
linea = 'repos: [' + ', '.join(sorted(elegidos)) + ']'
|
|
294
|
+
nuevo, n = re.subn(r'^repos:.*$', linea, texto, count=1, flags=re.M)
|
|
295
|
+
if n == 0:
|
|
296
|
+
return False
|
|
297
|
+
with open(ruta, 'w', encoding='utf-8') as f:
|
|
298
|
+
f.write(nuevo)
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cambia_roles_repos(ruta, elegidos, asignados):
|
|
303
|
+
"""Atomically persist one explicit operating role per selected repo.
|
|
304
|
+
|
|
305
|
+
Stale ``role.<actor>`` keys are removed when a repo leaves the city. Other
|
|
306
|
+
per-window settings (model, effort and runtime) remain untouched.
|
|
307
|
+
"""
|
|
308
|
+
roles = normaliza_roles_repos(elegidos, asignados)
|
|
309
|
+
with open(ruta, encoding='utf-8') as f:
|
|
310
|
+
texto = f.read()
|
|
311
|
+
if not texto.startswith('---'):
|
|
312
|
+
return False
|
|
313
|
+
fin = texto.find('---', 3)
|
|
314
|
+
if fin < 0:
|
|
315
|
+
return False
|
|
316
|
+
cabeza = re.sub(r'^role\.[a-z0-9._-]+:.*\n', '', texto[:fin], flags=re.M)
|
|
317
|
+
lineas = ''.join(f'role.{ventana(repo)}: {rol}\n' for repo, rol in roles.items())
|
|
318
|
+
if lineas:
|
|
319
|
+
cabeza += lineas
|
|
320
|
+
with open(ruta, 'w', encoding='utf-8') as f:
|
|
321
|
+
f.write(cabeza + texto[fin:])
|
|
322
|
+
return True
|