diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..3e492f5 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,26 @@ +{ + "name": "agent-dispatch", + "owner": { + "name": "Octonove", + "url": "https://github.com/Octonove" + }, + "metadata": { + "description": "Decides whether to delegate to subagents, how many, and which model and effort each one gets." + }, + "plugins": [ + { + "name": "agent-dispatch", + "source": "./", + "description": "Decides whether to delegate to subagents, how many, and which model and effort each one gets. English.", + "category": "productivity", + "keywords": ["subagents", "delegation", "model-routing", "cost"] + }, + { + "name": "agent-dispatch-es", + "source": "./es", + "description": "Decide si delegar en subagentes, cuántos y qué modelo y esfuerzo lleva cada uno. Español.", + "category": "productivity", + "keywords": ["subagentes", "delegación", "modelos", "coste"] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..3c1c802 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "agent-dispatch", + "description": "Decides whether to delegate to subagents, how many, and which model and effort each one gets.", + "author": { + "name": "Octonove", + "url": "https://github.com/Octonove" + }, + "homepage": "https://github.com/Octonove/agent-dispatch", + "repository": "https://github.com/Octonove/agent-dispatch", + "license": "MIT", + "keywords": ["subagents", "delegation", "model-routing", "cost"] +} diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..9fd6967 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,34 @@ +name: checks + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + repo: + name: frontmatter, EN/ES parity, links, length, plugin layout + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install pyyaml==6.0.2 + - run: python3 scripts/check.py + + claude-plugin: + name: Claude Code plugin validator + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + # Pinned to the version these manifests were tested with; offline, needs no credentials. + - run: npx --yes @anthropic-ai/claude-code@2.1.300 plugin validate . + - run: npx --yes @anthropic-ai/claude-code@2.1.300 plugin validate es diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/README.es.md b/README.es.md index 0e33402..b755486 100644 --- a/README.es.md +++ b/README.es.md @@ -6,13 +6,14 @@ Una skill gratuita y suelta que evita que todos tus subagentes corran con el modelo más grande. -Por defecto, Claude Code le pasa a cada subagente el modelo que tienes seleccionado para la -sesión. Así que cuando una tarea se reparte en nueve agentes, los nueve salen en Fable 5.1 (u -Opus, o el que tengas puesto): los cuatro que traducen un `messages.json` cuestan lo mismo por -token que el que diseña la arquitectura. Esta skill hace el reparto explícito antes de lanzar -nada: **el modelo se elige por el tipo de tarea, nunca por la importancia del proyecto.** +Salvo que la llamada, la definición del subagente o `CLAUDE_CODE_SUBAGENT_MODEL` fijen un +modelo, Claude Code corre cada subagente en el modelo que tienes seleccionado para la sesión +([documentación](https://code.claude.com/docs/en/sub-agents)). Así que cuando una tarea se +reparte, los agentes que traducen un `messages.json` cuestan lo mismo por token que el que diseña +la arquitectura. Esta skill hace el reparto explícito antes de lanzar nada: **el modelo se elige +por el tipo de tarea, nunca por la importancia del proyecto.** -![Demo de Agent Dispatch](https://raw.githubusercontent.com/Octonove/agent-dispatch/main/docs/demo.gif) +![Demo de Agent Dispatch: la línea de despacho del ejemplo de la extensión de Chrome (ilustración, no una ejecución grabada)](https://raw.githubusercontent.com/Octonove/agent-dispatch/main/docs/demo.gif) **☕ Si esto te ahorra tiempo, invítame a un café.** [![Invítame a un café con PayPal](https://img.shields.io/badge/PayPal-Inv%C3%ADtame%20a%20un%20caf%C3%A9-00457C?logo=paypal&logoColor=white)](https://www.paypal.com/donate/?business=stradoxx%40gmail.com&no_recurring=0¤cy_code=EUR&item_name=Support%20agent%20dispatch) @@ -28,22 +29,30 @@ O en USDC. Envía **solo USDC** y **solo por la red indicada**; por otra r | Tipo de tarea | Ejemplos | Modelo · esfuerzo | |---|---|---| -| **Mecánica** | traducir cadenas fijas, listar, contar, medir, ejecutar un script y reportar la salida, comprobar que un manifest parsea | **pequeño** (p. ej. Haiku) · bajo | +| **Mecánica** | traducir cadenas fijas, listar, contar, extraer campos con formato fijo, recortar un log largo a sus errores | **pequeño** (p. ej. Haiku) · bajo | | **Analítica acotada** | resumir un módulo, mapear dependencias, escribir tests a partir de una especificación clara, un cambio mecánico en muchos ficheros | **medio** (p. ej. Sonnet) · medio | -| **Juicio** | arquitectura, escribir el código que decide cómo funciona algo, elegir entre opciones, la revisión final, lo que se entrega | **el modelo de la sesión** (p. ej. Fable 5.1, Opus) · alto | +| **Juicio** | arquitectura, escribir el código que decide cómo funciona algo, elegir entre opciones, la revisión final, lo que se entrega | **el modelo de la sesión** · el de la sesión | Antes de todo eso hace la pregunta barata: **¿hay que delegar, y cuántos?** Por defecto no, y la -cuenta empieza en cero — casi todo es trabajo en línea. - -Más cinco salvaguardas que la skill impone: la revisión final nunca se abarata; un agente -pequeño que falla sube la tarea de nivel en vez de reintentarse; menos agentes valen más que más -agentes, y ninguno existe solo para recomprobar la conclusión de otro; cada subagente recibe un -**encargo autocontenido y un contrato de retorno** (objetivo, contexto mínimo, qué no tocar, qué -mirará quien revise, formato de salida y criterio de terminado), porque nunca ve tu -conversación; y **todo bucle declara su tope** antes de empezar: la condición de éxito y el -máximo de rondas, con un solo reintento del mismo enfoque. -Además, cada ejecución **declara su reparto antes de lanzar y reporta el gasto medido después**, -o dice «sin medir». Nada de ahorros inventados. +cuenta empieza en cero: casi todo es trabajo en línea, y comprobar lo que comprueba un comando +(`grep`, pasar los tests) nunca es razón para delegar. + +Más cinco salvaguardas que la skill le pide al agente: la revisión final nunca se abarata; un +agente pequeño o medio que falla recibe su único reintento un nivel más arriba, nunca en el mismo +modelo; menos agentes valen más que más agentes, y ninguno existe solo para recomprobar la +conclusión de otro; cada subagente recibe un **encargo autocontenido y un contrato de retorno** +(objetivo, contexto mínimo, qué no tocar, qué mirará quien revise, formato de salida y criterio de +terminado), porque nunca ve tu conversación; y **todo bucle declara su tope** antes de empezar: la +condición de éxito y el máximo de rondas, con un solo reintento del mismo enfoque. Además, cada +ejecución **declara su reparto y su tope antes de lanzar y reporta el gasto medido después**, o +dice «sin medir». Nada de ahorros inventados. + +En Claude Code los niveles se traducen en parámetros de la propia herramienta `Agent`: +`model: "haiku"` o `model: "sonnet"` con `effort` por llamada (Claude Code 2.1.292 o +posterior), y ni `model` ni `effort` para el juicio, que así hereda los dos de la sesión: usa un +tipo de subagente cuya definición no fije ninguno y, si está fijado `CLAUDE_CODE_SUBAGENT_MODEL`, +pasa el alias de la sesión, como `model: "opus"`. Para otros clientes, mira +[los nombres de modelo por cliente](examples/decisions.es.md#nombres-de-modelo-por-cliente). ## Ejemplo trabajado: una extensión de Chrome pequeña @@ -53,54 +62,145 @@ o dice «sin medir». Nada de ahorros inventados. ![Despacho del ejemplo de la extensión de Chrome](https://raw.githubusercontent.com/Octonove/agent-dispatch/main/docs/example-chrome-extension.es.png) -| # | Subtarea | Modelo · esfuerzo | +| # | Subtarea | Dónde corre | |---|---|---| -| 1 | Diseño: manifest v3, ficheros, permisos, cómo hablan el content script y el popup | modelo de sesión · alto | -| 2 | Escribir el código: content script, popup, service worker, módulo de conversión | modelo de sesión · alto | -| 3 | Escribir los tests de conversión a partir de la especificación | medio · medio | -| 4–7 | Traducir `messages.json` a ES, FR, DE, IT — un agente por idioma | pequeño · bajo | -| 8 | Validar el manifest, pasar lint y los tests, reportar la salida literal | pequeño · bajo | -| 9 | Revisión final: idiomas completos, permisos coherentes con el código, tests en verde | modelo de sesión · alto | - -Sin la skill: 9 agentes en el modelo de la sesión. Con ella: **3 en el modelo de la sesión, 1 en -el medio, 5 en el pequeño.** La misma extensión. El juicio se quedó donde importa. El recorrido -completo, con el briefing que recibe cada agente, está en -[`examples/chrome-extension.es.md`](examples/chrome-extension.es.md). +| 1 | Diseño: manifest v3, ficheros, permisos, cómo hablan el content script y el popup | en línea · modelo de sesión | +| 2 | Escribir el código: content script, popup, service worker, módulo de conversión | en línea · modelo de sesión | +| 3 | Escribir los tests de conversión a partir de la especificación | 1 agente · medio · medio | +| 4–7 | Traducir `messages.json` a ES, FR, DE, IT, un agente por idioma | 4 agentes · pequeño · bajo | +| 8 | Validar el manifest, pasar lint y los tests | en línea · lo comprueba un comando | +| 9 | Revisión final: idiomas completos, permisos coherentes con el código, tests en verde | en línea · modelo de sesión | + +``` +Despacho: 5 agentes · 1 medio (tests) · 4 pequeños (traducciones) · en línea: diseño, código, lint + tests, revisión final · tope: 1 ronda de corrección +``` + +Diseño y código son una misma cadena de juicio, así que se quedan en la sesión; lint y tests son +un comando, no merecen un subagente. Lo que corre en paralelo y en niveles más baratos es el +trabajo de verdad independiente: los tests y las cuatro traducciones. La skill no le pone cifra al +ahorro; se la pone el gasto que reporte tu cliente después de la ejecución. El recorrido +completo, con el encargo que recibe cada agente, está en +[`examples/chrome-extension.es.md`](examples/chrome-extension.es.md), y más casos (no delegar, un +agente, varias parcelas, un agente que falla) en [`examples/decisions.es.md`](examples/decisions.es.md). ## Instalación -**Claude Code** — como skill global (aplica en todos los proyectos): +Elige una. La skill en inglés es [`SKILL.md`](SKILL.md): cambia `es/SKILL.md` por `SKILL.md` en +las URL, o instala el plugin `agent-dispatch`. + +### Claude Code + +**Skill personal**, para todos los proyectos de tu máquina: ```bash mkdir -p ~/.claude/skills/agent-dispatch -curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/es/SKILL.md -o ~/.claude/skills/agent-dispatch/SKILL.md +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/es/SKILL.md \ + -o ~/.claude/skills/agent-dispatch/SKILL.md +``` + +**Skill de proyecto**, en el repositorio para que la tenga todo el equipo: + +```bash +mkdir -p .claude/skills/agent-dispatch +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/es/SKILL.md \ + -o .claude/skills/agent-dispatch/SKILL.md +``` + +**Plugin**, desde el marketplace de este repositorio: + +```text +/plugin marketplace add Octonove/agent-dispatch +/plugin install agent-dispatch-es@agent-dispatch ``` -O por proyecto: el mismo fichero en `.claude/skills/agent-dispatch/SKILL.md`. Versión en inglés: -[`SKILL.md`](SKILL.md). Claude Code carga las skills solo; la descripción del frontmatter es lo -que hace que aplique cada vez que hay subagentes sobre la mesa. +Desde la terminal: `claude plugin marketplace add Octonove/agent-dispatch` y después +`claude plugin install agent-dispatch-es@agent-dispatch`. En Claude Code 2.1.275 o posterior es un +solo paso: `/plugin install agent-dispatch-es --marketplace Octonove/agent-dispatch`. El plugin en +inglés es `agent-dispatch@agent-dispatch`. Los marketplaces de terceros no se actualizan solos por +defecto: `claude plugin update agent-dispatch-es@agent-dispatch` trae el último commit. + +**Compruébalo:** `/skills` la lista y `/agent-dispatch` la ejecuta a mano (instalada como plugin, +lleva el espacio de nombres: `/agent-dispatch-es:agent-dispatch`). Una skill nueva se carga sin +reiniciar; si `~/.claude/skills` o el `.claude/skills` del proyecto no existían al empezar la +sesión, ejecuta `/reload-skills`. Un plugin instalado o actualizado desde la terminal necesita +`/reload-plugins` en una sesión ya abierta. + +**Que aplique siempre.** Una skill no está siempre activa: Claude Code solo mantiene su +descripción en el contexto y carga el cuerpo cuando la descripción encaja con lo que pides +(`claude plugin details agent-dispatch-es` estima unos 250 y 3.000 tokens). Para que salte cada +vez que se plantea delegar, añade una línea a tu `CLAUDE.md`: + +```text +Antes de lanzar cualquier subagente, agentes en paralelo, flujo o bucle, aplica la skill agent-dispatch. +``` -**Cursor** — copia el cuerpo de `SKILL.md` en una regla (`.cursor/rules/agent-dispatch.mdc`) -con `alwaysApply: true`. +### Cursor -**Cualquier otro cliente** — pega el cuerpo de `SKILL.md` en el system prompt o en las -instrucciones del proyecto. Si el cliente no permite elegir modelo por subagente, la skill lo dice -y aplica el resto (si delegar, cuántos, briefing, declarar, medir). +Como skill, que Cursor carga por su descripción desde `.cursor/skills/` (proyecto) o +`~/.cursor/skills/` (usuario): + +```bash +mkdir -p .cursor/skills/agent-dispatch +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/es/SKILL.md \ + -o .cursor/skills/agent-dispatch/SKILL.md +``` + +O como regla aplicada en cada petición, que entonces paga el fichero entero cada vez: + +```bash +mkdir -p .cursor/rules +body=$(curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/es/SKILL.md) && +{ printf '%s\n' '---' 'description: Antes de lanzar subagentes, decide si delegar, cuántos y qué modelo lleva cada uno' 'alwaysApply: true' '---' + printf '%s\n' "$body" | awk 'n >= 2; /^---$/ { n++ }' +} > .cursor/rules/agent-dispatch.mdc +``` + +Para dar a cada nivel su modelo, define los subagentes en `.cursor/agents/` con un campo `model` +(mira [los nombres de modelo por cliente](examples/decisions.es.md#nombres-de-modelo-por-cliente)). + +### Otros clientes + +Clientes que leen el formato [Agent Skills](https://agentskills.io) desde `.agents/skills/` +(Codex, Gemini CLI, GitHub Copilot y OpenCode, según su documentación): + +```bash +mkdir -p .agents/skills/agent-dispatch +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/es/SKILL.md \ + -o .agents/skills/agent-dispatch/SKILL.md +``` + +Usa `~/.agents/skills/` para todos los proyectos. Clientes sin skills: pega el cuerpo de +`es/SKILL.md` (todo lo que va después del segundo `---`) en el system prompt, en `AGENTS.md` o en +las instrucciones del proyecto. Si el cliente no permite elegir modelo por subagente, la skill lo +dice en la línea de despacho y aplica el resto: si delegar, cuántos, encargos, topes de bucle, +declarar y medir. + +Los comandos de Claude Code de arriba se ejecutaron al escribir este README: el plugin con +`claude plugin install` y `claude plugin details`, las carpetas de skill con `curl` y +`claude plugin validate`. Las rutas de Cursor y de otros clientes salen de la documentación de +cada fabricante y no se han probado aquí. ## Qué te da y qué no -- Te da una línea como `Despacho: 9 agentes · 3 de sesión · 1 medio · 5 pequeños` antes de que - corra nada, y el gasto que reporte la herramienta al terminar. +- Te da una línea como `Despacho: 5 agentes · 1 medio · 4 pequeños · en línea: diseño, código, revisión · tope: 1 ronda de corrección` + antes de que corra nada, y el gasto que reporte la herramienta al terminar. - **No** te da una cifra de ahorro. No puede medir lo que tu herramienta no reporta, y se niega a inventarla. Mide tus propias ejecuciones: de eso va. - El juicio nunca se abarata. Si quieres todo en Haiku, esta no es tu skill. +## Contribuir + +`python3 scripts/check.py` ejecuta las comprobaciones del repositorio que hace la CI en cada pull +request y en cada push a `main`: frontmatter válido; las mismas secciones, reglas y cifras en +inglés y en español; enlaces internos que funcionan; `SKILL.md` por debajo de su límite de +longitud; y los manifiestos del plugin. La CI además ejecuta `claude plugin validate .` y +`claude plugin validate es`. Cambia los dos idiomas en el mismo commit. + ## Relacionado -- **[Invokard](https://invokard.web.app)** — 55 «cartas» de habilidades para Claude y cualquier - cliente MCP, 9 de ellas gratis. Esta regla es la sección 1G de **El Orquestador**, la carta - gratuita que enruta cada petición al especialista correcto; si usas esa carta, el despacho ya - va dentro. +- **[Invokard](https://invokard.web.app)** — «cartas» de habilidades para Claude y cualquier + cliente MCP. Esta regla es la sección 1G de **El Orquestador**, la carta gratuita que enruta + cada petición al especialista correcto; si usas esa carta, el despacho ya va dentro. - **[CRBRO](https://github.com/Octonove/crbro-memory)** — un servidor MCP de código abierto que da memoria persistente a tu IA: neuronas, sesiones y un índice de búsqueda en tu propio disco, sin cuenta. Ahí acaba el gasto medido de cada ejecución cuando quiero comparar semanas. diff --git a/README.md b/README.md index e7f927b..fc90e82 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,14 @@ A free, standalone skill that stops every subagent from running on your biggest model. -By default, Claude Code hands every subagent the model you have selected for the session. So -when a task fans out into nine agents, all nine run on Fable 5.1 (or Opus, or whatever you -picked) — the four that translate a `messages.json` file cost the same per token as the one +Unless the call, the subagent's definition or `CLAUDE_CODE_SUBAGENT_MODEL` sets a model, Claude +Code runs every subagent on the model you selected for the session +([docs](https://code.claude.com/docs/en/sub-agents)). So when a +task fans out, the agents that translate a `messages.json` cost the same per token as the one that designs the architecture. This skill makes the split explicit before anything launches: **the model is picked by the type of task, never by the importance of the project.** -![Agent Dispatch demo](https://raw.githubusercontent.com/Octonove/agent-dispatch/main/docs/demo.gif) +![Agent Dispatch demo: the dispatch line of the Chrome extension example (illustration, not a recorded run)](https://raw.githubusercontent.com/Octonove/agent-dispatch/main/docs/demo.gif) **☕ If this saves you time, buy me a coffee.** [![Buy me a coffee with PayPal](https://img.shields.io/badge/PayPal-Buy%20me%20a%20coffee-00457C?logo=paypal&logoColor=white)](https://www.paypal.com/donate/?business=stradoxx%40gmail.com&no_recurring=0¤cy_code=EUR&item_name=Support%20agent%20dispatch) @@ -28,22 +29,30 @@ Or in USDC. Send **USDC only** and **only on the network shown**; on any other n | Type of task | Examples | Model · effort | |---|---|---| -| **Mechanical** | translations of fixed strings, listing, counting, measuring, running a script and reporting the output, checking that a manifest parses | **small** (e.g. Haiku) · low | +| **Mechanical** | translations of fixed strings, listing, counting, extracting fields in a fixed format, cutting a long log down to its errors | **small** (e.g. Haiku) · low | | **Bounded analysis** | summarizing a module, mapping dependencies, writing tests from a clear spec, one mechanical change across many files | **mid** (e.g. Sonnet) · medium | -| **Judgment** | architecture, writing the code that decides how something works, choosing between options, the final review, anything that ships | **the session's model** (e.g. Fable 5.1, Opus) · high | +| **Judgment** | architecture, writing the code that decides how something works, choosing between options, the final review, anything that ships | **the session's model** · the session's | Before any of that it asks the cheaper question first: **should this be delegated at all, and how -many?** The default is no, and the count starts at zero — most work is inline work. - -Plus five guardrails the skill enforces: the final reviewer never gets cheapened; a small agent -that fails moves the task up a tier instead of being retried; fewer agents beat more agents, and -no agent exists just to double-check another's conclusion; every subagent gets a -**self-contained brief and a return contract** (goal, minimum context, what it must not touch, -what the reviewer will check, output format, definition of done), because it never sees your -conversation; and **every loop declares its cap** before it starts — the success condition and -the maximum number of rounds, one retry of the same approach at most. -On top of that, every run **declares its split before launching and reports the measured spend -after** — or says "not measured". No invented savings. +many?** The default is no, and the count starts at zero: most work is inline work, and checking +what a command can check (`grep`, running the tests) is never a reason to delegate. + +Plus five guardrails the skill tells the agent to keep: the final reviewer never gets cheapened; +a small or mid agent that fails gets its one retry a tier up, never on the same model; fewer +agents beat more agents, and no agent exists just to re-check another's conclusion; every +subagent gets a **self-contained brief and a return contract** (goal, minimum context, what it +must not touch, what the reviewer will check, output format, definition of done), because it +never sees your conversation; and **every loop declares its cap** before it starts: the success +condition and the maximum number of rounds, one retry of the same approach at most. On top of +that, every run **declares its split and its cap before launching and reports the measured spend +after**, or says "not measured". No invented savings. + +In Claude Code the tiers map to the `Agent` tool's own parameters: `model: "haiku"` or +`model: "sonnet"` with `effort` per call (Claude Code 2.1.292 or later), and neither `model` nor +`effort` for judgment, so it inherits both from the session: use a subagent type whose definition +sets neither, and if `CLAUDE_CODE_SUBAGENT_MODEL` is set, pass the session's alias, such as +`model: "opus"`. For other clients, see +[the model names per client](examples/decisions.md#model-names-per-client). ## Worked example: a small Chrome extension @@ -53,53 +62,144 @@ after** — or says "not measured". No invented savings. ![Dispatch of the Chrome extension example](https://raw.githubusercontent.com/Octonove/agent-dispatch/main/docs/example-chrome-extension.png) -| # | Subtask | Model · effort | +| # | Subtask | Runs on | |---|---|---| -| 1 | Design: manifest v3, files, permissions, how content script and popup talk | session model · high | -| 2 | Write the code: content script, popup, service worker, conversion module | session model · high | -| 3 | Write the conversion tests from the spec | mid · medium | -| 4–7 | Translate `messages.json` into ES, FR, DE, IT — one agent per language | small · low | -| 8 | Validate the manifest, run lint and the tests, report the output verbatim | small · low | -| 9 | Final review: locales complete, permissions match the code, tests green | session model · high | - -Without the skill: 9 agents on the session's model. With it: **3 on the session's model, 1 on -the mid model, 5 on the small one.** Same extension. The judgment stayed where it matters. -The full walkthrough, with the briefing each agent receives, is in -[`examples/chrome-extension.md`](examples/chrome-extension.md). +| 1 | Design: manifest v3, files, permissions, how content script and popup talk | inline · session model | +| 2 | Write the code: content script, popup, service worker, conversion module | inline · session model | +| 3 | Write the conversion tests from the spec | 1 agent · mid · medium | +| 4–7 | Translate `messages.json` into ES, FR, DE, IT, one agent per language | 4 agents · small · low | +| 8 | Validate the manifest, run lint and the tests | inline · a command checks it | +| 9 | Final review: locales complete, permissions match the code, tests green | inline · session model | + +``` +Dispatch: 5 agents · 1 mid (tests) · 4 small (translations) · inline: design, code, lint + tests, final review · cap: 1 fix round +``` + +Design and code are one chain of judgment, so they stay with the session; lint and tests are a +command, not worth a subagent. What runs in parallel on cheaper tiers is the work that is truly +independent: the tests and the four translations. The skill puts no number on the saving; the +spend your client reports after the run does. The full walkthrough, with the brief each agent +receives, is in [`examples/chrome-extension.md`](examples/chrome-extension.md), and more cases (no +delegation, one agent, several slices, a failing agent) are in +[`examples/decisions.md`](examples/decisions.md). ## Install -**Claude Code** — as a global skill (applies in every project): +Pick one. The Spanish skill is [`es/SKILL.md`](es/SKILL.md): swap `SKILL.md` for `es/SKILL.md` in +the URLs, or install the `agent-dispatch-es` plugin. + +### Claude Code + +**Personal skill**, for every project on your machine: ```bash mkdir -p ~/.claude/skills/agent-dispatch -curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/SKILL.md -o ~/.claude/skills/agent-dispatch/SKILL.md +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/SKILL.md \ + -o ~/.claude/skills/agent-dispatch/SKILL.md +``` + +**Project skill**, committed so your team gets it too: + +```bash +mkdir -p .claude/skills/agent-dispatch +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/SKILL.md \ + -o .claude/skills/agent-dispatch/SKILL.md +``` + +**Plugin**, from the marketplace in this repository: + +```text +/plugin marketplace add Octonove/agent-dispatch +/plugin install agent-dispatch@agent-dispatch ``` -Or per project: put the same file at `.claude/skills/agent-dispatch/SKILL.md`. Spanish version: -[`es/SKILL.md`](es/SKILL.md). Claude Code loads skills automatically; the description in the -frontmatter is what makes it apply whenever subagents are on the table. +From the shell: `claude plugin marketplace add Octonove/agent-dispatch`, then +`claude plugin install agent-dispatch@agent-dispatch`. On Claude Code 2.1.275 or later it is one +step: `/plugin install agent-dispatch --marketplace Octonove/agent-dispatch`. The Spanish plugin +is `agent-dispatch-es@agent-dispatch`. Third-party marketplaces do not auto-update by default: +`claude plugin update agent-dispatch@agent-dispatch` fetches the latest commit. + +**Check it:** `/skills` lists it and `/agent-dispatch` runs it by hand (installed as a plugin, the +skill is namespaced: `/agent-dispatch:agent-dispatch`). A new skill is picked up without a +restart; if `~/.claude/skills` or the project's `.claude/skills` did not exist when the session +started, run `/reload-skills`. A plugin installed or updated from the shell needs +`/reload-plugins` in a session that is already open. + +**Make it apply every time.** A skill is not always on: Claude Code keeps only its description in +context and loads the body when the description matches what you asked +(`claude plugin details agent-dispatch` estimates about 170 and 2,200 tokens). To make it fire +whenever delegation comes up, add one line to your `CLAUDE.md`: + +```text +Before launching any subagent, parallel agents, workflow or loop, apply the agent-dispatch skill. +``` -**Cursor** — copy the body of `SKILL.md` into a rule (`.cursor/rules/agent-dispatch.mdc`) with -`alwaysApply: true`. +### Cursor -**Any other client** — paste the body of `SKILL.md` into your system prompt or project -instructions. If the client cannot choose a model per subagent, the skill says so and applies -the rest (whether to delegate, how many, briefing, declare, measure). +As a skill, which Cursor loads by its description from `.cursor/skills/` (project) or +`~/.cursor/skills/` (user): + +```bash +mkdir -p .cursor/skills/agent-dispatch +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/SKILL.md \ + -o .cursor/skills/agent-dispatch/SKILL.md +``` + +Or as a rule applied to every request, which then pays for the whole file every time: + +```bash +mkdir -p .cursor/rules +body=$(curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/SKILL.md) && +{ printf '%s\n' '---' 'description: Before launching subagents, decide whether to delegate, how many, and which model each one gets' 'alwaysApply: true' '---' + printf '%s\n' "$body" | awk 'n >= 2; /^---$/ { n++ }' +} > .cursor/rules/agent-dispatch.mdc +``` + +To give each tier its own model, define the subagents in `.cursor/agents/` with a `model` field +(see [the model names per client](examples/decisions.md#model-names-per-client)). + +### Other clients + +Clients that read the [Agent Skills](https://agentskills.io) format from `.agents/skills/` +(Codex, Gemini CLI, GitHub Copilot and OpenCode, according to their docs): + +```bash +mkdir -p .agents/skills/agent-dispatch +curl -fsSL https://raw.githubusercontent.com/Octonove/agent-dispatch/main/SKILL.md \ + -o .agents/skills/agent-dispatch/SKILL.md +``` + +Use `~/.agents/skills/` for every project. Clients without skills: paste the body of `SKILL.md` +(everything after the second `---`) into the system prompt, `AGENTS.md` or the project +instructions. If the client cannot choose a model per subagent, the skill says so in the dispatch +line and applies the rest: whether to delegate, how many, briefs, loop caps, declare and measure. + +The Claude Code commands above were run while writing this README: the plugin with +`claude plugin install` and `claude plugin details`, the skill folders with `curl` and +`claude plugin validate`. The Cursor and other-client paths come from each vendor's documentation +and are untested here. ## What you get, and what you don't -- You get a line like `Dispatch: 9 agents · 3 session-tier · 1 mid · 5 small` before anything - runs, and the spend the tool reports when it is done. +- You get a line like `Dispatch: 5 agents · 1 mid · 4 small · inline: design, code, review · cap: 1 fix round` + before anything runs, and the spend the tool reports when it is done. - You do **not** get a savings number from the skill. It cannot measure what your tool does not report, and it refuses to invent one. Measure your own runs; that is the point. - Judgment never gets cheaper. If you want everything on Haiku, this is not the skill for you. +## Contributing + +`python3 scripts/check.py` runs the repository checks that CI runs on pull requests and on pushes +to `main`: valid frontmatter; the same sections, rules and numbers in English and Spanish; working +internal links; `SKILL.md` under its length limit; and the plugin manifests. CI also runs +`claude plugin validate .` and `claude plugin validate es`. Change both languages in the same +commit. + ## Related -- **[Invokard](https://invokard.web.app)** — 55 skill "cards" for Claude and any MCP client, - 9 of them free. This rule is section 1G of **The Orchestrator**, the free card that routes - every request to the right specialist; if you use that card, the dispatch is already inside it. +- **[Invokard](https://invokard.web.app)** — skill "cards" for Claude and any MCP client. This + rule is section 1G of **The Orchestrator**, the free card that routes every request to the + right specialist; if you use that card, the dispatch is already inside it. - **[CRBRO](https://github.com/Octonove/crbro-memory)** — an open-source MCP server that gives your AI persistent memory: neurons, sessions and a search index on your own disk, no account. It is where the measured spend of each run ends up when I want to compare weeks. diff --git a/SKILL.md b/SKILL.md index f558254..a2e42ee 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,129 +1,110 @@ --- name: agent-dispatch -description: "Always-on rule for any client that can launch subagents (Claude Code, Cursor, Antigravity…). Before delegating, it decides whether to delegate at all — the default is no — then how many (starting at zero), how to split the work, and which model and effort each subagent gets, by the type of task and never by the importance of the project. Mechanical work (translations, listing, measuring, running scripts) goes to a small model; bounded analysis to a mid model; design, code, decisions and the final review stay on the session's model. Declares the split before launching and reports the measured spend after." +description: "Decides whether to delegate to subagents at all (default: no), how many (start at zero) and which model and effort each one gets, by task type and never by project importance. Use before launching any subagent, parallel agents, background agent, workflow or review loop, or when a task could fan out. Mechanical work goes to a small model, bounded analysis to a mid model, judgment stays on the session's model. Declares the split and the loop cap first; reports the measured spend after." --- # Agent Dispatch — which model runs each subagent -You are running inside a client that can launch subagents (Claude Code's `Agent` and `Workflow` -tools, Cursor's background agents, or similar). By default every subagent inherits the model you -have selected for the session, so a four-language translation costs the same per token as an -architecture decision. This rule fixes that. It governs one thing only: **the spend on agents**. -It never changes the model of the conversation itself. +Unless told otherwise, a subagent runs on the session's model, so a four-language translation +costs the same per token as an architecture decision. This rule governs one thing: **the spend +on agents**. It never changes the model of the conversation. Apply it, in order, every time work +is about to leave the conversation: a subagent, parallel agents, a workflow, a loop. -Apply it every time work is about to leave the conversation. Apply it in order. - -## 0. Should this be delegated at all? +## 1. Delegate at all? Default: no Delegate only if one of these holds: - **independent parts** that genuinely run at the same time and save wall-clock; -- **more reading than fits in one context** (dozens of files, transcripts, long outputs). +- **more reading than fits in one context** (dozens of files, transcripts, long logs). -Otherwise do the work inline. A subagent does not see this conversation: it costs its whole -context plus the briefing you write for it, and its answer still has to be verified. Two things -are never a reason to delegate: checking something a command can check (`curl`, `grep`, reading -a file), and getting a second opinion on work that is already verified — where direct evidence -exists, the evidence decides. +Otherwise work inline. A subagent does not see this conversation: it costs its whole context +plus the brief you write, and its answer still has to be verified. Never a reason to delegate: +checking what a command can check (`grep`, `curl`, running the tests, reading a file), or a +second opinion on work that is already verified. Where direct evidence exists, it decides. -## 0b. How many? Start at zero +## 2. How many? Start at zero -Zero is the normal answer: most work is inline work. One well-briefed subagent covers most of -what is worth delegating at all. Several only when each one owns a slice no other one can cover -— and you name those slices before launching. If two of them sound alike as you write the split, -one is redundant: drop it. A fleet is not thoroughness; it is the same answer paid for several -times. +Zero is the normal answer. One well-briefed subagent covers most of what is worth delegating. +Several only when each owns a slice no other covers; name the slices before launching. If two +sound alike, drop one: a fleet is the same answer paid for several times. -## 1. Split the work and classify each part by TYPE +## 3. Classify each subtask by type -Write the subtasks down. Classify each one by what kind of work it is — **never by how important -the project is**. Three tiers: +Write the subtasks down and classify each by the kind of work, **never by how important the +project is**: | Tier | What it is | Examples | |---|---|---| -| **Mechanical** | No judgment; the output is right or wrong and easy to check | translations of fixed strings, listing files, counting, measuring sizes, transcribing an output verbatim, running an already-written script and reporting what it prints, checking that a JSON or a manifest parses, extracting fields with a fixed format, formatting, renaming | -| **Bounded analysis** | Reasoning inside a clear frame someone else set | summarizing a module, mapping dependencies, writing tests from a clear spec, applying one mechanical change across many files, comparing two versions of a text, filling a checklist | -| **Judgment** | Deciding, designing, or anything that ships without another review | architecture, writing the code that decides how something works, choosing between options, synthesizing a report, the final review that checks everything fits together, anything published or sent to a client | - -## 2. Assign a model and an effort per tier - -| Tier | Model | Effort | -|---|---|---| -| Mechanical | **small** (e.g. Haiku) | low | -| Bounded analysis | **mid** (e.g. Sonnet) | medium | -| Judgment | **the session's model** (e.g. Fable 5.1, Opus) | high | - -The model names are examples: use whatever tiers your client offers. If you hesitate between two -tiers, the lower one plus a verification by the upper one is cheaper than the upper one used -blind. - -If your client does not let you choose a model or an effort per subagent, do not pretend you -did: apply everything else (whether to delegate, how many, in what order, briefing, declare, -measure) and say so in one line. +| **Mechanical** | No judgment; the output is right or wrong and easy to check | translating fixed strings, listing, counting, extracting fields in a fixed format, formatting, renaming, cutting a long log down to its errors | +| **Bounded analysis** | Reasoning inside a frame someone else set | summarizing a module, mapping dependencies, writing tests from a clear spec, one mechanical change across many files, comparing two versions, filling a checklist | +| **Judgment** | Deciding, designing, or anything that ships without another review | architecture, the code that decides how something works, choosing between options, synthesizing a report, the final review, anything published or sent to a client | -## 3. Guardrails that are not negotiable +## 4. Model and effort per tier -1. **The final reviewer and whoever synthesizes never get cheapened.** The saving comes from - the mechanical work, never from what keeps a mistake from shipping. -2. **If a small agent fails or returns something doubtful, the task moves up one tier.** It is - not retried on the same model. +| Tier | Model | Effort | Claude Code `Agent` call | +|---|---|---|---| +| Mechanical | **small** (Haiku) | low | `model: "haiku"`, `effort: "low"` | +| Bounded analysis | **mid** (Sonnet) | medium | `model: "sonnet"`, `effort: "medium"` | +| Judgment | **the session's model** | the session's | no `model`, no `effort` (both inherit) | + +- In Claude Code, judgment inherits only through a subagent type whose definition sets no `model` + or `effort`. If `CLAUDE_CODE_SUBAGENT_MODEL` is set, pass the session's alias (`model: "opus"` + on an Opus session). +- The names are examples: use the tiers your client offers. Never go above the session's model; + if the session already runs the mid model, bounded analysis and judgment both stay on it. +- Unsure between two tiers: take the lower one. The final review (section 7) checks it. +- If the client cannot set the model or the effort per subagent (in Claude Code, `effort` needs + version `2.1.292` or later), do not pretend you did: apply the rest and say so in the dispatch + line. + +## 5. Guardrails that are not negotiable + +1. **The final reviewer and whoever synthesizes never get cheapened.** The saving comes from the + mechanical work, never from what keeps a mistake from shipping. +2. **Failure moves the task up a tier.** A small or mid agent that fails or returns something + doubtful gets its retry one tier up, never on the same model. At the session tier, the work + comes back inline. 3. **Fewer agents.** One reviewer with a clear checklist beats three vague ones. One translator - per language, not two "just in case". No agent whose only job is to double-check another's - conclusion. Mechanical agents run as a pipeline, not as a barrier, unless the next step needs - all their results at once. -4. **Every subagent gets a self-contained brief and a return contract.** It does not see this - conversation, so what is not in the brief does not exist for it. The brief carries six - things: goal, minimum context (paths, decisions already made), global constraints (what - must not be touched, published or run), review focus (what whoever checks the result will - look at), output format and definition of done — a verdict, at most five findings, file - paths instead of pasted contents. If you can't write the brief, the task isn't ready to - delegate. + per language, not two "just in case". No agent whose only job is to re-check another's + conclusion. Hand each mechanical result to the next step as it arrives, unless that step needs + all of them at once. +4. **Every subagent gets a self-contained brief and a return contract.** What is not in the brief + does not exist for it. The brief carries 6 things: goal; minimum context (paths, decisions + already made); constraints (what must not be touched, published or run); review focus (what + the reviewer will check); output format; definition of done. Ask for a verdict, at most 5 + findings, and file paths instead of pasted contents. If you can't write the brief, the task + isn't ready to delegate. 5. **Every loop declares its cap.** Before any loop or review-and-fix round (agents, workflows, - a `/loop`), state the success condition and the maximum number of rounds. Retries have a - fixed cap: one retry of the same approach. A watch loop the user asked for may stay open - with its stop condition. When the cap is reached no new round opens: every open finding is - closed as fixed, parked as debt, or rejected with a reason. The cap goes on the same - dispatch line as the split. + a `/loop`), state the success condition and the maximum number of rounds. At most 1 retry of + the same approach (guardrail 2 says on which model). A watch loop the user asked for may stay open + with its stop condition. At the cap no new round opens: every open finding is fixed, parked as + debt, or rejected with a reason. -## 4. Declare before, measure after +## 6. Declare before, measure after -Before launching, one line the user can read: +Before launching, one line the user can read, with the cap on it: ``` -Dispatch: 9 agents · 3 session-tier (design, code, final review) · 1 mid (tests) · 5 small (4 translations, lint + tests run) +Dispatch: 5 agents · 1 mid (tests) · 4 small (translations) · inline: design, code, lint + tests, final review · cap: 1 fix round ``` -When the work is done, report the spend the tool gives back, split between small and large. -If the tool gives no figure, write **"not measured"**. Never invent a saving: without a figure -there is no saving, only the feeling of one. - -## 5. The final review - -The last agent, or you inline, is always judgment-tier and checks that the parts fit together: -every string referenced by the code exists in every locale, permissions match what the code -uses, the tests the mechanical agent ran are the tests that matter, nothing was silently -skipped. This is where the small models' work is verified, once, by someone who can tell. +If nothing is delegated, say that instead: "Dispatch: 0 agents · inline". When the work is done, +report the spend the client gives back, per tier (in Claude Code: the token count each subagent +returns, or `/usage`). With no figure, write **"not measured"**. Never estimate a saving: without +a figure there is no saving, only the feeling of one. -## Worked example: a small Chrome extension - -Prompt: *"Build a small Chrome extension that highlights prices on any page and converts them to -euros. Popup with a currency selector. UI in English, translated into Spanish, French, German and -Italian. Tests for the conversion."* - -| # | Subtask | Tier | Model · effort | -|---|---|---|---| -| 1 | Design: manifest v3, files, permissions, how the content script and the popup talk | judgment | session model · high | -| 2 | Write the code: content script, popup, service worker, conversion module | judgment | session model · high | -| 3 | Write the conversion tests from the spec | bounded analysis | mid · medium | -| 4–7 | Translate `messages.json` into ES, FR, DE, IT (one agent each) | mechanical | small · low | -| 8 | Validate the manifest, run lint and the tests, report the output verbatim | mechanical | small · low | -| 9 | Final review: locales complete, permissions match the code, tests green | judgment | session model · high | +## 7. The final review -Without this rule: 9 agents on the session's model. With it: 3 on the session's model, 1 on the -mid model, 5 on the small one. The extension is the same; the judgment stayed where it matters. +The last step, inline or by one session-tier agent, checks that the parts fit together: every +string the code uses exists in every locale, permissions match what the code calls, the tests +that ran are the ones that matter, nothing was silently skipped. This is where the small models' +work is verified, once, by someone who can tell. -## Where this comes from +## Example -This rule is section 1G of **The Orchestrator**, the free routing card of -[Invokard](https://invokard.web.app). If that card is loaded, the rule already applies and you -do not need this file. This is the standalone version for anyone who only wants the dispatch. +*"A Chrome extension that converts prices to euros, with a currency popup, UI in English plus +Spanish, French, German and Italian, and tests for the conversion."* Design and code are one +chain of judgment: inline. Then the tests (mid) and the 4 translations (small, one per language) +run in parallel. Lint and tests are a command and the final review is judgment: inline. That gives +the dispatch line in section 6. More cases and the model names per client: +https://github.com/Octonove/agent-dispatch/blob/main/examples/decisions.md diff --git a/docs/demo.gif b/docs/demo.gif index 52af314..9d3c362 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/docs/example-chrome-extension.es.png b/docs/example-chrome-extension.es.png index 7e6a772..3eb156b 100644 Binary files a/docs/example-chrome-extension.es.png and b/docs/example-chrome-extension.es.png differ diff --git a/docs/example-chrome-extension.png b/docs/example-chrome-extension.png index 320a08f..19c0942 100644 Binary files a/docs/example-chrome-extension.png and b/docs/example-chrome-extension.png differ diff --git a/es/.claude-plugin/plugin.json b/es/.claude-plugin/plugin.json new file mode 100644 index 0000000..3c6b7a8 --- /dev/null +++ b/es/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "agent-dispatch-es", + "description": "Decide si delegar en subagentes, cuántos y qué modelo y esfuerzo lleva cada uno.", + "author": { + "name": "Octonove", + "url": "https://github.com/Octonove" + }, + "homepage": "https://github.com/Octonove/agent-dispatch", + "repository": "https://github.com/Octonove/agent-dispatch", + "license": "MIT", + "keywords": ["subagentes", "delegación", "modelos", "coste"] +} diff --git a/es/SKILL.md b/es/SKILL.md index 6bda132..7a111a4 100644 --- a/es/SKILL.md +++ b/es/SKILL.md @@ -1,128 +1,114 @@ --- name: agent-dispatch -description: "Regla siempre activa para cualquier cliente que pueda lanzar subagentes (Claude Code, Cursor, Antigravity…). Antes de delegar decide si hay que delegar —por defecto no—, después cuántos empezando por cero, cómo repartir el trabajo y qué modelo y esfuerzo lleva cada subagente: por el tipo de tarea, nunca por la importancia del proyecto. Lo mecánico (traducciones, listar, medir, ejecutar scripts) va a un modelo pequeño; el análisis acotado a uno medio; diseño, código, decisiones y la revisión final se quedan en el modelo de la sesión. Declara el reparto antes de lanzar y reporta el gasto medido después." +description: "Decide si delegar en subagentes (por defecto, no), cuántos (empezando por cero) y qué modelo y esfuerzo lleva cada uno, por el tipo de tarea y nunca por la importancia del proyecto. Úsala antes de lanzar cualquier subagente, agentes en paralelo, agente en segundo plano, flujo o bucle de revisión, o cuando una tarea pueda repartirse. Lo mecánico va a un modelo pequeño, el análisis acotado a uno medio y el juicio se queda en el modelo de la sesión. Declara antes el reparto y el tope de bucle; reporta después el gasto medido." --- # Despacho de agentes — qué modelo corre cada subagente -Trabajas dentro de un cliente que puede lanzar subagentes (las herramientas `Agent` y `Workflow` -de Claude Code, los agentes en segundo plano de Cursor, o similares). Por defecto cada subagente -hereda el modelo que tienes seleccionado para la sesión, así que traducir a cuatro idiomas cuesta -lo mismo por token que una decisión de arquitectura. Esta regla lo arregla. Gobierna una sola -cosa: **el gasto en agentes**. Nunca cambia el modelo de la conversación. +Salvo que se diga otra cosa, un subagente corre en el modelo de la sesión, así que traducir a +cuatro idiomas cuesta lo mismo por token que una decisión de arquitectura. Esta regla gobierna una +sola cosa: **el gasto en agentes**. Nunca cambia el modelo de la conversación. Aplícala, en este +orden, cada vez que un trabajo vaya a salir de la conversación: un subagente, agentes en paralelo, +un flujo, un bucle. -Aplícala cada vez que un trabajo vaya a salir de la conversación. Aplícala en este orden. - -## 0. ¿Hay que delegar? Por defecto, no +## 1. ¿Hay que delegar? Por defecto, no Delega solo si se cumple una de dos: - **partes independientes** que de verdad corren a la vez y ahorran tiempo de reloj; -- **más lectura de la que cabe en un contexto** (docenas de ficheros, transcripciones, - resultados largos). +- **más lectura de la que cabe en un contexto** (docenas de ficheros, transcripciones, logs + largos). Si no, hazlo en línea. Un subagente no ve esta conversación: cuesta su contexto entero más el -briefing que le escribes, y su respuesta hay que verificarla igual. Dos cosas no son nunca razón -para delegar: comprobar lo que un comando comprueba (`curl`, `grep`, leer un fichero), y pedir -una segunda opinión sobre algo que ya está verificado — donde hay evidencia directa, manda la -evidencia. +encargo que le escribes, y su respuesta hay que verificarla igual. Nunca es razón para delegar: +comprobar lo que un comando comprueba (`grep`, `curl`, pasar los tests, leer un fichero), ni una +segunda opinión sobre algo que ya está verificado. Donde hay evidencia directa, manda la evidencia. -## 0b. ¿Cuántos? Empieza por cero +## 2. ¿Cuántos? Empieza por cero -Cero es la respuesta normal: casi todo es trabajo en línea. Uno bien briefeado cubre casi todo lo -que sí merece delegarse. Varios, solo cuando cada uno tiene una parcela que ningún otro cubre, y -la nombras antes de lanzarlos. Si al escribir el reparto dos suenan parecidos, sobra uno. Una -flota no es rigor: es la misma respuesta pagada varias veces. +Cero es la respuesta normal. Un subagente con un buen encargo cubre casi todo lo que merece +delegarse. Varios, solo cuando cada uno tiene una parcela que ningún otro cubre; nombra las +parcelas antes de lanzarlos. Si dos suenan parecidos, sobra uno: una flota es la misma respuesta +pagada varias veces. -## 1. Reparte el trabajo y clasifica cada parte por TIPO +## 3. Clasifica cada subtarea por tipo -Escribe las subtareas. Clasifica cada una por la clase de trabajo que es — **nunca por lo -importante que sea el proyecto**. Tres niveles: +Escribe las subtareas y clasifica cada una por la clase de trabajo, **nunca por lo importante que +sea el proyecto**: | Nivel | Qué es | Ejemplos | |---|---|---| -| **Mecánica** | Sin juicio; el resultado está bien o mal y es fácil de comprobar | traducir cadenas fijas, listar ficheros, contar, medir tamaños, transcribir una salida literal, ejecutar un script ya escrito y reportar lo que imprime, comprobar que un JSON o un manifest parsea, extraer campos con formato fijo, formatear, renombrar | -| **Analítica acotada** | Razonar dentro de un marco claro que ha puesto otro | resumir un módulo, mapear dependencias, escribir tests a partir de una especificación clara, aplicar un cambio mecánico en muchos ficheros, comparar dos versiones de un texto, pasar un checklist | -| **Juicio** | Decidir, diseñar, o cualquier cosa que se entrega sin otra revisión detrás | arquitectura, escribir el código que decide cómo funciona algo, elegir entre opciones, sintetizar un informe, la revisión final que comprueba que todo encaja, lo que se publica o se manda a un cliente | - -## 2. Asigna modelo y esfuerzo por nivel - -| Nivel | Modelo | Esfuerzo | -|---|---|---| -| Mecánica | **pequeño** (p. ej. Haiku) | bajo | -| Analítica acotada | **medio** (p. ej. Sonnet) | medio | -| Juicio | **el modelo de la sesión** (p. ej. Fable 5.1, Opus) | alto | - -Los nombres de modelo son ejemplos: usa los niveles que ofrezca tu cliente. Si dudas entre dos -niveles, el de abajo más una verificación del de arriba sale más barato que el de arriba a ciegas. - -Si tu cliente no deja elegir modelo ni esfuerzo por subagente, no finjas que lo has hecho: aplica -todo lo demás (si delegar, cuántos, en qué orden, briefing, declarar, medir) y dilo en una línea. +| **Mecánica** | Sin juicio; el resultado está bien o mal y es fácil de comprobar | traducir cadenas fijas, listar, contar, extraer campos con formato fijo, formatear, renombrar, recortar un log largo a sus errores | +| **Analítica acotada** | Razonar dentro de un marco que ha puesto otro | resumir un módulo, mapear dependencias, escribir tests a partir de una especificación clara, un cambio mecánico en muchos ficheros, comparar dos versiones, pasar un checklist | +| **Juicio** | Decidir, diseñar, o cualquier cosa que se entrega sin otra revisión detrás | arquitectura, el código que decide cómo funciona algo, elegir entre opciones, sintetizar un informe, la revisión final, lo que se publica o se manda a un cliente | -## 3. Salvaguardas que no se negocian +## 4. Modelo y esfuerzo por nivel -1. **La revisión final y quien sintetiza nunca se abaratan.** El ahorro se toma en lo mecánico, - jamás en lo que evita que un fallo se publique. -2. **Si un agente pequeño falla o devuelve algo dudoso, la tarea sube un nivel.** No se reintenta - en el mismo modelo. +| Nivel | Modelo | Esfuerzo | Llamada a `Agent` en Claude Code | +|---|---|---|---| +| Mecánica | **pequeño** (Haiku) | bajo | `model: "haiku"`, `effort: "low"` | +| Analítica acotada | **medio** (Sonnet) | medio | `model: "sonnet"`, `effort: "medium"` | +| Juicio | **el modelo de la sesión** | el de la sesión | sin `model` ni `effort` (hereda los dos) | + +- En Claude Code, el juicio solo hereda con un tipo de subagente cuya definición no fije `model` + ni `effort`. Si está fijado `CLAUDE_CODE_SUBAGENT_MODEL`, pasa el alias de la sesión + (`model: "opus"` en una sesión Opus). +- Los nombres son ejemplos: usa los niveles que ofrezca tu cliente. Nunca subas por encima del + modelo de la sesión; si la sesión ya corre el modelo medio, la analítica acotada y el juicio se + quedan los dos en él. +- Si dudas entre dos niveles, elige el de abajo. La revisión final (sección 7) lo comprueba. +- Si el cliente no deja fijar el modelo o el esfuerzo por subagente (en Claude Code, `effort` pide + la versión `2.1.292` o posterior), no finjas que lo has hecho: aplica el resto y dilo en la línea + del despacho. + +## 5. Salvaguardas que no se negocian + +1. **La revisión final y quien sintetiza nunca se abaratan.** El ahorro sale de lo mecánico, + jamás de lo que evita que un fallo se publique. +2. **Un fallo sube la tarea de nivel.** Si un agente pequeño o medio falla o devuelve algo dudoso, + su reintento va un nivel más arriba, nunca en el mismo modelo. En el nivel de la sesión, el + trabajo vuelve a hacerse en línea. 3. **Menos agentes.** Un revisor con un checklist claro vale más que tres vagos. Un traductor por idioma, no dos «por si acaso». Ningún agente cuyo único trabajo sea recomprobar la conclusión - de otro. Los agentes mecánicos van en pipeline, no en barrera, salvo que el paso siguiente - necesite todos sus resultados a la vez. -4. **Cada subagente recibe un encargo autocontenido y un contrato de retorno.** No ve esta - conversación, así que lo que no está en el encargo no existe para él. El encargo lleva seis - cosas: objetivo, contexto mínimo (rutas y decisiones ya tomadas), restricciones globales (qué - no tocar, publicar ni ejecutar), foco de revisión (qué mirará quien revise el resultado), - formato de salida y criterio de terminado: un veredicto, como mucho cinco hallazgos, rutas de - fichero en vez de contenidos pegados. Si no sabes escribirlo, la tarea todavía no está lista - para delegarse. + de otro. Pasa cada resultado mecánico al paso siguiente según llega, salvo que ese paso + necesite todos a la vez. +4. **Cada subagente recibe un encargo autocontenido y un contrato de retorno.** Lo que no está en + el encargo no existe para él. El encargo lleva 6 cosas: objetivo; contexto mínimo (rutas y + decisiones ya tomadas); restricciones (qué no tocar, publicar ni ejecutar); foco de revisión + (qué mirará quien revise); formato de salida; criterio de terminado. Pide un veredicto, como + mucho 5 hallazgos y rutas de fichero en vez de contenidos pegados. Si no sabes escribir el + encargo, la tarea todavía no está lista para delegarse. 5. **Todo bucle declara su tope.** Antes de cualquier bucle o ronda de revisión y corrección - (agentes, flujos, un `/loop`), se fijan la condición de éxito y el máximo de rondas. Los - reintentos tienen tope fijo: un reintento del mismo enfoque. Un bucle de vigilancia que pide - el usuario puede quedar abierto con su condición de parada. Al llegar al tope no se abre otra - ronda: cada hallazgo abierto queda corregido, aparcado como deuda o rechazado con motivo. El - tope va en la misma línea del reparto. + (agentes, flujos, un `/loop`), fija la condición de éxito y el máximo de rondas. Como mucho 1 + reintento del mismo enfoque (la salvaguarda 2 dice en qué modelo). Un bucle de vigilancia que pide el + usuario puede quedar abierto con su condición de parada. Al llegar al tope no se abre otra + ronda: cada hallazgo abierto queda corregido, aparcado como deuda o rechazado con motivo. -## 4. Declara antes, mide después +## 6. Declara antes, mide después -Antes de lanzar, una línea que el usuario pueda leer: +Antes de lanzar, una línea que el usuario pueda leer, con el tope incluido: ``` -Despacho: 9 agentes · 3 de sesión (diseño, código, revisión final) · 1 medio (tests) · 5 pequeños (4 traducciones, lint + tests) +Despacho: 5 agentes · 1 medio (tests) · 4 pequeños (traducciones) · en línea: diseño, código, lint + tests, revisión final · tope: 1 ronda de corrección ``` -Al terminar, reporta el gasto que devuelva la herramienta, separando pequeños y grandes. Si la -herramienta no da cifra, escribe **«sin medir»**. Nunca inventes un ahorro: sin cifra no hay -ahorro, solo la sensación de haberlo tenido. - -## 5. La revisión final - -El último agente, o tú en línea, es siempre de nivel juicio y comprueba que las partes encajan: -cada cadena que usa el código existe en todos los idiomas, los permisos coinciden con lo que el -código usa, los tests que corrió el agente mecánico son los que importan, nada se saltó en -silencio. Aquí se verifica, una vez y por alguien que sabe distinguir, el trabajo de los modelos -pequeños. +Si no se delega nada, dilo así: «Despacho: 0 agentes · en línea». Al terminar, reporta el gasto +que devuelva el cliente, por nivel (en Claude Code: el recuento de tokens que devuelve cada +subagente, o `/usage`). Si no hay cifra, escribe **«sin medir»**. Nunca estimes un ahorro: sin +cifra no hay ahorro, solo la sensación de haberlo tenido. -## Ejemplo trabajado: una extensión de Chrome pequeña - -Prompt: *«Haz una extensión de Chrome pequeña que resalte los precios de cualquier página y los -convierta a euros. Popup con selector de moneda. Interfaz en inglés, traducida a español, francés, -alemán e italiano. Tests para la conversión.»* - -| # | Subtarea | Nivel | Modelo · esfuerzo | -|---|---|---|---| -| 1 | Diseño: manifest v3, ficheros, permisos, cómo hablan el content script y el popup | juicio | modelo de sesión · alto | -| 2 | Escribir el código: content script, popup, service worker, módulo de conversión | juicio | modelo de sesión · alto | -| 3 | Escribir los tests de conversión a partir de la especificación | analítica acotada | medio · medio | -| 4–7 | Traducir `messages.json` a ES, FR, DE, IT (un agente por idioma) | mecánica | pequeño · bajo | -| 8 | Validar el manifest, pasar lint y los tests, reportar la salida literal | mecánica | pequeño · bajo | -| 9 | Revisión final: idiomas completos, permisos coherentes con el código, tests en verde | juicio | modelo de sesión · alto | +## 7. La revisión final -Sin esta regla: 9 agentes en el modelo de la sesión. Con ella: 3 en el modelo de la sesión, 1 en -el medio, 5 en el pequeño. La extensión es la misma; el juicio se quedó donde importa. +El último paso, en línea o con un agente de nivel sesión, comprueba que las partes encajan: cada +cadena que usa el código existe en todos los idiomas, los permisos coinciden con lo que llama el +código, los tests que se pasaron son los que importan, nada se saltó en silencio. Aquí se +verifica, una vez y por alguien que sabe distinguir, el trabajo de los modelos pequeños. -## De dónde sale +## Ejemplo -Esta regla es la sección 1G de **El Orquestador**, la carta gratuita de enrutado de -[Invokard](https://invokard.web.app). Si esa carta está cargada, la regla ya aplica y no necesitas -este fichero. Esta es la versión suelta para quien solo quiera el despacho. +*«Una extensión de Chrome que convierta precios a euros, con un popup de moneda, interfaz en +inglés más español, francés, alemán e italiano, y tests para la conversión.»* Diseño y código son +una misma cadena de juicio: en línea. Después, los tests (medio) y las 4 traducciones (pequeño, +una por idioma) corren en paralelo. Lint y tests son un comando y la revisión final es juicio: en +línea. Sale la línea de despacho de la sección 6. Más casos y los nombres de modelo por cliente: +https://github.com/Octonove/agent-dispatch/blob/main/examples/decisions.es.md diff --git a/examples/chrome-extension.es.md b/examples/chrome-extension.es.md index 98fee3a..16efc0d 100644 --- a/examples/chrome-extension.es.md +++ b/examples/chrome-extension.es.md @@ -1,9 +1,9 @@ # Ejemplo trabajado: una extensión de Chrome pequeña, despachada -Es el ejemplo que usa la skill, paso a paso: qué pidió el usuario, cómo se repartió el trabajo, -qué modelo recibió cada subagente y por qué, qué se le dijo a cada uno y qué comprobó la revisión -final. El número de agentes es real para este plan; el gasto en tokens es el que reporte tu -herramienta en tu ejecución: la skill nunca lo rellena por ti. +Es el ejemplo que usa la skill, paso a paso: qué subtareas se quedan en línea y por qué, cuáles +se delegan y a qué modelo, qué se le dice a cada agente delegado y qué comprueba la revisión +final. El número de agentes es el del plan; el gasto en tokens es el que reporte tu cliente en tu +ejecución. La skill nunca lo rellena por ti. ## El prompt @@ -11,82 +11,99 @@ herramienta en tu ejecución: la skill nunca lo rellena por ti. > a euros. Popup con selector de moneda. Interfaz en inglés, traducida a español, francés, alemán > e italiano. Tests para la conversión. -## Paso 0 — ¿hay que delegar? +## Paso 1 — ¿hay que delegar? -Sí, por el primer motivo: hay partes independientes (cuatro traducciones, un fichero de tests, una -pasada de lint y tests) que pueden correr en paralelo y no necesitan ver la conversación. Diseño -y código podrían hacerse en línea; en este plan se delegan porque el usuario quiere medir la -ejecución. +En parte. Diseño y código son una misma cadena: el código depende del diseño y los dos son +juicio, así que se quedan en línea en el modelo de la sesión. Cuando el código existe, hay cinco +partes independientes que pueden correr a la vez: los tests de conversión y las cuatro +traducciones. Lint y tests son un comando, y lo ejecuta la propia sesión: un comando que +comprueba algo nunca es razón para delegar. -## Paso 1 — el reparto, clasificado por tipo +## Paso 2 — ¿cuántos? -| # | Subtarea | Tipo | Por qué ese tipo | -|---|---|---|---| -| 1 | Diseño: manifest v3, estructura de ficheros, permisos (`activeTab`, `storage`), cómo se pasan el content script y el popup la moneda elegida | juicio | todas las decisiones posteriores dependen de esto | -| 2 | Escribir el código: content script (encontrar precios, envolverlos), popup (selector, guarda en `chrome.storage`), service worker, `convert.js` | juicio | código que decide cómo funciona la cosa | -| 3 | Escribir `convert.test.js` a partir de la especificación de `convert.js` (formatos de entrada, redondeo, moneda desconocida) | analítica acotada | el marco está puesto; el trabajo es dentro de él | -| 4–7 | Traducir `_locales/en/messages.json` a ES, FR, DE, IT — un agente por idioma | mecánica | cadenas fijas, resultado fácil de comprobar | -| 8 | Validar `manifest.json`, pasar `npm run lint` y `npm test`, reportar la salida literal | mecánica | ejecutar y reportar, sin juicio | -| 9 | Revisión final: cada clave de `en` existe en los cuatro idiomas, los permisos del manifest coinciden con lo que llama el código, tests en verde, nada saltado | juicio | después de esto se entrega | +Cinco, cada uno con su parcela: un fichero de tests y un idioma por traductor. Ni un segundo +revisor ni un traductor «por si acaso». -## Paso 2 — modelo y esfuerzo por subtarea +## Paso 3 — el reparto, clasificado por tipo -| Subtareas | Modelo · esfuerzo | -|---|---| -| 1, 2, 9 | modelo de la sesión (Fable 5.1 en mi caso) · alto | -| 3 | Sonnet · medio | -| 4, 5, 6, 7, 8 | Haiku · bajo | +| # | Subtarea | Tipo | Dónde corre | Por qué | +|---|---|---|---|---| +| 1 | Diseño: manifest v3, estructura de ficheros, permisos (`activeTab`, `storage`), cómo se pasan el content script y el popup la moneda elegida | juicio | en línea | todas las decisiones posteriores dependen de esto | +| 2 | Código: content script (encontrar precios, envolverlos), popup (selector, guarda en `chrome.storage`), service worker, `convert.js` | juicio | en línea | código que decide cómo funciona la cosa | +| 3 | `test/convert.test.js` a partir de la especificación de `convert.js` | analítica acotada | 1 agente | el marco está puesto; corre mientras corren las traducciones | +| 4–7 | Traducir `_locales/en/messages.json` a ES, FR, DE, IT | mecánica | 4 agentes, uno por idioma | cadenas fijas, resultado fácil de comprobar | +| 8 | Validar `manifest.json`, pasar `npm run lint` y `npm test` | mecánica | en línea | lo comprueba un comando y la salida es corta | +| 9 | Revisión final: idiomas completos, permisos coherentes con el código, tests en verde, nada saltado | juicio | en línea | después de esto se entrega | -## Paso 4 — la declaración, antes de lanzar +## Paso 4 — modelo y esfuerzo + +| Subtareas | Modelo · esfuerzo | Llamada a `Agent` en Claude Code | +|---|---|---| +| 1, 2, 8, 9 | modelo de la sesión, en línea | ninguna | +| 3 | medio · medio | `model: "sonnet"`, `effort: "medium"` | +| 4–7 | pequeño · bajo | `model: "haiku"`, `effort: "low"` | + +## Paso 5 — salvaguardas y tope + +- Una ronda de corrección tras la revisión final. Un hallazgo que siga abierto después se aparca + como deuda o se rechaza con motivo; no se abre una segunda ronda. +- Un traductor que rompe un marcador recibe su único reintento en el modelo medio, no otra pasada + en el pequeño. Si también falla, ese idioma se hace en línea. + +## Paso 6 — la declaración, antes de lanzar ``` -Despacho: 9 agentes · 3 de sesión (diseño, código, revisión final) · 1 medio (tests) · 5 pequeños (4 traducciones, lint + tests) +Despacho: 5 agentes · 1 medio (tests) · 4 pequeños (traducciones) · en línea: diseño, código, lint + tests, revisión final · tope: 1 ronda de corrección ``` -## Qué se le dice a cada agente (briefing + contrato de retorno) +## Qué se le dice a cada agente delegado -Cada subagente arranca con el contexto vacío. Recibe las rutas que necesita, la especificación y -un contrato de retorno. Ejemplos: +Cada subagente arranca con el contexto vacío. Su encargo lleva las seis cosas que pide la skill: +objetivo, contexto, restricciones, foco de revisión, salida y criterio de terminado. -**Traductor (pequeño, ×4)** -> Traduce los valores de `_locales/en/messages.json` al francés. Conserva todas las claves, -> no toques los marcadores como `$AMOUNT$`, mantén la misma forma del JSON. Escribe el resultado -> en `_locales/fr/messages.json`. Devuelve: la ruta escrita, el número de claves y las cadenas de -> las que no estés seguro (máximo 5). +**Traductor (pequeño, ×4; se muestra el francés)** -**Lint + tests (pequeño)** -> Ejecuta `npm run lint` y `npm test` en la raíz del repo. No arregles nada. Devuelve: códigos de -> salida, las últimas 30 líneas de cada salida tal cual, y la ruta de cualquier fichero que las -> herramientas señalen. +> - **Objetivo:** traduce los valores de `_locales/en/messages.json` al francés. +> - **Contexto:** cadenas de la interfaz de una extensión de Chrome; el código las lee por clave, y +> los marcadores como `$AMOUNT$` se rellenan al ejecutar. +> - **Restricciones:** no cambies claves, marcadores ni la forma del JSON; escribe solo +> `_locales/fr/messages.json`. +> - **Foco de revisión:** quien revise comparará el conjunto de claves y los marcadores con `en`. +> - **Salida:** un veredicto (hecho o bloqueado), la ruta escrita, el número de claves y como +> mucho 5 cadenas de las que no estés seguro. +> - **Terminado:** el fichero parsea como JSON y tiene exactamente las claves de `en`. **Tests desde la especificación (medio)** -> `src/convert.js` exporta `convert(amount, from, to, rates)`. Especificación: los importes pueden -> llevar separador de miles y coma o punto decimal; el resultado se redondea a 2 decimales; una -> moneda desconocida lanza error. Escribe `test/convert.test.js` cubriendo cada regla y dos casos -> límite. Devuelve: la ruta y la lista de casos, uno por línea. - -**Revisión final (modelo de sesión)** -> Comprueba que la extensión encaja: cada clave de `_locales/en/messages.json` existe en `es`, -> `fr`, `de`, `it`; cada permiso de `manifest.json` lo usa el código y nada de lo que llama el -> código se queda sin permiso; los tests que pasó el agente de lint son los de `test/`; reporta -> cualquier cosa saltada. Devuelve: VEREDICTO pasa/no pasa, como mucho 5 hallazgos con fichero y -> línea, una acción SIGUIENTE. - -## Paso 5 — la revisión final -El revisor es el único agente que ve el conjunto a la vez. En este plan es donde se verifica el -trabajo de los cinco agentes pequeños, por alguien que distingue una clave que falta de una mala -traducción. Nunca se abarata: el ahorro está en los traductores y en el que pasa los tests, no -aquí. +> - **Objetivo:** escribe `test/convert.test.js` para `src/convert.js`. +> - **Contexto:** `convert(amount, from, to, rates)`. Los importes pueden llevar separador de miles +> y coma o punto decimal; el resultado se redondea a 2 decimales; una moneda desconocida lanza +> error. +> - **Restricciones:** no edites `src/` ni añadas dependencias. +> - **Foco de revisión:** un test por regla más dos casos límite; ningún test que solo repita el +> código. +> - **Salida:** un veredicto (todo pasa, o cuántos fallan), la ruta y la lista de casos, uno por +> línea; como mucho 5 tests que fallan, con fichero y línea. +> - **Terminado:** `npm test` ejecuta el fichero; los tests que fallen se reportan, no se arreglan. + +## Paso 7 — la revisión final, en línea + +Cada clave de `_locales/en/messages.json` existe en `es`, `fr`, `de` e `it` con los mismos +marcadores; cada permiso de `manifest.json` lo usa el código y nada de lo que llama el código se +queda sin permiso; los tests que se pasaron son los de `test/`; nada se saltó. El resultado es un +veredicto (pasa o no pasa), como mucho 5 hallazgos con fichero y línea, y una acción siguiente. +Aquí se verifica, una vez y por alguien que distingue una clave que falta de una mala traducción, +el trabajo de los cinco agentes. ## Después de la ejecución — medir -Reporta el gasto que devuelva la herramienta, separando pequeños y grandes. Si no da ninguno, -escribe «sin medir». La línea de la skill al final de una ejecución es así: +Reporta el gasto por nivel tal como lo devuelva el cliente. En Claude Code, el resultado de cada +subagente trae su recuento de tokens, y `/usage` muestra el uso por modelo. Si no hay cifra, +escribe «sin medir»: ``` -Gasto: 5 pequeños · 1 medio · 3 de sesión — cifras tal como las reporta la herramienta, o «sin medir» +Gasto: 1 medio · 4 pequeños — tokens tal como los reporta el cliente, o «sin medir» ``` -Sin la skill, los mismos nueve agentes corren en el modelo de la sesión. Con ella, cinco no. La -extensión es la misma. +El plan no promete ningún ahorro. La línea de después de la ejecución es la única cifra que +cuenta. diff --git a/examples/chrome-extension.md b/examples/chrome-extension.md index e3d54cc..e00cb8a 100644 --- a/examples/chrome-extension.md +++ b/examples/chrome-extension.md @@ -1,9 +1,9 @@ # Worked example: a small Chrome extension, dispatched -This is the example the skill uses, step by step: what the user asked, how the work was split, -which model each subagent got and why, what each one was told, and what the final review -checked. The numbers of agents are real for this plan; the token spend is whatever your tool -reports for your run — the skill never fills that in for you. +This is the example the skill uses, step by step: which subtasks stay inline and why, which ones +are delegated and to which model, what each delegated agent is told, and what the final review +checks. The number of agents is the plan's; the token spend is whatever your client reports for +your run. The skill never fills it in for you. ## The prompt @@ -11,80 +11,96 @@ reports for your run — the skill never fills that in for you. > Popup with a currency selector. UI in English, translated into Spanish, French, German and > Italian. Tests for the conversion. -## Step 0 — delegate at all? +## Step 1 — delegate at all? -Yes, on the first criterion: there are independent parts (four translations, a test file, a -lint/test run) that can run in parallel and do not need to see the conversation. Design and code -could be done inline; in this plan they are delegated because the user wants the run measured. +Partly. Design and code are one chain: the code depends on the design and both are judgment, so +they stay inline on the session's model. Once the code exists, five parts are independent and can +run at the same time: the conversion tests and the four translations. Lint and tests are a +command, and the session runs it itself: a command that checks something is never a reason to +delegate. -## Step 1 — the split, classified by type +## Step 2 — how many? -| # | Subtask | Type | Why this type | -|---|---|---|---| -| 1 | Design: manifest v3, file layout, permissions (`activeTab`, `storage`), how the content script and the popup exchange the selected currency | judgment | every later decision depends on it | -| 2 | Write the code: content script (find prices, wrap them), popup (selector, saves to `chrome.storage`), service worker, `convert.js` | judgment | code that decides how the thing works | -| 3 | Write `convert.test.js` from the spec of `convert.js` (input formats, rounding, unknown currency) | bounded analysis | the frame is set; the work is inside it | -| 4–7 | Translate `_locales/en/messages.json` into ES, FR, DE, IT — one agent per language | mechanical | fixed strings, output easy to check | -| 8 | Validate `manifest.json`, run `npm run lint` and `npm test`, report the output verbatim | mechanical | run and report, no judgment | -| 9 | Final review: every key in `en` exists in the four locales, permissions in the manifest match what the code calls, tests green, nothing skipped | judgment | it ships after this | +Five, one slice each: one test file and one locale per translator. No second reviewer and no +translator "just in case". -## Step 2 — model and effort per subtask +## Step 3 — the split, classified by type -| Subtasks | Model · effort | -|---|---| -| 1, 2, 9 | session's model (Fable 5.1 in my case) · high | -| 3 | Sonnet · medium | -| 4, 5, 6, 7, 8 | Haiku · low | +| # | Subtask | Type | Where it runs | Why | +|---|---|---|---|---| +| 1 | Design: manifest v3, file layout, permissions (`activeTab`, `storage`), how the content script and the popup share the selected currency | judgment | inline | every later decision depends on it | +| 2 | Code: content script (find prices, wrap them), popup (selector, saves to `chrome.storage`), service worker, `convert.js` | judgment | inline | code that decides how the thing works | +| 3 | `test/convert.test.js` from the spec of `convert.js` | bounded analysis | 1 agent | the frame is set; it runs while the translations run | +| 4–7 | Translate `_locales/en/messages.json` into ES, FR, DE, IT | mechanical | 4 agents, one per language | fixed strings, output easy to check | +| 8 | Validate `manifest.json`, run `npm run lint` and `npm test` | mechanical | inline | a command checks it and the output is short | +| 9 | Final review: locales complete, permissions match the code, tests green, nothing skipped | judgment | inline | it ships after this | -## Step 4 — the declaration, before launching +## Step 4 — model and effort + +| Subtasks | Model · effort | Claude Code `Agent` call | +|---|---|---| +| 1, 2, 8, 9 | session's model, inline | none | +| 3 | mid · medium | `model: "sonnet"`, `effort: "medium"` | +| 4–7 | small · low | `model: "haiku"`, `effort: "low"` | + +## Step 5 — guardrails and cap + +- One fix round after the final review. A finding still open after it is parked as debt or + rejected with a reason; no second round opens. +- A translator that breaks a placeholder gets its one retry on the mid model, not another run on + the small one. If that fails too, the locale is done inline. + +## Step 6 — the declaration, before launching ``` -Dispatch: 9 agents · 3 session-tier (design, code, final review) · 1 mid (tests) · 5 small (4 translations, lint + tests run) +Dispatch: 5 agents · 1 mid (tests) · 4 small (translations) · inline: design, code, lint + tests, final review · cap: 1 fix round ``` -## What each agent is told (briefing + return contract) +## What each delegated agent is told -Every subagent starts with an empty context. It gets the paths it needs, the spec, and a -return contract. Examples: +Every subagent starts with an empty context. Its brief carries the six things the skill asks for: +goal, context, constraints, review focus, output and definition of done. -**Translator (small, ×4)** -> Translate the values of `_locales/en/messages.json` into French. Keep every key, keep -> placeholders like `$AMOUNT$` untouched, keep the same JSON shape. Write the result to -> `_locales/fr/messages.json`. Return: the path you wrote, the number of keys, and any string -> you were not sure about (max 5). +**Translator (small, ×4; French shown)** -**Lint + tests (small)** -> Run `npm run lint` and `npm test` in the repo root. Do not fix anything. Return: exit codes, -> the last 30 lines of each output verbatim, and the path of any file the tools flagged. +> - **Goal:** translate the values of `_locales/en/messages.json` into French. +> - **Context:** UI strings of a Chrome extension; the code reads them by key, and placeholders like +> `$AMOUNT$` are filled at run time. +> - **Constraints:** do not change keys, placeholders or the JSON shape; write only +> `_locales/fr/messages.json`. +> - **Review focus:** the reviewer will compare the key set and the placeholders with `en`. +> - **Output:** a verdict (done or blocked), the path you wrote, the number of keys, and at most 5 +> strings you were unsure about. +> - **Done:** the file parses as JSON and has exactly the keys of `en`. -**Tests from spec (mid)** -> `src/convert.js` exports `convert(amount, from, to, rates)`. Spec: amounts may carry -> thousands separators and either decimal comma or point; result rounded to 2 decimals; unknown -> currency throws. Write `test/convert.test.js` covering each rule and two edge cases. Return: -> the path and the list of cases, one line each. +**Tests from the spec (mid)** -**Final review (session model)** -> Check that the extension fits together: every key in `_locales/en/messages.json` exists in -> `es`, `fr`, `de`, `it`; every permission in `manifest.json` is used by the code and nothing the -> code calls is missing a permission; the tests the lint agent ran are the ones in `test/`; -> report anything skipped. Return: VERDICT pass/fail, at most 5 findings with file and line, -> one NEXT action. +> - **Goal:** write `test/convert.test.js` for `src/convert.js`. +> - **Context:** `convert(amount, from, to, rates)`. Amounts may carry thousands separators and a +> decimal comma or point; the result is rounded to 2 decimals; an unknown currency throws. +> - **Constraints:** do not edit `src/` and do not add dependencies. +> - **Review focus:** one test per rule plus two edge cases; no test that just restates the code. +> - **Output:** a verdict (all pass, or how many fail), the path, and the list of cases, one line +> each; at most 5 failing tests, with file and line. +> - **Done:** `npm test` runs the file; failing tests are reported, not fixed. -## Step 5 — the final review +## Step 7 — the final review, inline -The reviewer is the only agent that sees the whole thing at once. In this plan it is where the -five small agents' work gets verified, by someone who can tell a missing key from a bad -translation. It is never cheapened: the saving is in the translators and the test runner, not -here. +Every key in `_locales/en/messages.json` exists in `es`, `fr`, `de` and `it` with the same +placeholders; every permission in `manifest.json` is used by the code and nothing the code calls +is missing one; the tests that ran are the ones in `test/`; nothing was skipped. The result is a +verdict (pass or fail), at most 5 findings with file and line, and one next action. This is where +the five agents' work is verified, once, by someone who can tell a missing key from a bad +translation. ## After the run — measure -Report the spend the tool gives back, split small vs large. If it gives none, write -"not measured". The skill's own line at the end of a run looks like: +Report the spend per tier as the client gives it back. In Claude Code each subagent's result +carries its token count, and `/usage` shows usage by model. With no figure, write +"not measured": ``` -Spend: 5 small · 1 mid · 3 session-tier — figures as reported by the tool, or "not measured" +Spend: 1 mid · 4 small — tokens as the client reports them, or "not measured" ``` -Without the skill, the same nine agents run on the session's model. With it, five of them do -not. The extension is the same. +The plan promises no saving. The line after the run is the only number that counts. diff --git a/examples/decisions.es.md b/examples/decisions.es.md new file mode 100644 index 0000000..093325c --- /dev/null +++ b/examples/decisions.es.md @@ -0,0 +1,154 @@ +# Decisiones de despacho: si delegar, cuántos, qué modelo + +Casos breves y realistas, cada uno con la decisión, la línea de despacho y la regla en la que se +apoya (los números de sección son los de [`es/SKILL.md`](../es/SKILL.md)). Al final, los nombres +de modelo de cada cliente. + +## Sin delegar + +### 1. Renombrar una función en todo el repositorio + +> Renombra `getUser` a `fetchUser` en todas partes y asegúrate de que no se rompe nada. + +`grep` encuentra cada llamada, la edición se hace en línea y la batería de tests dice si algo se +rompió. Un subagente pagaría un contexto entero por hacer lo que ya hacen dos comandos +(sección 1). + +``` +Despacho: 0 agentes · en línea +``` + +### 2. Una segunda opinión sobre un arreglo que ya pasa + +> El arreglo está hecho y los tests pasan. Que otro agente lo revise. + +La evidencia existe: el test que fallaba ahora pasa y el diff es lo bastante corto para leerlo. +Un segundo agente volvería a deducir lo que el test ya demostró (sección 1, salvaguarda 3). Lee el +diff en línea; si un camino no está cubierto, escribe el test que lo cubra. + +``` +Despacho: 0 agentes · en línea (evidencia: el test que fallaba ahora pasa) +``` + +### 3. ¿Por qué está en rojo la CI? + +> La CI está en rojo en mi pull request. Averigua por qué. + +El log de un job, de unos cientos de líneas, cabe en el contexto. Léelo en línea y vuelve a +ejecutar el comando que falla. Delegar solo empieza a tener sentido cuando el log ya no cabe +(caso 5). + +``` +Despacho: 0 agentes · en línea +``` + +## Un agente + +### 4. Mapear un módulo heredado grande antes de refactorizar + +> Antes de tocar la facturación necesito saber cómo viaja el token de sesión por `billing/` +> (unos 140 ficheros). + +Más lectura de la que cabe en un contexto, así que un agente. Mapear dependencias dentro de un +marco que pones tú es analítica acotada: modelo medio. El diseño del refactor se queda en línea; +es juicio, y necesita el mapa, no los 140 ficheros. + +``` +Despacho: 1 agente · 1 medio (mapa de billing/) · en línea: diseño del refactor, código, revisión final · tope: sin bucle +``` + +El encargo: + +> - **Objetivo:** mapear dónde se crea, se lee, se pasa y se guarda `SessionToken` dentro de +> `billing/`. +> - **Contexto:** los puntos de entrada son `billing/api/*.py`; la clase del token está en +> `billing/auth.py`. +> - **Restricciones:** solo lectura; no edites ficheros ni arranques servicios. +> - **Foco de revisión:** cada salto lleva fichero y línea. +> - **Salida:** un veredicto (mapa completo o no), los saltos como `file:line → file:line`, uno +> por línea, más como mucho 5 sorpresas. +> - **Terminado:** cada referencia a `SessionToken` que encuentra `grep` en `billing/` está en el +> mapa o explicada. + +### 5. Recortar un log enorme a los fallos + +> La ejecución end-to-end de anoche imprimió 40.000 líneas. ¿Qué tests fallaron y por qué? + +Demasiado largo para leerlo en línea, y sacar los fallos no pide juicio: un agente pequeño. +Devuelve cada test que falla con su primera línea de error y la ruta del log. El diagnóstico se +queda en línea. + +``` +Despacho: 1 agente · 1 pequeño (extraer fallos) · en línea: diagnóstico · tope: sin bucle +``` + +## Varios agentes, una parcela cada uno + +### 6. La misma migración en tres paquetes independientes + +> Pasa `packages/web`, `packages/api` y `packages/cli` de `date-lib` v2 a v3 (el codemod está +> escrito) y después decide qué sustituye al `parseLoose` eliminado. + +Los tres paquetes no se importan entre sí: tres parcelas que de verdad corren a la vez. Ejecutar +un codemod escrito y arreglar lo que rompe dentro de un paquete es analítica acotada, así que cada +uno lleva el modelo medio. Qué sustituye a `parseLoose` es juicio: en línea, una vez, después de +los tres informes. + +``` +Despacho: 3 agentes · 3 medios (web, api, cli) · en línea: decisión de parseLoose, revisión final · tope: 1 ronda de corrección por paquete +``` + +### 7. «Tres revisores: seguridad, rendimiento, estilo» + +> Revisa este pull request en seguridad, rendimiento y estilo, con tres revisores. + +Tres revisores del mismo diff no tienen parcelas distintas; releen las mismas líneas (sección 2, +salvaguarda 3). Propón un revisor con un checklist que cubra las tres cosas, o la revisión en +línea si el diff cabe en el contexto. Si el diff es demasiado grande, pártelo por ficheros, no por +puntos de vista. Si el usuario sigue queriendo tres, lanza tres y dilo en la línea de despacho. + +``` +Despacho: 0 agentes · en línea (una revisión, checklist de seguridad + rendimiento + estilo) +``` + +### 8. Una funcionalidad con tests y traducciones + +La extensión de Chrome de [`chrome-extension.es.md`](chrome-extension.es.md): diseño, código, +lint y revisión final en línea; los tests (medio) y cuatro traducciones (pequeño) en paralelo. + +``` +Despacho: 5 agentes · 1 medio (tests) · 4 pequeños (traducciones) · en línea: diseño, código, lint + tests, revisión final · tope: 1 ronda de corrección +``` + +## Cuando algo sale mal + +### 9. Un agente pequeño devuelve algo dudoso + +El traductor de alemán escribe `$BETRAG$` donde `en` tiene `$AMOUNT$`. Su único reintento va al +modelo medio, con el mismo encargo más el hallazgo; no se vuelve a lanzar en el pequeño +(salvaguarda 2). Si el medio también falla, el fichero alemán se hace en línea. + +### 10. La sesión ya corre el modelo medio + +Con Sonnet como modelo de la sesión no hay nada por encima: la analítica acotada y el juicio +corren los dos en Sonnet, y lo mecánico sigue yendo a Haiku (sección 4). La línea de despacho lo +dice. + +``` +Despacho: 3 agentes · 3 pequeños (traducciones) · en línea en el modelo de la sesión (Sonnet): código, revisión · tope: 1 ronda de corrección +``` + +## Nombres de modelo por cliente + +Los niveles son la regla; los nombres cambian según el cliente y la versión. Filas contrastadas +con la documentación de cada fabricante en octubre de 2026. Solo la fila de Claude Code se ha +probado en este repositorio; las demás están sin probar, así que compruébalas con la versión de tu +cliente. + +| Cliente | Pequeño (mecánica) | Medio (analítica acotada) | Juicio | Esfuerzo por subagente | Dónde se ve el gasto | +|---|---|---|---|---|---| +| Claude Code, herramienta `Agent` | `model: "haiku"` | `model: "sonnet"` | sin `model` ni `effort`: hereda los dos de la sesión si la definición del subagente no fija ninguno; si está fijado `CLAUDE_CODE_SUBAGENT_MODEL`, pasa el alias de la sesión, p. ej. `model: "opus"` | `effort` por llamada (`2.1.292`+), o `effort:` en `.claude/agents/*.md` | recuento de tokens en el resultado de cada subagente; `/usage` | +| Cursor | subagente en `.cursor/agents/.md` con `model: fast` o el ID de un modelo pequeño | el mismo fichero, el ID de un modelo medio | `model: inherit` | no documentado | panel de uso, por petición | +| Codex CLI | subagente TOML en `.codex/agents/` con `model` | igual, un modelo medio | sin `model` | `model_reasoning_effort` | sin comprobar | +| Gemini CLI, GitHub Copilot, OpenCode | definición del subagente con un campo `model` | igual, un modelo medio | sin él, o `inherit` | no documentado | sin comprobar | +| Sin modelo por subagente | ninguno | ninguno | el de la sesión | ninguno | aplica las secciones 1, 2 y 5 a 7, y escribe «modelo no seleccionable» en la línea de despacho | diff --git a/examples/decisions.md b/examples/decisions.md new file mode 100644 index 0000000..c882828 --- /dev/null +++ b/examples/decisions.md @@ -0,0 +1,146 @@ +# Dispatch decisions: whether to delegate, how many, which model + +Short, realistic cases, each with the decision, the dispatch line and the rule it rests on +(section numbers refer to [`SKILL.md`](../SKILL.md)). At the end, the model names for each client. + +## No delegation + +### 1. Rename a function across the repository + +> Rename `getUser` to `fetchUser` everywhere and make sure nothing breaks. + +`grep` finds every call site, the edit is inline and the test suite says whether anything broke. +A subagent would pay for a whole context to do what two commands already do (section 1). + +``` +Dispatch: 0 agents · inline +``` + +### 2. A second opinion on a fix that already passes + +> The fix is in and the tests pass. Get another agent to double-check it. + +The evidence exists: the test that failed now passes and the diff is short enough to read. A +second agent would re-derive what the test already proved (section 1, guardrail 3). Read the diff +inline; if a path is not covered, write the test that covers it. + +``` +Dispatch: 0 agents · inline (evidence: the failing test now passes) +``` + +### 3. Why is CI red? + +> CI is red on my pull request. Find out why. + +One job log of a few hundred lines fits in context. Read it inline and rerun the failing command. +Delegation only starts to make sense when the log no longer fits (case 5). + +``` +Dispatch: 0 agents · inline +``` + +## One agent + +### 4. Map a large legacy module before a refactor + +> Before we touch billing, I need to know how the session token flows through `billing/` +> (about 140 files). + +More reading than fits in one context, so one agent. Mapping dependencies inside a frame you set +is bounded analysis: mid model. The refactor design stays inline; it is judgment, and it needs the +map, not the 140 files. + +``` +Dispatch: 1 agent · 1 mid (map billing/) · inline: refactor design, code, final review · cap: no loop +``` + +The brief: + +> - **Goal:** map where `SessionToken` is created, read, passed and stored inside `billing/`. +> - **Context:** entry points are `billing/api/*.py`; the token class is in `billing/auth.py`. +> - **Constraints:** read only; do not edit files or start services. +> - **Review focus:** every hop has a file and a line. +> - **Output:** a verdict (map complete or not), the hops as `file:line → file:line`, one per +> line, plus at most 5 surprises. +> - **Done:** every `SessionToken` reference that `grep` finds in `billing/` is in the map or +> explained. + +### 5. Cut a huge log down to the failures + +> Last night's end-to-end run printed 40,000 lines. Which tests failed, and why? + +Too long to read inline, and pulling the failures out needs no judgment: one small agent. It +returns each failing test with its first error line and the path of the log. The diagnosis stays +inline. + +``` +Dispatch: 1 agent · 1 small (extract failures) · inline: diagnosis · cap: no loop +``` + +## Several agents, one slice each + +### 6. The same migration in three independent packages + +> Move `packages/web`, `packages/api` and `packages/cli` from `date-lib` v2 to v3 (the codemod is +> written), then decide what replaces the removed `parseLoose`. + +The three packages do not import each other: three slices that genuinely run at the same time. +Running a written codemod and fixing its fallout inside one package is bounded analysis, so each +gets the mid model. What replaces `parseLoose` is judgment: inline, once, after the three reports. + +``` +Dispatch: 3 agents · 3 mid (web, api, cli) · inline: parseLoose decision, final review · cap: 1 fix round per package +``` + +### 7. "Three reviewers: security, performance, style" + +> Review this pull request for security, performance and style, with three reviewers. + +Three reviewers of the same diff do not own different slices; they reread the same lines +(section 2, guardrail 3). Propose one reviewer with a checklist that covers the three, or the +review inline if the diff fits in context. If the diff is too big, split it by files, not by +point of view. If the user still wants three, run three and say so in the dispatch line. + +``` +Dispatch: 0 agents · inline (one review, security + performance + style checklist) +``` + +### 8. A feature with tests and translations + +The Chrome extension in [`chrome-extension.md`](chrome-extension.md): design, code, lint and the +final review inline; the tests (mid) and four translations (small) in parallel. + +``` +Dispatch: 5 agents · 1 mid (tests) · 4 small (translations) · inline: design, code, lint + tests, final review · cap: 1 fix round +``` + +## When something goes wrong + +### 9. A small agent returns something doubtful + +The German translator writes `$BETRAG$` where `en` has `$AMOUNT$`. Its one retry goes to the mid +model, with the same brief plus the finding; it is not run again on the small one (guardrail 2). +If the mid model fails too, the German file is done inline. + +### 10. The session already runs the mid model + +With Sonnet as the session's model there is nothing above it: bounded analysis and judgment both +run on Sonnet, and mechanical work still goes to Haiku (section 4). The dispatch line says so. + +``` +Dispatch: 3 agents · 3 small (translations) · inline on the session's model (Sonnet): code, review · cap: 1 fix round +``` + +## Model names per client + +The tiers are the rule; the names change by client and version. Rows checked against each +vendor's documentation in October 2026. Only the Claude Code row was tried in this repository; +the others are untested, so check them against your client's version. + +| Client | Small (mechanical) | Mid (bounded analysis) | Judgment | Effort per subagent | Where the spend shows | +|---|---|---|---|---|---| +| Claude Code, `Agent` tool | `model: "haiku"` | `model: "sonnet"` | omit `model` and `effort`: both inherit the session's if the subagent's definition sets neither; if `CLAUDE_CODE_SUBAGENT_MODEL` is set, pass the session's alias, e.g. `model: "opus"` | `effort` per call (`2.1.292`+), or `effort:` in `.claude/agents/*.md` | token count in each subagent's result; `/usage` | +| Cursor | subagent in `.cursor/agents/.md` with `model: fast` or a small model ID | same file, a mid model ID | `model: inherit` | not documented | usage dashboard, per request | +| Codex CLI | subagent TOML in `.codex/agents/` with `model` | same, a mid model | omit `model` | `model_reasoning_effort` | not checked | +| Gemini CLI, GitHub Copilot, OpenCode | subagent definition with a `model` field | same, a mid model | omit it, or `inherit` | not documented | not checked | +| No per-subagent model | none | none | the session's | none | apply sections 1, 2 and 5 to 7, and write "model not selectable" in the dispatch line | diff --git a/scripts/check.py b/scripts/check.py new file mode 100644 index 0000000..369df99 --- /dev/null +++ b/scripts/check.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +"""Repository checks for agent-dispatch. Run from anywhere: python3 scripts/check.py + +1. Frontmatter: SKILL.md and es/SKILL.md are valid YAML with a valid name and description. +2. EN/ES parity: every English doc has a Spanish twin with the same structure (headings, + tables, lists, code blocks). For SKILL.md also the same numbered headings, inline code + and numbers (in prose and in code blocks) in each section, so a rule changed in one + language fails until the other follows. +3. Links: every relative link, image, anchor and absolute link into this repository resolves + (Markdown, HTML and the URLs inside code blocks, such as the install commands). +4. Length: SKILL.md stays under the word limit, so loading it does not eat the context. +5. Plugin: the Claude Code marketplace and plugin manifests parse, names match, and each + plugin loads its root SKILL.md (no skills/ folder or 'skills' key that would hide it). + +Needs Python 3.9+ and PyYAML. Exits 1 on any error. +""" +from __future__ import annotations + +import json +import re +import sys +from collections import Counter +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parent.parent +REPO = "Octonove/agent-dispatch" + +SKILL_EN = "SKILL.md" +SKILL_ES = "es/SKILL.md" +# Whole file, frontmatter included. Measured on the Spanish version, the longer one. +MAX_SKILL_WORDS = 1300 +# Agent Skills spec (agentskills.io): name and description limits, allowed keys. +NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +MAX_NAME = 64 +MAX_DESCRIPTION = 1024 +ALLOWED_KEYS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} + + +errors: list[str] = [] + + +def error(path: str, msg: str) -> None: + errors.append(f"{path}: {msg}") + + +def read(rel: str) -> str: + return (ROOT / rel).read_text(encoding="utf-8") + + +# --- Markdown parsing ------------------------------------------------------------------ + +FENCE_RE = re.compile(r"^(```|~~~)") +HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$") +TABLE_SEP_RE = re.compile(r"^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$") +NUMBERED_RE = re.compile(r"^\s*\d+[.)]\s+") +BULLET_RE = re.compile(r"^\s*[-*+]\s+") +INLINE_CODE_RE = re.compile(r"(`+)(.+?)\1") +NUMBER_RE = re.compile(r"\d+(?:[.,]\d+)?") + + +def split_frontmatter(text: str) -> tuple[str | None, str]: + if not text.startswith("---\n"): + return None, text + end = text.find("\n---\n", 4) + if end == -1: + return None, text + return text[4:end], text[end + 5:] + + +def sections(body: str) -> list[dict]: + """Split a Markdown body into sections, one per heading, with a structural summary.""" + out: list[dict] = [] + cur = {"heading": "(top)", "level": 0, "lines": [], "code": [], "code_text": []} + in_fence = False + fence_lines = 0 + for line in body.splitlines(): + if FENCE_RE.match(line.strip()): + if in_fence: + cur["code"].append(fence_lines) + in_fence, fence_lines = not in_fence, 0 + continue + if in_fence: + fence_lines += 1 + cur["code_text"].append(line) + continue + m = HEADING_RE.match(line) + if m: + out.append(cur) + cur = {"heading": m.group(2), "level": len(m.group(1)), "lines": [], "code": [], + "code_text": []} + continue + cur["lines"].append(line) + out.append(cur) + for sec in out: + sec["summary"] = summarize(sec) + return out + + +def summarize(sec: dict) -> dict: + tables, rows, cols = [], 0, 0 + numbered = bullets = images = 0 + for line in sec["lines"] + [""]: + s = line.strip() + if s.startswith("|"): + if not TABLE_SEP_RE.match(s): + rows += 1 + cols = max(cols, s.strip("|").count("|") + 1) + continue + if rows: + tables.append((rows, cols)) + rows, cols = 0, 0 + if NUMBERED_RE.match(line): + numbered += 1 + elif BULLET_RE.match(line): + bullets += 1 + images += len(re.findall(r"!\[", line)) + len(re.findall(r" str: + return "\n".join(sec["lines"]) + + +def inline_code(text: str) -> Counter: + return Counter(m.group(2).strip() for m in INLINE_CODE_RE.finditer(text)) + + +def numbers(text: str) -> Counter: + # Numbers outside inline code, digits only: "at most 5 findings", "cap: 1 fix round". + return Counter(NUMBER_RE.findall(INLINE_CODE_RE.sub(" ", text))) + + +# --- 1. Frontmatter -------------------------------------------------------------------- + +def check_frontmatter(rel: str) -> dict | None: + raw, _ = split_frontmatter(read(rel)) + if raw is None: + error(rel, "no YAML frontmatter: the file must start with '---' and close it with '---'") + return None + try: + data = yaml.safe_load(raw) + except yaml.YAMLError as exc: + error(rel, f"frontmatter is not valid YAML: {exc}") + return None + if not isinstance(data, dict): + error(rel, "frontmatter must be a YAML mapping") + return None + unknown = set(data) - ALLOWED_KEYS + if unknown: + error(rel, f"unknown frontmatter keys {sorted(unknown)}; allowed: {sorted(ALLOWED_KEYS)}") + name, desc = data.get("name"), data.get("description") + if not isinstance(name, str) or not NAME_RE.match(name) or len(name) > MAX_NAME: + error(rel, f"name {name!r} must be lowercase letters, digits and single hyphens, " + f"at most {MAX_NAME} characters") + if not isinstance(desc, str) or not desc.strip(): + error(rel, "description is missing or empty") + else: + if len(desc) > MAX_DESCRIPTION: + error(rel, f"description is {len(desc)} characters; the limit is {MAX_DESCRIPTION}") + if "<" in desc or ">" in desc: + error(rel, "description must not contain '<' or '>'") + return data + + +# --- 2. EN/ES parity ------------------------------------------------------------------- + +def doc_pairs() -> list[tuple[str, str]]: + pairs = [(SKILL_EN, SKILL_ES)] + english = [ROOT / "README.md"] + sorted((ROOT / "examples").glob("*.md")) \ + + sorted((ROOT / "docs").glob("*.md")) + for path in english: + if path.name.endswith(".es.md"): + continue + twin = path.with_name(path.name[:-3] + ".es.md") + rel, rel_twin = str(path.relative_to(ROOT)), str(twin.relative_to(ROOT)) + if twin.exists(): + pairs.append((rel, rel_twin)) + else: + error(rel, f"no Spanish twin: create {rel_twin}") + for twin in sorted(ROOT.rglob("*.es.md")): + if ".git" in twin.parts: + continue + en = twin.with_name(twin.name[: -len(".es.md")] + ".md") + if not en.exists(): + error(str(twin.relative_to(ROOT)), f"no English twin: {en.relative_to(ROOT)}") + return pairs + + +def check_parity(en: str, es: str, strict: bool) -> None: + sec_en = sections(split_frontmatter(read(en))[1]) + sec_es = sections(split_frontmatter(read(es))[1]) + if len(sec_en) != len(sec_es): + error(es, f"{len(sec_es) - 1} headings vs {len(sec_en) - 1} in {en}: " + f"headings EN {[s['heading'] for s in sec_en[1:]]} / " + f"ES {[s['heading'] for s in sec_es[1:]]}") + return + for a, b in zip(sec_en, sec_es): + where = f"section '{b['heading']}' (EN '{a['heading']}')" + for key, val in a["summary"].items(): + if b["summary"][key] != val: + error(es, f"{where}: {key} is {b['summary'][key]} vs {val} in {en}") + if not strict: + continue + code_a, code_b = inline_code(prose(a)), inline_code(prose(b)) + if code_a != code_b: + only_en = sorted((code_a - code_b).elements()) + only_es = sorted((code_b - code_a).elements()) + error(es, f"{where}: inline code differs; only in EN {only_en}, only in ES {only_es}") + # Prose and code blocks apart, so a change in one cannot cancel a change in the other. + for what, text_a, text_b in (("numbers", prose(a), prose(b)), + ("numbers in code blocks", "\n".join(a["code_text"]), + "\n".join(b["code_text"]))): + num_a, num_b = numbers(text_a), numbers(text_b) + if num_a != num_b: + error(es, f"{where}: {what} differ; only in EN {sorted((num_a - num_b).elements())}, " + f"only in ES {sorted((num_b - num_a).elements())}") + + +# --- 3. Links -------------------------------------------------------------------------- + +MD_LINK_RE = re.compile(r"!?\[(?:[^\]\[]|\[[^\]]*\])*\]\(\s*]+)>?(?:\s+\"[^\"]*\")?\s*\)") +HTML_LINK_RE = re.compile(r"""(?:href|src)\s*=\s*["']([^"']+)["']""") +REF_LINK_RE = re.compile(r"^\s*\[[^\]]+\]:\s*(\S+)", re.M) +OWN_URL_RE = re.compile( + rf"https://(?:raw\.githubusercontent\.com/{REPO}/(?:main|HEAD)" + rf"|github\.com/{REPO}/(?:blob|tree|raw)/main)/([^\s)\"'>`]+)", re.I) + + +def slug(text: str) -> str: + text = re.sub(r"!?\[([^\]]*)\]\([^)]*\)", r"\1", text) # keep link text only + text = re.sub(r"[^\w\- ]", "", text.strip().lower()) + return text.replace(" ", "-") + + +def anchors(rel: str) -> set[str]: + seen: Counter = Counter() + out = set() + for sec in sections(split_frontmatter(read(rel))[1])[1:]: + base = slug(sec["heading"]) + out.add(base if not seen[base] else f"{base}-{seen[base]}") + seen[base] += 1 + return out + + +def check_target(rel: str, target: str) -> None: + if target.startswith(("http://", "https://", "mailto:")): + return + path, _, anchor = target.partition("#") + dest = (ROOT / rel).parent / path if path else ROOT / rel + try: + dest.resolve().relative_to(ROOT) + except ValueError: + error(rel, f"link leaves the repository: {target}") + return + if not dest.exists(): + error(rel, f"broken link: {target}") + return + if anchor and dest.suffix == ".md" and anchor not in anchors(str(dest.resolve().relative_to(ROOT))): + error(rel, f"broken anchor: {target}") + + +def check_links(rel: str) -> int: + text = read(rel) + count = 0 + for url in OWN_URL_RE.findall(text): # prose and code blocks: install commands too + count += 1 + if not (ROOT / url.split("#")[0].split("?")[0]).exists(): + error(rel, f"link into this repository points to a missing file: {url}") + body = "\n".join(line for sec in sections(text) # headings can hold links too + for line in ([sec["heading"]] if sec["level"] else []) + sec["lines"]) + body = INLINE_CODE_RE.sub(" ", body) + for target in MD_LINK_RE.findall(body) + HTML_LINK_RE.findall(body) + REF_LINK_RE.findall(body): + count += 1 + check_target(rel, target) + return count + + +# --- 5. Plugin manifests --------------------------------------------------------------- + +def load_json(rel: str) -> dict | None: + try: + data = json.loads(read(rel)) + except (OSError, json.JSONDecodeError) as exc: + error(rel, f"not valid JSON: {exc}") + return None + if not isinstance(data, dict): + error(rel, "must be a JSON object") + return None + return data + + +def check_plugins() -> int: + """Each language ships as a plugin whose root holds the skill: Claude Code loads a plugin + with SKILL.md at its root, no skills/ folder and no 'skills' key as a single skill.""" + market_rel = ".claude-plugin/marketplace.json" + if not (ROOT / market_rel).exists(): + error(market_rel, "missing") + return 0 + market = load_json(market_rel) + if market is None: + return 0 + for key in ("name", "owner", "plugins"): + if key not in market: + error(market_rel, f"missing '{key}'") + plugins = market.get("plugins") or [] + shipped = set() + for entry in plugins: + name, source = entry.get("name"), entry.get("source") + if not isinstance(source, str) or not source.startswith("./"): + error(market_rel, f"plugin {name!r}: expected a relative source such as './es'") + continue + base = (ROOT / source).resolve() + if not base.is_dir(): + error(market_rel, f"plugin {name!r}: source {source} is not a folder") + continue + manifest_rel = str((base / ".claude-plugin" / "plugin.json").relative_to(ROOT)) + manifest = load_json(manifest_rel) if (ROOT / manifest_rel).exists() else None + if manifest is None: + error(manifest_rel, f"missing or invalid manifest for plugin {name!r}") + continue + if manifest.get("name") != name: + error(manifest_rel, f"name {manifest.get('name')!r} differs from the marketplace entry {name!r}") + if "skills" in manifest or (base / "skills").exists(): + error(manifest_rel, "a skills/ folder or a 'skills' key stops the root SKILL.md from loading") + skill_rel = str((base / "SKILL.md").relative_to(ROOT)) + if not (base / "SKILL.md").exists(): + error(manifest_rel, f"plugin {name!r} has no SKILL.md at its root") + shipped.add(skill_rel) + for rel in (SKILL_EN, SKILL_ES): + if rel not in shipped: + error(market_rel, f"no plugin ships {rel}") + return len(plugins) + + +# --- main ------------------------------------------------------------------------------ + +def main() -> int: + fm_en, fm_es = check_frontmatter(SKILL_EN), check_frontmatter(SKILL_ES) + if fm_en and fm_es and fm_en.get("name") != fm_es.get("name"): + error(SKILL_ES, f"name {fm_es.get('name')!r} differs from {SKILL_EN} ({fm_en.get('name')!r})") + print("frontmatter SKILL.md, es/SKILL.md") + + pairs = doc_pairs() + for en, es in pairs: + check_parity(en, es, strict=(en == SKILL_EN)) + print(f"parity {len(pairs)} EN/ES pairs: " + ", ".join(en for en, _ in pairs)) + + files = sorted(str(p.relative_to(ROOT)) for p in ROOT.rglob("*.md") if ".git" not in p.parts) + links = sum(check_links(rel) for rel in files) + print(f"links {links} links checked in {len(files)} Markdown files") + + for rel in (SKILL_EN, SKILL_ES): + words = len(read(rel).split()) + if words > MAX_SKILL_WORDS: + error(rel, f"{words} words; the limit is {MAX_SKILL_WORDS}. Move examples out of the skill.") + print(f"length {rel}: {words} words (limit {MAX_SKILL_WORDS})") + + print(f"plugins {check_plugins()} plugins in .claude-plugin/marketplace.json") + + if errors: + print(f"\n{len(errors)} error(s):", file=sys.stderr) + for line in errors: + print(f" - {line}", file=sys.stderr) + return 1 + print("\nAll checks passed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())