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,561 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Agents come first. A repo is just one thing an agent can mount.
|
|
3
|
+
|
|
4
|
+
The old model was "a repo is an agent": every window was a git checkout, and a
|
|
5
|
+
person with knowledge in a folder of documents — no git at all — had nowhere to
|
|
6
|
+
live. This module inverts it. The primary unit is the **agent**; each agent owns
|
|
7
|
+
a **workspace folder**, and inside it are **mounts**: symlinks to wherever the
|
|
8
|
+
real work lives — a git repo, a linked worktree, or a plain folder of documents.
|
|
9
|
+
|
|
10
|
+
The inversion is total but backward-compatible, because the old model is a
|
|
11
|
+
special case of the new one:
|
|
12
|
+
|
|
13
|
+
repos: [nova, store-service] # legacy card
|
|
14
|
+
≡
|
|
15
|
+
agents: [nova, store-service] # each agent's single mount is that repo
|
|
16
|
+
|
|
17
|
+
So `agentes()` normalises BOTH card shapes into one list of `Agente`, and every
|
|
18
|
+
consumer (the launcher, the cage, the map) reads that list without caring which
|
|
19
|
+
shape the card was written in. New agent-first cards add `mounts.<agent>` and a
|
|
20
|
+
`kind.<agent>`; legacy cards keep working untouched.
|
|
21
|
+
|
|
22
|
+
The workspace is also exactly what the cage seals to: `mount_targets()` resolves
|
|
23
|
+
each symlink to its real path so the launcher can make those — and only those —
|
|
24
|
+
writable, following the symlink to its destination like the kernel does.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
import card
|
|
32
|
+
import rutas
|
|
33
|
+
|
|
34
|
+
#: Where an agent's own workspace folder lives inside the city.
|
|
35
|
+
AGENTS_DIR = 'agents'
|
|
36
|
+
#: The subfolder inside a workspace that holds the symlinks to real work.
|
|
37
|
+
MOUNTS_DIR = 'mounts'
|
|
38
|
+
|
|
39
|
+
#: What kind of work an agent does — this is what makes the map polymorphic
|
|
40
|
+
#: instead of assuming everyone ships pull requests.
|
|
41
|
+
CLASES = ('code', 'knowledge', 'coordinator')
|
|
42
|
+
CLASE_DEFECTO = 'code'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#: The instruction file each engine actually reads. Engine knowledge, not web
|
|
46
|
+
#: routing: the Hall's editor and any future CLI view answer from this one map.
|
|
47
|
+
INSTRUCCIONES = {'CLAUDE.md': 'claude', 'AGENTS.md': 'codex/opencode/kimi'}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _slug(nombre):
|
|
51
|
+
"""The agent's stable bus/tmux/filesystem identity — reuses card.ventana so
|
|
52
|
+
a worktree `App@feature` slugs identically everywhere."""
|
|
53
|
+
return card.ventana(nombre)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Agente:
|
|
57
|
+
"""One agent: identity, role, runtime, its workspace folder, and its mounts.
|
|
58
|
+
|
|
59
|
+
A plain object rather than a closure so it copies only the fields it needs
|
|
60
|
+
and never captures the whole card text.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
__slots__ = ('nombre', 'slug', 'rol', 'runtime', 'clase', 'workspace', 'mounts', 'legacy')
|
|
64
|
+
|
|
65
|
+
def __init__(self, nombre, slug, rol, runtime, clase, workspace, mounts, legacy):
|
|
66
|
+
self.nombre = nombre
|
|
67
|
+
self.slug = slug
|
|
68
|
+
self.rol = rol
|
|
69
|
+
self.runtime = runtime
|
|
70
|
+
self.clase = clase
|
|
71
|
+
self.workspace = workspace # the agent's cwd
|
|
72
|
+
self.mounts = mounts # declared mount sources, as written (unresolved)
|
|
73
|
+
self.legacy = legacy # True when derived from a bare `repos:` entry
|
|
74
|
+
|
|
75
|
+
def como_dict(self):
|
|
76
|
+
return {'name': self.nombre, 'slug': self.slug, 'role': self.rol,
|
|
77
|
+
'runtime': self.runtime, 'kind': self.clase, 'workspace': self.workspace,
|
|
78
|
+
'mounts': list(self.mounts), 'legacy': self.legacy}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _clase_valida(valor):
|
|
82
|
+
v = str(valor or '').strip().lower()
|
|
83
|
+
return v if v in CLASES else CLASE_DEFECTO
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def workspace_de(data, slug):
|
|
87
|
+
"""The workspace folder for one agent inside the city data dir."""
|
|
88
|
+
return os.path.join(data, AGENTS_DIR, slug)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def agentes(texto_ficha, data, resolver_legacy=None):
|
|
92
|
+
"""Normalise a card into a list of `Agente`, from either card shape.
|
|
93
|
+
|
|
94
|
+
`resolver_legacy(repo) -> path` turns a legacy repo name into its on-disk
|
|
95
|
+
path (find-repos). When omitted, a legacy repo's workspace is left as the
|
|
96
|
+
bare name — callers that only need identity (the map) do not pay for disk
|
|
97
|
+
lookups.
|
|
98
|
+
"""
|
|
99
|
+
explicitos = card.lista(card.campo(texto_ficha, 'agents'))
|
|
100
|
+
if explicitos:
|
|
101
|
+
return _sin_colisiones([_agente_nuevo(n, texto_ficha, data) for n in explicitos])
|
|
102
|
+
repos = card.lista(card.campo(texto_ficha, 'repos'))
|
|
103
|
+
return _sin_colisiones([_agente_legacy(r, texto_ficha, resolver_legacy) for r in repos])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _sin_colisiones(lista):
|
|
107
|
+
"""Reject two agents that slug to one identity, like the legacy repo path.
|
|
108
|
+
|
|
109
|
+
`[store-service, store_service]` or `[Writer, writer]` both collapse to a
|
|
110
|
+
single slug — one workspace, one tmux window, one bus actor — so two agents
|
|
111
|
+
would silently overwrite each other. Refuse it up front instead.
|
|
112
|
+
"""
|
|
113
|
+
vistos = {}
|
|
114
|
+
for a in lista:
|
|
115
|
+
if a.slug in vistos:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f'agents "{vistos[a.slug]}" and "{a.nombre}" both resolve to the '
|
|
118
|
+
f'same identity "{a.slug}"; give them distinct names')
|
|
119
|
+
vistos[a.slug] = a.nombre
|
|
120
|
+
return lista
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _agente_nuevo(nombre, texto, data):
|
|
124
|
+
slug = _slug(nombre)
|
|
125
|
+
rol = card.rol_seguro(card.campo(texto, f'role.{slug}'))
|
|
126
|
+
runtime = card.campo(texto, f'runs.{slug}') or 'claude'
|
|
127
|
+
clase = _clase_valida(card.campo(texto, f'kind.{slug}'))
|
|
128
|
+
mounts = card.lista(card.campo(texto, f'mounts.{slug}'))
|
|
129
|
+
return Agente(nombre, slug, rol, runtime, clase, workspace_de(data, slug), mounts, legacy=False)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _agente_legacy(repo, texto, resolver_legacy):
|
|
133
|
+
slug = _slug(repo)
|
|
134
|
+
rol = card.rol_seguro(card.campo(texto, f'role.{slug}'))
|
|
135
|
+
runtime = card.campo(texto, f'runs.{slug}') or 'claude'
|
|
136
|
+
# A legacy repo agent's workspace IS the repo, and its single mount is that
|
|
137
|
+
# repo — so the cage and cwd behave exactly as they did before this module.
|
|
138
|
+
destino = resolver_legacy(repo) if resolver_legacy else repo
|
|
139
|
+
return Agente(repo, slug, rol, runtime, 'code', destino, [destino], legacy=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── the workspace on disk ────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def crea_workspace(data, slug):
|
|
146
|
+
"""Create an agent's workspace folder and its mounts dir. Idempotent."""
|
|
147
|
+
ws = workspace_de(data, slug)
|
|
148
|
+
os.makedirs(os.path.join(ws, MOUNTS_DIR), mode=0o700, exist_ok=True)
|
|
149
|
+
return ws
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _etiqueta_implicita(carpeta, base, destino):
|
|
153
|
+
"""A collision-free label from an auto-derived basename.
|
|
154
|
+
|
|
155
|
+
The slug of `base`, or that slug plus a short target hash when the plain
|
|
156
|
+
label is already taken by a DIFFERENT target. This is what stops two sources
|
|
157
|
+
sharing a basename (a card that mounts `~/work/api` and `~/other/api`) from
|
|
158
|
+
silently collapsing into one — the second no longer overwrites the first.
|
|
159
|
+
An explicit `--name` skips this and repoints deliberately.
|
|
160
|
+
"""
|
|
161
|
+
raiz = _slug(base) or 'mount'
|
|
162
|
+
directo = os.path.join(carpeta, raiz)
|
|
163
|
+
if not os.path.islink(directo) or rutas.canonicaliza(directo) == destino:
|
|
164
|
+
return raiz # free, or an idempotent re-mount of the same target
|
|
165
|
+
sufijo = hashlib.sha256(destino.encode()).hexdigest()[:6]
|
|
166
|
+
return f'{raiz}-{sufijo}'
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def monta(data, slug, origen, nombre=None):
|
|
170
|
+
"""Symlink a real disk location into the agent's workspace as a mount.
|
|
171
|
+
|
|
172
|
+
The target is canonicalised and must exist. An explicit `nombre` is the
|
|
173
|
+
label verbatim (slugged) and repoints on reuse — the caller chose it. An
|
|
174
|
+
auto-derived basename is disambiguated on collision so two different sources
|
|
175
|
+
that share a basename do not overwrite each other. Returns the link path.
|
|
176
|
+
"""
|
|
177
|
+
destino = rutas.canonicaliza(origen)
|
|
178
|
+
if not os.path.exists(destino):
|
|
179
|
+
raise ValueError(f'nothing to mount at {destino}')
|
|
180
|
+
# The mount pipeline (sync-all output, the cage --mounts list) is
|
|
181
|
+
# colon-separated, so a colon in the real path would split into bogus
|
|
182
|
+
# roots. Refuse it rather than hand the cage a wrong ancestor.
|
|
183
|
+
if ':' in destino:
|
|
184
|
+
raise ValueError(f'mount path contains a colon, which is not supported: {destino}')
|
|
185
|
+
ws = crea_workspace(data, slug)
|
|
186
|
+
carpeta = os.path.join(ws, MOUNTS_DIR)
|
|
187
|
+
if nombre:
|
|
188
|
+
etiqueta = _slug(nombre) or 'mount'
|
|
189
|
+
else:
|
|
190
|
+
base = os.path.basename(destino.rstrip(os.sep)) or 'mount'
|
|
191
|
+
etiqueta = _etiqueta_implicita(carpeta, base, destino)
|
|
192
|
+
enlace = os.path.join(carpeta, etiqueta)
|
|
193
|
+
tmp = f'{enlace}.tmp-link'
|
|
194
|
+
if os.path.islink(tmp) or os.path.exists(tmp):
|
|
195
|
+
os.unlink(tmp)
|
|
196
|
+
os.symlink(destino, tmp)
|
|
197
|
+
os.replace(tmp, enlace) # atomic repoint
|
|
198
|
+
return enlace
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def desmonta(data, slug, etiqueta):
|
|
202
|
+
"""Remove one mount symlink. Returns True if one was removed."""
|
|
203
|
+
enlace = os.path.join(workspace_de(data, slug), MOUNTS_DIR, _slug(etiqueta))
|
|
204
|
+
if os.path.islink(enlace):
|
|
205
|
+
os.unlink(enlace)
|
|
206
|
+
return True
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def mounts_en_disco(data, slug):
|
|
211
|
+
"""The mounts actually present in the workspace: [(label, real target)]."""
|
|
212
|
+
carpeta = os.path.join(workspace_de(data, slug), MOUNTS_DIR)
|
|
213
|
+
try:
|
|
214
|
+
etiquetas = sorted(os.listdir(carpeta))
|
|
215
|
+
except OSError:
|
|
216
|
+
return []
|
|
217
|
+
salida = []
|
|
218
|
+
for e in etiquetas:
|
|
219
|
+
enlace = os.path.join(carpeta, e)
|
|
220
|
+
if os.path.islink(enlace):
|
|
221
|
+
salida.append((e, rutas.canonicaliza(enlace)))
|
|
222
|
+
return salida
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def mount_targets(agente, data=None):
|
|
226
|
+
"""The real, canonical paths this agent may write to — for the cage.
|
|
227
|
+
|
|
228
|
+
Prefers the mounts materialised on disk (following each symlink to its
|
|
229
|
+
destination, as the kernel does); falls back to the card's declared sources
|
|
230
|
+
when nothing is materialised yet. A legacy agent yields exactly its repo.
|
|
231
|
+
"""
|
|
232
|
+
if not agente.legacy and data is not None:
|
|
233
|
+
en_disco = [t for _, t in mounts_en_disco(data, agente.slug)]
|
|
234
|
+
if en_disco:
|
|
235
|
+
return en_disco
|
|
236
|
+
return [rutas.canonicaliza(m) for m in agente.mounts if m]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def sincroniza(agente, data):
|
|
240
|
+
"""Make the agent's workspace match its card: create it, mount every
|
|
241
|
+
declared source that exists. Returns the resolved mount targets.
|
|
242
|
+
|
|
243
|
+
A declared source that does not exist is skipped with a warning rather than
|
|
244
|
+
aborting the launch — a missing folder should degrade one mount, not the
|
|
245
|
+
whole city. Idempotent: re-running repoints existing links.
|
|
246
|
+
"""
|
|
247
|
+
crea_workspace(data, agente.slug)
|
|
248
|
+
for origen in agente.mounts:
|
|
249
|
+
# `monta` already canonicalises and raises on a missing path; let its
|
|
250
|
+
# error drive the skip rather than pre-checking existence a second time.
|
|
251
|
+
try:
|
|
252
|
+
monta(data, agente.slug, os.path.expanduser(origen))
|
|
253
|
+
except (OSError, ValueError) as e:
|
|
254
|
+
sys.stderr.write(f'workspace: skipping mount {origen}: {e}\n')
|
|
255
|
+
# Only what actually materialised on disk — never the declared-source
|
|
256
|
+
# fallback, which could hand the cage a path that was never mounted.
|
|
257
|
+
return [t for _, t in mounts_en_disco(data, agente.slug)]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def como_ficha(agente, motor=None):
|
|
261
|
+
"""One agent in the shape the card writers speak: a plain dict.
|
|
262
|
+
|
|
263
|
+
`Agente` is what readers get; this is what writers take. Keeping the two
|
|
264
|
+
shapes converted in one place is what stops the wizard and the Hall from
|
|
265
|
+
each inventing their own idea of what an agent looks like on a card.
|
|
266
|
+
"""
|
|
267
|
+
return {
|
|
268
|
+
'nombre': agente.nombre,
|
|
269
|
+
'slug': agente.slug,
|
|
270
|
+
'clase': agente.clase,
|
|
271
|
+
'rol': agente.rol,
|
|
272
|
+
'mounts': list(agente.mounts),
|
|
273
|
+
'motor': dict(motor or {}),
|
|
274
|
+
'skills': [],
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def claves_de_roster(roster):
|
|
279
|
+
"""Every card key a roster implies: identity, kind, role, mounts, engine.
|
|
280
|
+
|
|
281
|
+
The single owner of "what does a city's roster look like on a card". The
|
|
282
|
+
wizard writes it after asking seven questions and the Hall writes it after
|
|
283
|
+
one POST; both land here, so neither can drift into writing `kinds.` or
|
|
284
|
+
forgetting `mounts.` — the class of bug that only appears at the seam.
|
|
285
|
+
An empty mount list REMOVES the key rather than writing `[]`, because an
|
|
286
|
+
empty value is how `card.pon_campo` says "back to nothing".
|
|
287
|
+
"""
|
|
288
|
+
claves = {'agents': '[' + ', '.join(a['nombre'] for a in roster) + ']'}
|
|
289
|
+
for a in roster:
|
|
290
|
+
claves[f'kind.{a["slug"]}'] = a['clase']
|
|
291
|
+
claves[f'role.{a["slug"]}'] = a['rol']
|
|
292
|
+
claves[f'mounts.{a["slug"]}'] = (
|
|
293
|
+
'[' + ', '.join(a['mounts']) + ']' if a['mounts'] else ''
|
|
294
|
+
)
|
|
295
|
+
for clave, valor in (a.get('motor') or {}).items():
|
|
296
|
+
claves[clave] = valor
|
|
297
|
+
return claves
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ── skills, the one deliberate write ─────────────────────────────────────────
|
|
301
|
+
#
|
|
302
|
+
# Recognition of skills is read-only everywhere else in this codebase. This is
|
|
303
|
+
# the single exception: the owner explicitly hands over a skill and asks for it
|
|
304
|
+
# to live in one agent's home. Both doors — the wizard's question and the Hall's
|
|
305
|
+
# upload — come through here, so the containment rules are written once. A skill
|
|
306
|
+
# is the Claude runtime's format; other engines ignore the folder entirely.
|
|
307
|
+
|
|
308
|
+
#: One skill's extraction budget. Whatever bounds the upload (a byte cap, a file
|
|
309
|
+
#: on disk), only these bound what a hostile deflate ratio inflates it to.
|
|
310
|
+
MAX_ENTRADAS_ZIP = 2048
|
|
311
|
+
MAX_EXTRAIDO = 64 * 1024 * 1024
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def raiz_de_skills(hogar):
|
|
315
|
+
"""The home's own `.claude/skills`, verified to still be inside the home
|
|
316
|
+
after resolution, or ''.
|
|
317
|
+
|
|
318
|
+
A repo can commit `.claude/skills` (or `.claude` itself) as a symlink to
|
|
319
|
+
anywhere the user can write — the global skills folder included — and both
|
|
320
|
+
an install and a removal would follow it straight out of the agent's home.
|
|
321
|
+
"""
|
|
322
|
+
real = os.path.realpath(os.path.join(hogar, '.claude', 'skills'))
|
|
323
|
+
if real != os.path.join(os.path.realpath(hogar), '.claude', 'skills'):
|
|
324
|
+
return ''
|
|
325
|
+
return real
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def zip_inseguro(entradas):
|
|
329
|
+
"""The shapes a hostile zip uses, refused by name before a byte lands."""
|
|
330
|
+
if len(entradas) > MAX_ENTRADAS_ZIP:
|
|
331
|
+
return 'the zip holds too many files for one skill'
|
|
332
|
+
if sum(info.file_size for info in entradas) > MAX_EXTRAIDO:
|
|
333
|
+
return 'the zip inflates too large for one skill'
|
|
334
|
+
for info in entradas:
|
|
335
|
+
nombre = info.filename
|
|
336
|
+
if nombre.startswith(('/', '\\')) or '..' in nombre.split('/'):
|
|
337
|
+
return 'the zip tries to escape its folder'
|
|
338
|
+
if (info.external_attr >> 16) & 0o170000 == 0o120000:
|
|
339
|
+
return 'symlinks in a skill are refused'
|
|
340
|
+
return ''
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def extrae_zip(archivo, entradas, base, destino):
|
|
344
|
+
"""Write a zip's files under `destino`, containment resolved twice and the
|
|
345
|
+
decompressed bytes metered as they land — the headers already passed
|
|
346
|
+
`zip_inseguro`, but a header's declared size is the sender's word."""
|
|
347
|
+
presupuesto = MAX_EXTRAIDO
|
|
348
|
+
for info in entradas:
|
|
349
|
+
relativa = info.filename[len(base):] if info.filename.startswith(base) else ''
|
|
350
|
+
if not relativa or relativa.endswith('/'):
|
|
351
|
+
continue
|
|
352
|
+
ruta = os.path.realpath(os.path.join(destino, relativa))
|
|
353
|
+
if not ruta.startswith(destino + os.sep) and ruta != destino:
|
|
354
|
+
return 'the zip tries to escape its folder'
|
|
355
|
+
os.makedirs(os.path.dirname(ruta), exist_ok=True)
|
|
356
|
+
with archivo.open(info) as origen, open(ruta, 'wb') as salida:
|
|
357
|
+
while True:
|
|
358
|
+
trozo = origen.read(1024 * 64)
|
|
359
|
+
if not trozo:
|
|
360
|
+
break
|
|
361
|
+
presupuesto -= len(trozo)
|
|
362
|
+
if presupuesto < 0:
|
|
363
|
+
return 'the zip inflates too large for one skill'
|
|
364
|
+
salida.write(trozo)
|
|
365
|
+
return ''
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _nombre_de_skill_en_zip(entradas, pedido):
|
|
369
|
+
"""The skill's name and its base inside the zip: the single top folder when
|
|
370
|
+
there is one, else the zip root plus a name the caller supplies."""
|
|
371
|
+
raices = {e.filename.split('/')[0] for e in entradas if e.filename.strip('/')}
|
|
372
|
+
con_carpeta = len(raices) == 1 and any(
|
|
373
|
+
'/' in e.filename or e.filename.endswith('/') for e in entradas
|
|
374
|
+
)
|
|
375
|
+
base = (next(iter(raices)) + '/') if con_carpeta else ''
|
|
376
|
+
crudo = next(iter(raices)) if con_carpeta else str(pedido or '')
|
|
377
|
+
rutas_dentro = {e.filename[len(base):] for e in entradas if e.filename.startswith(base)}
|
|
378
|
+
return card.rol_seguro(crudo, defecto=''), base, rutas_dentro
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _sitio_para_skill(raiz, etiqueta):
|
|
382
|
+
"""(destination, '') for a free, plainly-named slot under the skills root."""
|
|
383
|
+
if not etiqueta:
|
|
384
|
+
return '', 'the skill needs a plain folder name'
|
|
385
|
+
destino = os.path.join(raiz, etiqueta)
|
|
386
|
+
if os.path.lexists(destino):
|
|
387
|
+
return '', f'{etiqueta} already exists there'
|
|
388
|
+
return destino, ''
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _copia_skill(ruta, raiz, nombre):
|
|
392
|
+
import shutil as _sh
|
|
393
|
+
|
|
394
|
+
if not os.path.isfile(os.path.join(ruta, 'SKILL.md')):
|
|
395
|
+
return '', 'a skill is a folder with a SKILL.md in it'
|
|
396
|
+
etiqueta = card.rol_seguro(nombre or os.path.basename(ruta.rstrip(os.sep)), defecto='')
|
|
397
|
+
destino, mal = _sitio_para_skill(raiz, etiqueta)
|
|
398
|
+
if mal:
|
|
399
|
+
return '', mal
|
|
400
|
+
os.makedirs(raiz, exist_ok=True)
|
|
401
|
+
try:
|
|
402
|
+
# symlinks=False: a link inside the source would otherwise land in the
|
|
403
|
+
# agent's home still pointing wherever the copier could reach.
|
|
404
|
+
_sh.copytree(ruta, destino, symlinks=False, ignore_dangling_symlinks=True)
|
|
405
|
+
except OSError as e:
|
|
406
|
+
_sh.rmtree(destino, ignore_errors=True)
|
|
407
|
+
return '', f'could not install the skill: {e}'
|
|
408
|
+
return etiqueta, ''
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _descomprime_skill(ruta, raiz, nombre):
|
|
412
|
+
import shutil as _sh
|
|
413
|
+
import zipfile
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
archivo = zipfile.ZipFile(ruta)
|
|
417
|
+
entradas = archivo.infolist()
|
|
418
|
+
except (OSError, zipfile.BadZipFile):
|
|
419
|
+
return '', 'that is not a readable folder or zip'
|
|
420
|
+
mal = zip_inseguro(entradas)
|
|
421
|
+
if mal:
|
|
422
|
+
return '', mal
|
|
423
|
+
etiqueta, base, dentro = _nombre_de_skill_en_zip(entradas, nombre)
|
|
424
|
+
if 'SKILL.md' not in dentro:
|
|
425
|
+
return '', 'a skill is a folder with a SKILL.md in it'
|
|
426
|
+
destino, mal = _sitio_para_skill(raiz, etiqueta)
|
|
427
|
+
if mal:
|
|
428
|
+
return '', mal
|
|
429
|
+
try:
|
|
430
|
+
os.makedirs(destino)
|
|
431
|
+
mal = extrae_zip(archivo, entradas, base, destino)
|
|
432
|
+
except OSError as e:
|
|
433
|
+
mal = f'could not install the skill: {e}'
|
|
434
|
+
if mal:
|
|
435
|
+
_sh.rmtree(destino, ignore_errors=True)
|
|
436
|
+
return '', mal
|
|
437
|
+
return etiqueta, ''
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def instala_skill(data, slug, origen, nombre=None):
|
|
441
|
+
"""Install one skill from a folder or a `.zip` into an agent's own home.
|
|
442
|
+
|
|
443
|
+
Returns (skill name, ''), or ('', why). Never raises for a bad input: both
|
|
444
|
+
callers answer a person, and a traceback is not an answer.
|
|
445
|
+
"""
|
|
446
|
+
ruta = rutas.canonicaliza(origen)
|
|
447
|
+
if not os.path.exists(ruta):
|
|
448
|
+
return '', f'nothing at {ruta}'
|
|
449
|
+
raiz = raiz_de_skills(crea_workspace(data, slug))
|
|
450
|
+
if not raiz:
|
|
451
|
+
return '', "this agent's .claude/skills is a link out of its home"
|
|
452
|
+
if os.path.isdir(ruta):
|
|
453
|
+
return _copia_skill(ruta, raiz, nombre)
|
|
454
|
+
return _descomprime_skill(ruta, raiz, nombre)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ── CLI ──────────────────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _lee_ficha(ruta):
|
|
461
|
+
with open(ruta, encoding='utf-8') as f:
|
|
462
|
+
return f.read()
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _cmd_agents(args):
|
|
466
|
+
if not args.card:
|
|
467
|
+
print('--card is required', file=sys.stderr)
|
|
468
|
+
return 2
|
|
469
|
+
for a in agentes(_lee_ficha(args.card), args.data):
|
|
470
|
+
# name<TAB>slug<TAB>role<TAB>runtime<TAB>kind<TAB>cwd<TAB>legacy
|
|
471
|
+
print('\t'.join([a.nombre, a.slug, a.rol, a.runtime, a.clase,
|
|
472
|
+
a.workspace, '1' if a.legacy else '0']))
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _cmd_sync(args):
|
|
477
|
+
if not args.card or not args.agent:
|
|
478
|
+
print('sync needs --card and --agent', file=sys.stderr)
|
|
479
|
+
return 2
|
|
480
|
+
objetivo = _slug(args.agent)
|
|
481
|
+
for a in agentes(_lee_ficha(args.card), args.data):
|
|
482
|
+
if a.slug == objetivo:
|
|
483
|
+
print(':'.join(sincroniza(a, args.data)))
|
|
484
|
+
return 0
|
|
485
|
+
return 0 # unknown agent: no targets, not an error
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _cmd_sync_all(args):
|
|
489
|
+
"""Read the card once, sync every agent, emit `slug<TAB>cwd<TAB>targets`.
|
|
490
|
+
|
|
491
|
+
The launcher consumes this single stream instead of one `agents` call plus
|
|
492
|
+
one `sync` per agent — one card parse and one interpreter start for the
|
|
493
|
+
whole city, not N+1.
|
|
494
|
+
"""
|
|
495
|
+
if not args.card:
|
|
496
|
+
print('--card is required', file=sys.stderr)
|
|
497
|
+
return 2
|
|
498
|
+
for a in agentes(_lee_ficha(args.card), args.data):
|
|
499
|
+
targets = ':'.join(sincroniza(a, args.data))
|
|
500
|
+
print('\t'.join([a.slug, a.workspace, targets]))
|
|
501
|
+
return 0
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _cmd_mount(args):
|
|
505
|
+
if not args.src:
|
|
506
|
+
print('mount needs --src', file=sys.stderr)
|
|
507
|
+
return 2
|
|
508
|
+
print(monta(args.data, args.agent, args.src, nombre=args.name))
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _cmd_unmount(args):
|
|
513
|
+
print('removed' if desmonta(args.data, args.agent, args.name or args.src or '')
|
|
514
|
+
else 'no such mount')
|
|
515
|
+
return 0
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _cmd_mounts(args):
|
|
519
|
+
for etiqueta, destino in mounts_en_disco(args.data, args.agent):
|
|
520
|
+
print(f'{etiqueta}\t{destino}')
|
|
521
|
+
return 0
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _cmd_targets(args):
|
|
525
|
+
agente = Agente(args.agent, _slug(args.agent), '', 'claude', CLASE_DEFECTO,
|
|
526
|
+
workspace_de(args.data, _slug(args.agent)), [], legacy=False)
|
|
527
|
+
print(':'.join(mount_targets(agente, args.data)))
|
|
528
|
+
return 0
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
#: Handlers that need an --agent; the two above (agents/sync) work off the card.
|
|
532
|
+
_NECESITAN_AGENTE = {'mount': _cmd_mount, 'unmount': _cmd_unmount,
|
|
533
|
+
'mounts': _cmd_mounts, 'targets': _cmd_targets}
|
|
534
|
+
_SOBRE_FICHA = {'list': _cmd_agents, 'sync': _cmd_sync, 'sync-all': _cmd_sync_all}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def main():
|
|
538
|
+
import argparse
|
|
539
|
+
|
|
540
|
+
p = argparse.ArgumentParser(description='Agent-first workspaces and their mounts.')
|
|
541
|
+
p.add_argument('orden', choices=list(_SOBRE_FICHA) + list(_NECESITAN_AGENTE))
|
|
542
|
+
p.add_argument('--data', required=True, help='the city data dir')
|
|
543
|
+
p.add_argument('--card', help='the owner card path (for `agents`/`sync`)')
|
|
544
|
+
p.add_argument('--agent', help='the agent slug (for mount/unmount/mounts/targets)')
|
|
545
|
+
p.add_argument('--src', help='mount: the real path to symlink in')
|
|
546
|
+
p.add_argument('--name', help='mount: an explicit link label')
|
|
547
|
+
args = p.parse_args()
|
|
548
|
+
try:
|
|
549
|
+
if args.orden in _SOBRE_FICHA:
|
|
550
|
+
return _SOBRE_FICHA[args.orden](args)
|
|
551
|
+
if not args.agent:
|
|
552
|
+
print('--agent is required', file=sys.stderr)
|
|
553
|
+
return 2
|
|
554
|
+
return _NECESITAN_AGENTE[args.orden](args)
|
|
555
|
+
except (OSError, ValueError) as e:
|
|
556
|
+
print(f'{e}', file=sys.stderr)
|
|
557
|
+
return 1
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
if __name__ == '__main__':
|
|
561
|
+
sys.exit(main())
|