19 de agosto de 2026 · 13 min de lectura

Dejé el vibe coding por un grafo que sí corre

Grok rutea. Otros modelos escriben y revisan. Un comando de verdad dice si pasó el gate. Así dejé de codear con un solo modelo.

CompartirXLinkedIn
Dejé el vibe coding por un grafo que sí corre

Esta es la nota que dije que iba a publicar.

Durante meses usé Cursor, Claude Code, Codex, Gemini por CLI y los mismos modelos como extensiones de VS Code. Las sesiones que funcionaban seguían siendo un solo modelo haciendo el trabajo entero: planear, escribir, revisar, declarar listo. A Claude se le acababa la cuota. El swarm de Kimi paralelizaba mejor, pero la misma familia se seguía hablando sola.

El árbol de abajo es OpenCode más dos cuentas. Grok 4.6, en un asiento SuperGrok por OAuth, clasifica el pedido y delega. Los especialistas en OpenRouter — DeepSeek, Qwen, Kimi, Gemini, MiniMax, GPT-5.6 Terra — buscan, implementan, revisan, depuran, hacen la UI y eligen un diseño sin haber escrito ninguna de las opciones. Un plugin corre typecheck, lint y tests. Nadie firma APPROVE por sonar seguro.

Dos asientos: SuperGrok clasifica, OpenRouter hace el volumen
El asiento fijo planifica. El medido escribe.

Eso es lo que quise decir con dejar el vibe coding por algo más cercano a ingeniería de grafos. Los nodos son agentes con modelos y permisos distintos. Las aristas son un router, un GOAL.md persistido, un swarm con sets de escritura disjuntos, y un gate cuyo PASS/FAIL es un proceso real. El grafo puede quedarse en un objetivo durante horas. Un solo chat con un solo modelo no puede, salvo esperando que los mismos pesos se mantengan honestos después de haber escrito el diff.

Instalas el programa, conectas las dos cuentas, pegas nueve agentes más un puñado de skills, commands y plugins, y te queda un chat que clasifica, trabaja y verifica con un comando cuya salida puedes citar. Hay una última sección opcional para la misma sesión desde el celular, en Safari, por Tailscale.

Qué recorre esta nota, y por qué está cada pieza

  1. Instalar OpenCode y conectar dos cuentas — para que el proceso no quede preso de un solo CLI de vendor, y para que el asiento caro planifique mientras los modelos baratos hacen el volumen.
  2. Cómo se comporta el chat — para escribir una frase en vez de acordarte de un slash command, y para que un rename no lance un swarm de seis.
  3. Dónde viven los archivos — un directorio de usuario, todos los repos. El proceso es el producto; el repo solo pisa comandos del stack.
  4. Nueve agentes en cinco linajesorquestador (Grok 4.6), explorador (DeepSeek Flash, solo lectura), coder (DeepSeek V4 Pro), coder-escalado (Grok Build), revisor (Qwen 3.8 Max), debugger (GPT-5.6 Terra), ui (Kimi K3), ux (Gemini 3.7 Flash), juez (MiniMax M3). El modelo que escribe no es el que aprueba.
  5. Cuatro commands y seis skills/goal, /flujo, /swarm, /verificar, más los skills equivalentes para que el chat normal lance las mismas máquinas. Dos skills extra: debug y planificar. Los commands son atajos. Los skills son por qué no los necesitas.
  6. Dos pluginsgoal-inject mantiene vivo el objetivo después de la compactación. gate corre los checks reales del repo. Un prompt que dice “pasaron los tests” no es una corrida de tests.
  7. Un check, y después el celular — confirmar ambos auth y el agente por defecto, y opcionalmente abrir la misma UI web desde Safari en el tailnet. Sin Termux. El grafo se queda en la máquina que tiene el repo.

Pega los bloques en orden. No inventes un décimo agente. No pongas claves en los archivos.

Lo que hace falta primero

Una herramienta de un solo modelo es más simple. También es lo que ya venía haciendo: una ventana de contexto, una factura, un set de hábitos, y una review escrita por los mismos pesos que produjeron el diff. OpenCode es el runtime porque habla varios providers en una sesión y deja declarar agentes, skills y plugins. Claude Code y Codex son excelentes siendo Claude y Codex. No son un lugar donde poner a DeepSeek, Qwen, Kimi, Gemini y Grok en el mismo grafo.

Dos cuentas es el split de costo. SuperGrok es un asiento fijo: úsalo en los turnos que clasifican, delegan y escalan, no en hacerle grep al árbol. OpenRouter es por token: úsalo en los especialistas, y cambias un modelo editando una línea de frontmatter. Apuntar el orquestador a openrouter/x-ai/grok-4.6 tira ese split — pagas tarifas de OpenRouter en el rol de más volumen.

OpenCode. Desktop desde opencode.ai/download, o:

npm install -g opencode-ai
opencode --version

Una suscripción de Grok. SuperGrok, o un plan de X que incluya acceso a la API de Grok. El orquestador es xai/grok-4.6. Ese id solo existe después de iniciar sesión adentro de OpenCode:

/connect  →  xAI  →  browser (o el código + URL si estás en headless)

No apuntes el orquestador a openrouter/x-ai/grok-4.6. Eso factura OpenRouter. El OAuth usa la cuota de la suscripción.

Una key de OpenRouter. Todo lo demás (DeepSeek, Qwen, Kimi, Gemini, MiniMax, GPT-5.6 Terra) se paga por token ahí. Crea una key en openrouter.ai, después:

/connect  →  OpenRouter  →  pega la key

La key no va a git.

  • Cuenta: OpenCode · Para qué: El programa
  • Cuenta: SuperGrok / X + Grok · Para qué: Orquestador, y el coder de respaldo (grok-build-0.1)
  • Cuenta: OpenRouter · Para qué: Implementación, búsqueda, review, juicio

Cómo se comporta

Codear directo en un agente es: describes la feature, edita, te dice que está bien. El modo de fallo es siempre el mismo. El modelo que está orgulloso del parche es el que lo califica. Nunca hay clasificación, así que un crash, un rename y un objetivo de cuatro horas reciben el mismo trato.

Acá la primera salida de un turno es una ruta, no una edición de archivo. Una pregunta se detiene. Un bug tiene que producir una causa antes del parche. Un objetivo largo se escribe a disco y lo maneja flujo. La review es otro modelo, y gate es un proceso, no una oración.

Escribes una frase normal. El orquestador imprime RUTA: … y carga un skill. Los slash commands siguen andando; son atajos.

El router convierte una frase en una ruta
La primera salida de un turno es una ruta, no un archivo.
  • Escribes algo como: una pregunta · Debería correr: solo responder
  • Escribes algo como: un rename de un archivo · Debería correr: coder, después revisor
  • Escribes algo como: un crash, un test rojo, “está roto” · Debería correr: debug, después implementación si la causa está clara
  • Escribes algo como: “hasta que pasen los tests”, un objetivo de varios pasos · Debería correr: goal, después flujo
  • Escribes algo como: frontend y API juntos · Debería correr: swarm, después flujo
  • Escribes algo como: “revisa el diff” · Debería correr: solo verificar
  • Escribes algo como: “cómo lo estructurarías” · Debería correr: planificar (sin writes)
  • Escribes algo como: “agrega export a CSV” · Debería correr: flujo

El modelo que escribe no es el que aprueba. Grok 4.6 no revisa a Grok Build. DeepSeek no revisa a DeepSeek.

El que escribe no es el que revisa
Grok no revisa a Grok. DeepSeek no revisa a DeepSeek.

gate es una herramienta de shell: typecheck, lint, tests. Fail es reject. Un approve sin un gate en verde no es un approve.

Máquina de flujo: clasifica, explora, implementa, gate
FALLA es reject. Sin gate en verde no hay approve.

Dónde viven los archivos

Si el proceso vive adentro de un repo, el repo siguiente es vibe coding otra vez. El directorio de usuario es el grafo. Un AGENTS.md de proyecto puede pisar comandos del stack; no debería tener que reinventar el router.

Un directorio de usuario. Aplica a todos los repos.

  • OS: Windows · Path: %USERPROFILE%\.config\opencode\
  • OS: macOS / Linux · Path: ~/.config/opencode/
.config/opencode/
  opencode.json
  AGENTS.md
  package.json
  agents/
  commands/
  skills/{goal,flujo,swarm,verificar,debug,planificar}/
  plugins/

Windows:

cd $env:USERPROFILE\.config
mkdir opencode\agents, opencode\commands, opencode\plugins -Force
mkdir opencode\skills\goal, opencode\skills\flujo, opencode\skills\swarm, opencode\skills\verificar, opencode\skills\debug, opencode\skills\planificar -Force

Después crea cada archivo de abajo.

Los prompts que corro día a día están en español. Los bloques de acá son esos contratos, listos para pegar. Hay una versión en inglés con los mismos contratos en inglés.

1. opencode.json

Este archivo es el cableado del grafo, no una lista de preferencias. default_agent es lo que impide que OpenCode sea “el modelo que elegiste la última vez”. El allowlist de task es lo que impide que el orquestador invente un décimo especialista cuando se traba. setCacheKey es por qué Grok puede reusar el prefijo largo en vez de volver a pagar el mismo system prompt en cada turno. small_model es para títulos — no gastes el orquestador en nombrar la pestaña.

Modelo por defecto, agente por defecto, a quién puede spawnar el orquestador.

{
  "$schema": "https://opencode.ai/config.json",
  "model": "xai/grok-4.6",
  "small_model": "openrouter/qwen/qwen3.7-flash",
  "default_agent": "orquestador",
  "provider": {
    "xai": {
      "options": {
        "setCacheKey": true
      }
    }
  },
  "agent": {
    "plan": {
      "model": "xai/grok-4.6",
      "options": {
        "reasoningEffort": "high"
      },
      "permission": {
        "edit": "deny"
      }
    },
    "explore": {
      "model": "openrouter/deepseek/deepseek-v4-flash-0731",
      "options": {
        "reasoningEffort": "low"
      }
    },
    "orquestador": {
      "permission": {
        "skill": "allow",
        "task": {
          "*": "deny",
          "explorador": "allow",
          "coder": "allow",
          "coder-escalado": "allow",
          "revisor": "allow",
          "debugger": "allow",
          "ui": "allow",
          "ux": "allow",
          "juez": "allow"
        }
      }
    }
  }
}

small_model es para títulos de sesión. setCacheKey deja a Grok con un prefijo caliente. El mapa de task es un allowlist: el orquestador no puede inventar agentes extra.

Los plugins necesitan un package.json local al lado de ese archivo:

{
  "dependencies": {
    "@opencode-ai/plugin": "1.18.15"
  },
  "type": "module"
}

Corres npm install una vez en ~/.config/opencode.

2. Agentes

Un agente con un modelo es un interno talentoso que además firma su propio trabajo. Nueve agentes no es “más IA”. Es un split de trabajos que una sola ventana de contexto mezcla todo el tiempo: buscar, escribir, revisar, debuggear, UI, juicio de producto, y una elección a ciegas.

La restricción que sirve es el linaje. DeepSeek no revisa a DeepSeek. Grok 4.6 no revisa a Grok Build. El juez nunca escribió A, B ni C. Si colapsas esto a un modelo con nueve archivos de prompt, tienes role-play. La review va a sonar a review y va a compartir los puntos ciegos del que escribió.

El nombre del archivo es el nombre del agente. coder.md se invoca como coder.

  • Archivo: orquestador.md · Modelo: Grok 4.6 · Factura: SuperGrok · Rol: Rutea. No implementa.
  • Archivo: explorador.md · Modelo: DeepSeek V4 Flash · Factura: OpenRouter · Rol: Búsqueda del codebase, solo lectura
  • Archivo: coder.md · Modelo: DeepSeek V4 Pro · Factura: OpenRouter · Rol: Casi toda la implementación
  • Archivo: coder-escalado.md · Modelo: Grok Build 0.1 · Factura: SuperGrok · Rol: Solo tras dos rejects, o un trabajo multi-fase largo
  • Archivo: revisor.md · Modelo: Qwen 3.8 Max · Factura: OpenRouter · Rol: Review + gate. Sin edits
  • Archivo: debugger.md · Modelo: GPT-5.6 Terra · Factura: OpenRouter · Rol: Causa raíz. Sin edits
  • Archivo: ui.md · Modelo: Kimi K3 · Factura: OpenRouter · Rol: Código de UI y capturas
  • Archivo: ux.md · Modelo: Gemini 3.7 Flash · Factura: OpenRouter · Rol: Si el flujo tiene sentido
  • Archivo: juez.md · Modelo: MiniMax M3 · Factura: OpenRouter · Rol: Elección a ciegas entre enfoques de diseño

agents/orquestador.md

Grok 4.6 se queda en este asiento porque ruteo, síntesis y “¿escalamos?” son las decisiones caras. Tiene prohibido implementar más de dos archivos. Ese es el punto de pagar un modelo fuerte: no debería gastarse reescribiendo un componente que le va a mandar a DeepSeek.

---
description: Orquestador (Grok 4.6). Rutea el chat a goal/flujo/swarm/verificar/debug/planificar. No espera slash commands.
mode: primary
model: xai/grok-4.6
color: primary
options:
  reasoningEffort: high
permission:
  skill: allow
  task:
    "*": deny
    explorador: allow
    coder: allow
    coder-escalado: allow
    revisor: allow
    debugger: allow
    ui: allow
    ux: allow
    juez: allow
---

Clasificas, decides, delegas y sintetizas. No implementas.

Corres como `xai/grok-4.6` vía OAuth de SuperGrok, no OpenRouter. Puedes leer imágenes. `coder` y `explorador` no — nunca les mandes capturas.

# Router (lo primero de cada turno)

El usuario no tiene que escribir /goal, /flujo, /swarm ni /verificar. Eliges tú. Carga el skill que corresponda con la herramienta `skill`. Anuncia una línea: `RUTA: <nombre>`.

Si existe `.opencode/GOAL.md` y el mensaje no es una pregunta suelta ni un cancelar, trátalo como continuación (`RUTA: goal`).

| Intención del usuario | RUTA | Acción |
|---|---|---|
| pregunta, explica, qué es | chat | Responde. Parar. |
| typo, rename, un archivo mecánico | coder | coder + revisor |
| bug, crash, 500, test rojo, roto | debug | Cargar skill `debug` |
| hasta que, epic, multi-fase, varios criterios | goal | Cargar skill `goal` |
| en paralelo, varios módulos, frontend+backend | swarm | Cargar `swarm`, después `flujo` |
| review, auditar, mira el diff | verificar | Cargar `verificar`. No implementar. |
| plan, cómo lo harías, arquitectura | planificar | Cargar `planificar`. Sin writes. |
| captura, se ve mal, UI | ui | ux y/o ui; implementar UI vía `flujo` |
| implementar, feature, fix, agregar, refactor | flujo | Cargar skill `flujo` |

Si dos lecturas implican trabajo distinto, haz una pregunta concreta, después rutea.

# Goal y swarm

`goal` es el objetivo. `flujo` es el motor. Swarm es cómo paralelizas explorar/implementar — no un agente nuevo.

Escribe `.opencode/GOAL.md` con OBJETIVO, CRITERIOS, NO-GOALS, SWARM, ESTADO, EVIDENCIA, QUÉ FALTA.

Swarm on si el goal lo pide, el trabajo es normal/complejo, o hay dos o más áreas disjuntas. Off si es trivial.
Una ola de 2–6 workers. Máximo 4 `explorador`. Máximo 3 writers. Nunca dos writers sobre el mismo archivo.
El swarm no aprueba.

# Máquina de estados (adentro de `flujo`)

1. Clasifica tamaño: trivial → implementar + revisor. Normal → flujo completo. Compleja/crítica (auth, plata, multi-tenant, migraciones destructivas) → flujo con duelo de diseño. Bug sin causa → `RUTA: debug`.
2. Explora vía `explorador` (varios en paralelo si swarm está on). Sintetiza antes del paso 3.
3. Duelo solo si hay una decisión de diseño real. Tres linajes que no sean xAI (`revisor`, `ui`, más `ux` / `debugger` / `coder`). Escribe tu propio enfoque *antes* de leer los de ellos. Manda A/B/C/D anonimizados a `juez`. Acepta al ganador. `coder-escalado` es el mismo lab que tú — déjalo afuera del duelo.
4. Implementa con `coder` (o `ui` para frontend). Siempre pasa: resultado, fuera de alcance, restricciones, decisiones ya tomadas, fases, y **verificación comprobable** incluyendo un `gate` en verde. Escala a `coder-escalado` solo tras dos rejects o un trabajo multi-fase largo.
5. Audita una vez sobre el diff final: `revisor` + `gate` full. Gate fail es reject. Un revisor que erra no es un approve. Máximo dos ciclos de fix.
6. Reporta: goal, swarm, archivos, decisiones, salida literal de `gate`, costo. Si gate no corrió, escribe NO VERIFICADO.

# Reglas duras

- No implementes más de dos archivos tú.
- No afirmes que un gate pasó salvo que `gate` haya corrido.
- No inventes la opinión de un subagente que no respondió.
- Ni commit ni push salvo que te lo pidan.
- El reasoning por defecto es high. Usa xhigh solo para arquitectura grande.

agents/explorador.md

Solo lectura, barato, lanza varios. Grep no es un trabajo de Grok. Un solo agente que “explora” abriendo veinte archivos en el mismo hilo y después implementa en ese mismo hilo ya contaminó la escritura con un árbol a medio leer. Los exploradores devuelven paths y dicen lo que no encontraron. No proponen el fix.

---
description: Explorador de codebase (DeepSeek V4 Flash). Solo lectura. Barato. Lanzar varios en paralelo.
mode: subagent
model: openrouter/deepseek/deepseek-v4-flash-0731
options:
  reasoningEffort: low
tools:
  gate: false
  skill: false
permission:
  edit: deny
  bash: deny
---

Junta contexto. No diseñes, no implementes.

Localiza con glob/grep, después lee. Sigue cadenas de llamadas reales. Anota AGENTS.md y convenciones locales. Si la pregunta apuntaba al lugar equivocado, dilo.

Devuelve: RESPUESTA, ARCHIVOS RELEVANTES (path:línea), CÓMO ENCAJA, CONVENCIONES, NO ENCONTRADO.

Paths exactos. No inventes. No propongas el fix.

agents/coder.md

La implementación de volumen vive acá para que el orquestador no esté tipeando el producto. DeepSeek V4 Pro es el writer por defecto porque alcanza en diffs ordinarios y te puedes permitir tirar un reject. Tiene que correr gate antes de reportar. Un coder que solo afirma que los tests están verdes es el loop del vibe coding.

---
description: Implementador principal (DeepSeek V4 Pro). Objetivo adentro, working tree afuera. Solo texto, sin visión.
mode: subagent
model: openrouter/deepseek/deepseek-v4-pro-0813
color: warning
options:
  reasoningEffort: high
---

Implementas todo salvo frontend (`ui`). Sigue AGENTS.md y el estilo de al lado.

Un reject duplica el ciclo — corre `gate` antes de reportar. Dos rejects escalan a `coder-escalado`. No podés leer imágenes; devolvé la tarea si depende de una.

Trivial: edita y reporta. Acotada: método completo. Larga: plan corto por fases, verifica cada fase.

Método: lee contexto; test primero cuando el comportamiento sea testeable (rojo → implementar → verde); corre `gate`; reporta bloqueos en vez de adivinar.

Reporte: QUÉ HICE, ARCHIVOS, GATE (literal o NO CORRIÓ), TESTS, DECISIONES, RIESGOS.
Sin commits. No silencies el typechecker con `any` / `@ts-ignore`.

agents/coder-escalado.md

Grok Build está en el asiento SuperGrok, mismo lab que el orquestador. Úsalo después de dos rejects o en un trabajo multi-fase largo — no como writer de todos los días, y nunca en el duelo de diseño. Si se sentara en el duelo, Grok estaría juzgando a un primo.

---
description: Coder de horizonte largo (Grok Build 0.1, cuota SuperGrok). Solo tras dos rejects o un trabajo multi-fase. Mismo lab que el orquestador — nunca en el duelo.
mode: subagent
model: xai/grok-build-0.1
color: warning
options:
  reasoningEffort: high
---

Te toca trabajo que ya excedió al coder principal. Nombra la condición (dos rejects vs horizonte largo) en el reporte; si la omitieron, haz el trabajo igual y dilo.

Sin visión. Corre `gate`. Eres el último implementador: un tercer reject para la corrida.

Reporte: QUÉ HICE, CONDICIÓN DE ESCALADA, ARCHIVOS, GATES, DECISIONES, RIESGOS.

agents/revisor.md

Este es el argumento contra codear con un solo modelo. Qwen 3.8 Max no escribió el diff. No puede editar, así que no puede “arreglar mientras revisa”. Gate FAIL es REJECT, punto. El estilo no es un reject. Un segundo reject es cómo el trabajo escala a Grok Build en vez de dar vueltas para siempre en DeepSeek.

---
description: Review adversarial del diff (Qwen 3.8 Max). APPROVE/REJECT con evidencia. No puede editar.
mode: subagent
model: openrouter/qwen/qwen3.8-max
color: error
options:
  reasoningEffort: high
permission:
  edit: deny
  bash: allow
---

Postura por defecto: el diff tiene bugs. Eres un linaje distinto al writer (DeepSeek o Grok Build) y al orquestador (Grok 4.6).

Un pase sobre el diff **final**. Reporta cada hallazgo con severidad y confianza. El orquestador filtra.

Método: `git status --short` y `git diff` (staged y untracked). Revisa el objetivo, corrección, regresiones, completitud, convenciones. Seguridad: input no confiable, injection, auth, aislamiento de tenant, secretos, PII, deps nuevas. Corre `gate` con `full: true` y pega la salida.

Gate FAIL es REJECT. No hay APPROVE sin un gate en verde que hayas corrido vos. Reject por hallazgos solo en CRITICAL/MAJOR con confianza media o alta — no por estilo. Un segundo reject escala la implementación; sé específico.

agents/debugger.md

Un solo agente al que le pides “arregla el 500” va a parchear la primera línea plausible. Terra es OpenAI, no xAI, y no puede editar. Su trabajo es una causa demostrada. Sin causa, no hay flujo. Esa sola regla es la mayor parte de la diferencia entre debuggear y dar palos de ciego.

---
description: Debugger de causa raíz (GPT-5.6 Terra). Sin edits. Solo con un síntoma reproducible.
mode: subagent
model: openrouter/openai/gpt-5.6-terra
color: error
options:
  reasoningEffort: high
permission:
  edit: deny
  bash: allow
---

Devuelve una causa demostrada, no una plausible. Reproduce primero. Escribe hipótesis que se puedan falsificar. No implementes.

El orquestador es xAI; tú eres OpenAI. Puedes proponer un *diseño* en un duelo (solo lectura).

Reporte: REPRODUCIDO, CÓMO, CAUSA RAÍZ, EVIDENCIA, CADENA CAUSAL, DESCARTADO, FIX SUGERIDO, EFECTOS COLATERALES, NO VERIFICADO.

agents/ui.md

El frontend es un lío distinto a una migración de Prisma. Kimi K3 escribe la UI y puede mirar capturas; DeepSeek no. No tiene shell, así que no puede fingir que el build pasó. Vacío, loading y error son el trabajo — el happy path es lo que un solo agente siempre entrega.

---
description: Implementador de frontend y crítico visual (Kimi K3). Sin shell.
mode: subagent
model: openrouter/moonshotai/kimi-k3
color: primary
permission:
  bash: deny
---

Escribe UI. Encaja en el sistema que ya está. Cubre empty/loading/error. a11y de verdad.

No podés correr el build. Nunca afirmes que compila. Decí qué habría que correr.

Si te piden un enfoque de duelo: solo estructura, sin código.

agents/ux.md

Gemini está acá para responder una pregunta que el writer no se va a hacer: ¿esto acerca a la persona a lo que vino a hacer? Solo lectura. Si UX puede editar, deja de ser juicio y se vuelve otro implementador con opiniones.

---
description: Juicio de producto/UX (Gemini 3.7 Flash). Solo lectura.
mode: subagent
model: openrouter/google/gemini-3.7-flash
options:
  reasoningEffort: high
tools:
  gate: false
  skill: false
permission:
  edit: deny
  bash: deny
---

No escribes código. ¿Esto acerca a la persona a lo que vino a hacer?

Reconstruye el camino real, no el happy path. Vacío, error, permisos, sesión vencida. Toma posición.

agents/juez.md

MiniMax nunca escribió A, B ni C, y no puede inventar D. Un “compara estos enfoques” de un solo modelo es el mismo modelo eligiendo el ensayo que habría escrito. El scoring a ciegas es la única razón por la que existe el duelo.

---
description: Juez ciego de enfoques de diseño (MiniMax M3). Solo lectura. Ningún otro rol.
mode: subagent
model: openrouter/minimax/minimax-m3
temperature: 0.1
tools:
  gate: false
  skill: false
permission:
  edit: deny
  bash: deny
---

Recibes enfoques anonimizados A/B/C/D. Puntúalos. Elige un ganador. Rescata ideas de los perdedores.
Si comparten la misma premisa falsa, dilo primero. No agregues un quinto enfoque.

3. Commands

En una sesión de un solo agente, “sigue hasta que pasen los tests” es una esperanza. Después de la compactación, el modelo olvida los criterios y arranca otra feature. /goal escribe el objetivo a disco. /flujo es el motor que camina explorar → (tal vez duelo) → implementar → review → gate. /swarm es la idea de Kimi — varios especialistas a la vez — con una regla de write-set para que dos writers no compartan un archivo. /verificar existe para que “mira el diff” no se convierta en más implementación.

Son opcionales. El router del chat cubre el mismo terreno. Quédate con ellos igual: un slash command es una forma de forzar la máquina cuando el modelo clasifica mal.

commands/goal.md

---
description: Objetivo de la sesión. Persistirlo y correr /flujo. status / continue / clear / swarm.
agent: orquestador
---

Eres dueño de un GOAL. No implementas: orquestas y corres /flujo.

$ARGUMENTS

Primer token:
- status — lee `.opencode/GOAL.md`, reporta, no trabajes
- continue / resume — retoma desde QUÉ FALTA
- clear — marca cancelado, parar
- swarm — fuerza swarm, después /flujo
- cualquier otra cosa — objetivo nuevo

Escribe `.opencode/GOAL.md` (OBJETIVO, CRITERIOS, NO-GOALS, SWARM, ESTADO, EVIDENCIA, QUÉ FALTA).
Después corre la máquina completa de /flujo.
Un goal no autoriza commit ni push.

commands/flujo.md

---
description: Motor de implementación. Lo invoca /goal.
agent: orquestador
---

Si existe `.opencode/GOAL.md`, ese objetivo gana.

Corre la máquina completa sobre:

$ARGUMENTS

Clasifica. Explora (swarm si no es trivial). Duelo si hay decisión de diseño. Implementa con coder/ui. Audita con revisor + gate full. Reporta la salida literal de gate.
No inventes un subagente que no respondió.

commands/swarm.md

---
description: /goal con swarm forzado.
agent: orquestador
---

$ARGUMENTS

Pon SWARM: on en `.opencode/GOAL.md`. Una ola de 2–6 especialistas, write sets disjuntos. Después /flujo completo. No implementes tú. No reemplaces al revisor.

commands/verificar.md

---
description: Revisar el working tree. No implementar.
agent: orquestador
---

Lanza `revisor` sobre git status + git diff, gate full, cobertura completa.
Lanza `debugger` solo si hay un síntoma reproducible sin causa establecida.
Un revisor que no responde no es un approve.

4. Skills

Por esto la versión de LinkedIn no es “acordarte de tipear /flujo”. Claude Code y Codex se sienten mágicos porque el chat simplemente hace la cosa. Los skills son esa superficie. El campo description es la clave de match — si le sacas las frases gatillo, el orquestador vuelve a ser un chatbot que escribe código.

Los commands fuerzan una máquina. Los skills dejan que una frase la lance. Quieres las dos. Un grafo que solo corre cuando te acuerdas del slash es un grafo que no vas a usar.

Estos son los que el orquestador carga desde el chat normal.

skills/goal/SKILL.md

---
name: goal
description: Objetivo largo o autónomo. Úsalo cuando el usuario diga goal, objetivo, hasta que, epic, multi-fase, varios criterios, continuar el trabajo, status del goal, pause, resume, o quiera un resultado que sobreviva más de un turno. También ship this end-to-end, keep going until tests pass. NO lo uses para un typo, una pregunta, o "revisa el diff".
---

No implementes. Orquesta y carga el skill `flujo`.

status → lee `.opencode/GOAL.md`, reporta.
continue → retoma desde QUÉ FALTA.
clear → marca cancelado.

Objetivo nuevo: escribe `.opencode/GOAL.md`, carga `flujo`, carga `swarm` si hay dos o más áreas.
No marques cumplido sin un gate en verde (o NO CONFIGURADO declarado) y un APPROVE del revisor si cambió código.
Un goal no autoriza commit ni push.

skills/flujo/SKILL.md

---
name: flujo
description: Motor de implementación. Úsalo cuando el usuario pida implementar, agregar una feature, fix, refactor, build, cambiar código — cualquier trabajo de código que no sea solo review o solo plan. También implement, build, fix, add, ship (cuando no sea un goal largo de varios criterios). NO lo uses para "revisa el diff", "cómo lo harías", o una pregunta.
---

Si existe `.opencode/GOAL.md`, gana ese.

Clasifica. Explora. Duelo solo si hay decisión de diseño. Implementa. Audita con gate full. Reporta.
No inventes un subagente que faltó. Ni commit ni push.

skills/swarm/SKILL.md

---
name: swarm
description: Especialistas en paralelo. Úsalo cuando el usuario diga swarm, en paralelo, varios módulos, frontend y backend, muchos archivos, varias áreas, o el trabajo se parte en subtareas disjuntas. También paralelizar, partir en módulos, frontend+API. NO lo uses para un archivo, un typo, o una review.
---

2–6 workers. Máximo 4 explorador. Máximo 3 writers. Nunca dos writers sobre el mismo archivo.
Ruteo: codebase → explorador; runtime → debugger; flujo de usuario → ux; frontend → ui; código → coder.
El swarm no aprueba. Sigue con el skill `flujo`.

skills/verificar/SKILL.md

---
name: verificar
description: Revisar el working tree sin implementar. Úsalo cuando el usuario diga review, auditar, verificar, QA, mira el diff, code review, APPROVE/REJECT, es seguro esto. NO lo uses para implementar features ni para un plan abstracto.
---

No implementes. Lanza revisor + gate full. Debugger solo si hay síntoma sin causa.
Un revisor que falló no es un approve.

skills/debug/SKILL.md

---
name: debug
description: Causa raíz antes del parche. Úsalo cuando el usuario mencione un bug, crash, error, 500, stacktrace, test rojo, no arranca, roto, repro, exception. También test que falla, no bootea. NO lo uses para features nuevas ni review de estilo.
---

Lanza debugger. Espera una causa demostrada. Después carga el skill `flujo`. Si no reproduce, parar — no mandes al coder a adivinar.

skills/planificar/SKILL.md

---
name: planificar
description: Pensar sin editar. Úsalo cuando el usuario diga plan, cómo lo harías, arquitectura, trade-offs, diseño, propón enfoques, o el camino no está claro. También cómo lo estructurarías, no codees todavía. NO lo uses si pidieron implementar, o para un typo.
---

Sin writes. explorador si hace falta. Duelo si la decisión es real. Entrega el plan. Implementa solo si el mensaje siguiente lo pide.

5. Plugins

Los prompts no son un runtime. Después de suficientes turnos, el objetivo se cae de la ventana de contexto y el modelo inventa uno nuevo. Después de un párrafo seguro, “pasaron los tests” es solo castellano. Los plugins son las partes del grafo que no dependen de que el modelo sea honesto.

Los archivos en plugins/ se cargan solos.

plugins/goal-inject.js

Sin esto, un goal de cuatro horas es una conversación de cuatro horas que se va convirtiendo en otro proyecto. El archivo es la fuente de verdad; el plugin lo vuelve a meter en el system prompt en cada turno para que la compactación no borre los criterios.

Si el repo tiene .opencode/GOAL.md, lo inyecta en el system prompt.

import fs from "node:fs"
import path from "node:path"

const MAX = 4000

export const GoalInjectPlugin = async ({ worktree, directory }) => {
  return {
    "experimental.chat.system.transform": async (_input, output) => {
      const raiz = worktree || directory
      if (!raiz) return
      const p = path.join(raiz, ".opencode", "GOAL.md")
      let texto
      try {
        texto = fs.readFileSync(p, "utf8")
      } catch {
        return
      }
      const t = (texto || "").trim()
      if (!t) return
      const cuerpo = t.length > MAX ? t.slice(0, MAX) + "\n[...truncated...]" : t
      output.system.push(
        `<goal-vivo>\nHay un GOAL persistido en ${p}. Tratalo como el objetivo de la sesión ` +
          `salvo que el usuario cancele o haga una pregunta suelta.\n\n${cuerpo}\n</goal-vivo>`,
      )
    },
  }
}

plugins/gate.js

Este es el borde duro. Un solo agente diciendo “corrí los tests” es indistinguible de un solo agente que no los corrió. gate spawnea el typecheck, lint y tests del repo y devuelve PASS o FAIL. El revisor no puede APPROVE sin citar una corrida full en verde. La sesión queda sucia en cada edit hasta que eso pasa.

Corre comandos de verdad. Nadie puede falsificar PASS. Copia el archivo del apéndice.

Por repo, opcional pero mejor que la autodetección:

.opencode/gate.json

{
  "cwd": ".",
  "checks": [
    { "name": "typecheck", "cmd": "pnpm exec tsc --noEmit" },
    { "name": "lint", "cmd": "pnpm lint" },
    { "name": "test", "cmd": "pnpm test", "slow": true }
  ]
}

cwd es el directorio del paquete si package.json no está en la raíz del repo (web, platform, …). slow: true se saltea en el pase rápido del coder; el revisor lo corre con full: true.

.opencode/.gitignore:

goals/
GOAL.md

6. AGENTS.md

El archivo de usuario es el contrato que ves arriba de una sesión: escribe normal, acá es lo que pasa, Grok no revisa a Grok, sin gate no hay approve, sin commit salvo que te lo pidan. Un archivo de proyecto gana en stack y comandos para que el grafo global no pelee pnpm vs npm. Si te lo saltas, vas a volver a explicar el router todas las semanas.

Nivel usuario. Un AGENTS.md de proyecto gana para ese repo.

# OpenCode

Si este repo tiene su propio AGENTS.md, ese archivo gana en stack y comandos.

Escribe normal. El orquestador rutea.

| Intención | Ejemplo | Corre |
|---|---|---|
| Pregunta | dónde se cobra el tenant | chat |
| Edit chico | renombra X | coder + revisor |
| Bug | el login tira 500 | debug |
| Objetivo largo | hasta que pasen los tests de billing | goal → flujo |
| Paralelo | frontend y api juntos | swarm |
| Review | revisa el diff | verificar |
| Pensar | cómo lo estructurarías | planificar |
| Feature | agrega export a CSV | flujo |

Atajos: /goal /flujo /swarm /verificar

Grok no revisa ni juzga su propio trabajo.
gate es un hecho. Sin gate en verde, no hay APPROVE.
Ni commit ni push salvo que te lo pidan.

Check

El fallo silencioso habitual es un árbol lindo apuntado al provider equivocado. Si falta xAI, Grok 4.6 no es el orquestador. Si falta OpenRouter, cada especialista da 404 y el grafo se derrumba otra vez en un solo modelo tratando de hacer el trabajo.

opencode auth list

Quieres xAI oauth y OpenRouter api.

opencode debug config

Busca "model": "xai/grok-4.6" y "default_agent": "orquestador".

opencode run --agent orquestador --auto "Responde con exactamente dos líneas: RUTA: chat y 2+2=4. Sin herramientas."

Si eso vuelve, el orquestador está vivo.

Reinicia OpenCode después de agregar archivos.

Celular (opcional)

Un goal largo no sirve si tienes que estar en el escritorio para empujarlo. Termux en el celular es una segunda computadora, peor: sin repo, sin gate, sin nodo en Tailscale. El grafo se queda en la máquina que tiene los archivos. El celular es un viewport.

OpenCode no corre en el celular. La máquina que tiene el repo sirve la UI web; el celular entra por Tailscale.

$env:Path = "$env:APPDATA\npm;" + $env:Path
$env:OPENCODE_SERVER_PASSWORD = "elegí-una-contraseña"
opencode web --port 4096 --hostname 0.0.0.0

En el celular, Tailscale conectado, Safari:

http://tu-maquina.tu-tailnet.ts.net:4096

Usuario opencode. No pongas esto en internet público sin contraseña.

No commitees

  • ~/.local/share/opencode/auth.json
  • Keys de OpenRouter
  • La contraseña de opencode web

Apéndice — plugins/gate.js

/**
 * Deterministic project gate.
 * Tool `gate`: typecheck/lint/tests → real PASS/FAIL.
 * Marks the session dirty on edit. A full green gate is required to clear it.
 * Config: <repo>/.opencode/gate.json — otherwise autodetect.
 */
import { tool } from "@opencode-ai/plugin"
import { spawn } from "node:child_process"
import fs from "node:fs"
import path from "node:path"

const MAX_SALIDA = 4000
const TIMEOUT_POR_DEFECTO = 300000
const RE_LENTO = /playwright|cypress|detox|supabase db reset|expo |--e2e|\be2e\b|nightly|containers|lighthouse/i
const HERRAMIENTAS_ESCRITURA = new Set(["edit", "write", "patch", "multiedit", "apply_patch"])
const sucias = new Map()

function ejecutar(cmd, cwd, timeoutMs) {
  return new Promise((resolve) => {
    const t0 = Date.now()
    let salida = ""
    let matado = false
    let hijo
    try {
      hijo = spawn(cmd, { cwd, shell: true, windowsHide: true })
    } catch (e) {
      return resolve({ code: -1, salida: `could not start: ${e && e.message}`, ms: 0 })
    }
    const temporizador = setTimeout(() => {
      matado = true
      try { hijo.kill("SIGKILL") } catch {}
    }, timeoutMs)
    const acumula = (d) => { salida += d.toString() }
    hijo.stdout && hijo.stdout.on("data", acumula)
    hijo.stderr && hijo.stderr.on("data", acumula)
    hijo.on("error", (e) => {
      clearTimeout(temporizador)
      resolve({ code: -1, salida: `${salida}\n${e && e.message}`, ms: Date.now() - t0 })
    })
    hijo.on("close", (code) => {
      clearTimeout(temporizador)
      resolve({
        code: matado ? -2 : (code === null ? -1 : code),
        salida,
        ms: Date.now() - t0,
        timeout: matado,
      })
    })
  })
}

function recorta(texto) {
  const t = (texto || "").trim()
  if (t.length <= MAX_SALIDA) return t
  return "[...truncated...]\n" + t.slice(-MAX_SALIDA)
}

function leeJSON(p) {
  try { return JSON.parse(fs.readFileSync(p, "utf8")) } catch { return null }
}

function gestor(dir) {
  const pkg = leeJSON(path.join(dir, "package.json"))
  if (pkg && typeof pkg.packageManager === "string") {
    const n = pkg.packageManager.split("@")[0]
    if (n) return n
  }
  if (fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) return "pnpm"
  if (fs.existsSync(path.join(dir, "yarn.lock"))) return "yarn"
  if (fs.existsSync(path.join(dir, "bun.lockb"))) return "bun"
  return "npm"
}

function localizaPaquete(raiz) {
  if (fs.existsSync(path.join(raiz, "package.json"))) return raiz
  let hijos = []
  try {
    hijos = fs.readdirSync(raiz, { withFileTypes: true })
      .filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "node_modules")
      .map((d) => path.join(raiz, d.name))
      .filter((p) => fs.existsSync(path.join(p, "package.json")))
  } catch {}
  return hijos.length === 1 ? hijos[0] : null
}

function autodetecta(raiz) {
  const dir = localizaPaquete(raiz)
  if (!dir) return null
  const pkg = leeJSON(path.join(dir, "package.json"))
  if (!pkg) return null
  const scripts = pkg.scripts || {}
  const pm = gestor(dir)
  const correr = (s) => `${pm} run ${s}`
  const checks = []
  const primero = (...cands) => cands.find((c) => scripts[c])
  const tc = primero("typecheck", "type-check", "check-types", "tsc")
  if (tc) checks.push({ name: tc, cmd: correr(tc) })
  else if (fs.existsSync(path.join(dir, "tsconfig.json")))
    checks.push({ name: "tsc --noEmit", cmd: `${pm === "npm" ? "npx" : pm + " exec"} tsc --noEmit` })
  const lint = primero("lint", "check")
  if (lint) checks.push({ name: lint, cmd: correr(lint) })
  const test = primero("test:unit", "test")
  if (test) checks.push({ name: test, cmd: correr(test), slow: RE_LENTO.test(scripts[test] || "") })
  return { cwd: path.relative(raiz, dir) || ".", checks, origen: "autodetect" }
}

function resuelveConfig(raiz) {
  const p = path.join(raiz, ".opencode", "gate.json")
  if (fs.existsSync(p)) {
    const cfg = leeJSON(p)
    if (!cfg) return { error: `${p} exists but is not valid JSON` }
    if (!Array.isArray(cfg.checks) || cfg.checks.length === 0)
      return { error: `${p} needs a non-empty checks array` }
    return { ...cfg, cwd: cfg.cwd || ".", origen: ".opencode/gate.json" }
  }
  const auto = autodetecta(raiz)
  if (!auto) return { error: "no package.json and no .opencode/gate.json" }
  return auto
}

export const GatePlugin = async ({ worktree, directory }) => {
  const raizPorDefecto = worktree || directory
  return {
    tool: {
      gate: tool({
        description:
          "Run typecheck, lint, and tests. Returns real PASS/FAIL. Pass full=true for slow checks.",
        args: {
          full: tool.schema.boolean().optional().describe("Include slow checks. Default false."),
          only: tool.schema.string().optional().describe("Only this check, e.g. typecheck."),
        },
        async execute(args, ctx) {
          const raiz = ctx.worktree || ctx.directory || raizPorDefecto
          const cfg = resuelveConfig(raiz)
          if (cfg.error) {
            return [
              "GATE: NOT CONFIGURED",
              `root: ${raiz}`,
              "",
              cfg.error,
              "",
              'Create <root>/.opencode/gate.json: { "cwd": ".", "checks": [ { "name": "test", "cmd": "pnpm test" } ] }',
              "",
              "You cannot report this task as verified.",
            ].join("\n")
          }
          const cwd = path.resolve(raiz, cfg.cwd || ".")
          let checks = cfg.checks
          if (args.only) {
            checks = checks.filter((c) => c.name === args.only)
            if (checks.length === 0)
              return `GATE: '${args.only}' missing. Available: ${cfg.checks.map((c) => c.name).join(", ")}`
          }
          const resultados = []
          for (const c of checks) {
            if (c.slow && !args.full) {
              resultados.push({ name: c.name, estado: "SKIP", nota: "slow; pass full=true" })
              continue
            }
            if (ctx.abort && ctx.abort.aborted) {
              resultados.push({ name: c.name, estado: "SKIP", nota: "aborted" })
              continue
            }
            const r = await ejecutar(c.cmd, cwd, c.timeoutMs || TIMEOUT_POR_DEFECTO)
            resultados.push({
              name: c.name,
              cmd: c.cmd,
              estado: r.timeout ? "TIMEOUT" : r.code === 0 ? "PASS" : "FAIL",
              code: r.code,
              ms: r.ms,
              salida: r.salida,
            })
          }
          const fallos = resultados.filter((r) => r.estado === "FAIL" || r.estado === "TIMEOUT")
          const corridos = resultados.filter((r) => r.estado !== "SKIP")
          const veredicto = corridos.length === 0 ? "NOTHING RAN" : fallos.length ? "FAIL" : "PASS"
          const completo = veredicto === "PASS" && args.full && !args.only
          if (completo) sucias.delete(ctx.sessionID)
          const lineas = [`GATE: ${veredicto}`, `root: ${raiz}  ·  cwd: ${cfg.cwd}  ·  config: ${cfg.origen}`, ""]
          for (const r of resultados) {
            const dur = r.ms == null ? "" : `${(r.ms / 1000).toFixed(1)}s`
            lineas.push(`  ${String(r.name).padEnd(16)} ${r.estado.padEnd(8)} ${dur.padStart(7)}  ${r.nota || r.cmd || ""}`)
          }
          for (const f of fallos) {
            lineas.push("", `--- ${f.name} (exit ${f.code}${f.estado === "TIMEOUT" ? ", TIMEOUT" : ""}) ---`)
            lineas.push(recorta(f.salida) || "(no output)")
          }
          if (veredicto === "FAIL")
            lineas.push("", "REJECT. Do not mark this task done until gate passes.")
          if (veredicto === "NOTHING RAN")
            lineas.push("", "This is not a PASS.")
          if (veredicto === "PASS" && !completo)
            lineas.push("", "PARTIAL PASS. APPROVE requires gate with full=true.")
          return { title: `gate ${veredicto.toLowerCase()}`, output: lineas.join("\n"), metadata: { veredicto } }
        },
      }),
    },
    "tool.execute.after": async (input) => {
      if (!HERRAMIENTAS_ESCRITURA.has(input.tool)) return
      sucias.set(input.sessionID, (sucias.get(input.sessionID) || 0) + 1)
    },
    "experimental.chat.system.transform": async (input, output) => {
      const n = input.sessionID ? sucias.get(input.sessionID) : 0
      if (!n) return
      output.system.push(
        `<gate-pendiente>\nThere are ${n} unverified edit(s). Run \`gate\` and quote its output. A gate that did not run is not a gate that passed.\n</gate-pendiente>`,
      )
    },
  }
}
opencodeaitoolingagents
CompartirXLinkedIn

Comentarios(0)

Sé el primero en comentar.

Leo cada comentario antes de publicarlo.

Tu email es privado. Tu IP se conserva 90 días contra spam. Privacidad.

Sigue leyendo