August 19, 2026 · 12 min read

I left vibe coding for a graph that actually runs

Grok routes. Other models write and review. A real command says whether the gate passed. That is how I stopped coding with one model.

ShareXLinkedIn
I left vibe coding for a graph that actually runs

This is the writeup I said I would publish.

For months I used Cursor, Claude Code, Codex, Gemini on the CLI, and the same models as VS Code extensions. The sessions that worked were still one model doing the whole job: plan, write, review, announce done. Claude burned its allowance. Kimi’s swarm was better at parallelism, but the same lineage was still talking to itself.

The tree below is OpenCode plus two accounts. Grok 4.6, on a SuperGrok OAuth seat, classifies the request and delegates. Specialists on OpenRouter — DeepSeek, Qwen, Kimi, Gemini, MiniMax, GPT-5.6 Terra — search, implement, review, debug, do the UI, and pick a design without having written any of the options. A plugin runs typecheck, lint, and tests. Nobody gets to APPROVE by sounding sure.

Two seats: SuperGrok classifies, OpenRouter does the volume
The flat seat plans. The metered seat writes.

That is what I meant by leaving vibe coding for something closer to graph engineering. The nodes are agents with different models and permissions. The edges are a router, a persisted GOAL.md, a swarm with disjoint write sets, and a gate whose PASS/FAIL is a real process. The graph can sit on one objective for hours. A single chat with one model cannot, except by hoping the same weights stay honest after they wrote the diff.

You install the program, connect the two accounts, paste nine agents plus a handful of skills, commands and plugins, and you get a chat that classifies, works, and verifies with a command whose output you can quote. There is an optional last section for the same session from a phone, over Safari, through Tailscale.

What this note walks through, and why each piece is there

  1. Install OpenCode and connect two accounts — so the process is not locked to one vendor CLI, and so the expensive seat plans while cheaper models do the volume work.
  2. How chat behaves — so you type a sentence instead of remembering a slash command, and so a rename does not launch a six-agent swarm.
  3. Where the files live — one user directory, every repo. The process is the product; the repo only overrides stack commands.
  4. Nine agents on five lineagesorquestador (Grok 4.6), explorador (DeepSeek Flash, read-only), 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). The model that writes is not the model that approves.
  5. Four commands and six skills/goal, /flujo, /swarm, /verificar, plus matching skills so ordinary chat launches the same machines. Two extra skills: debug and planificar. Commands are shortcuts. Skills are why you do not need them.
  6. Two pluginsgoal-inject keeps the objective alive after compaction. gate runs the repo’s real checks. A prompt that says “tests passed” is not a test run.
  7. A check, then the phone — confirm both auths and the default agent, then optionally open the same web UI from Safari on a tailnet. No Termux. The graph stays on the machine that has the repo.

Paste the blocks in order. Do not invent a tenth agent. Do not put keys in the files.

What you need first

A single-model tool is simpler. It is also the thing I was already doing: one context window, one bill, one set of habits, and a review written by the same weights that produced the diff. OpenCode is the runtime because it speaks several providers in one session and lets you declare agents, skills, and plugins. Claude Code and Codex are excellent at being Claude and Codex. They are not a place to put DeepSeek, Qwen, Kimi, Gemini, and Grok on the same graph.

Two accounts is the cost split. SuperGrok is a flat seat: spend it on the turns that classify, delegate, and escalate, not on grepping the tree. OpenRouter is metered: spend it on the specialists, and swap a model by editing one frontmatter line. Pointing the orchestrator at openrouter/x-ai/grok-4.6 throws that split away — you pay OpenRouter rates for the highest-volume role.

OpenCode. Desktop from opencode.ai/download, or:

npm install -g opencode-ai
opencode --version

A Grok subscription. SuperGrok, or an X plan that includes Grok API access. The orchestrator is xai/grok-4.6. That id only exists after you sign in inside OpenCode:

/connect  →  xAI  →  browser (or the headless code + URL)

Do not point the orchestrator at openrouter/x-ai/grok-4.6. That bills OpenRouter. OAuth uses the subscription allowance.

An OpenRouter key. Everyone else (DeepSeek, Qwen, Kimi, Gemini, MiniMax, GPT-5.6 Terra) is paid per token there. Create a key at openrouter.ai, then:

/connect  →  OpenRouter  →  paste the key

Keep the key out of git.

  • Account: OpenCode · Used for: The program
  • Account: SuperGrok / X + Grok · Used for: Orchestrator, and the backup coder (grok-build-0.1)
  • Account: OpenRouter · Used for: Implementation, search, review, judgment

How it behaves

Direct coding in one agent is: you describe the feature, it edits, it tells you it is fine. The failure mode is always the same. The model that is proud of the patch is the model that grades the patch. Classification never happens, so a crash and a rename and a four-hour objective all get the same treatment.

Here the first output of a turn is a route, not a file edit. A question stops. A bug has to produce a cause before a patch. A long objective is written to disk and driven by flujo. Review is a different model, and gate is a process, not a sentence.

You type a normal sentence. The orchestrator prints ROUTE: … and loads a skill. Slash commands still work; they are shortcuts.

The router turns a sentence into a route
The first output of a turn is a route, not a file.
  • You type something like: a question · It should run: answer only
  • You type something like: a one-file rename · It should run: coder, then revisor
  • You type something like: a crash, a red test, “it’s broken” · It should run: debug, then implementation if the cause is known
  • You type something like: “until the tests pass”, a multi-step objective · It should run: goal, then flujo
  • You type something like: frontend and API together · It should run: swarm, then flujo
  • You type something like: “review the diff” · It should run: verificar only
  • You type something like: “how would you structure this” · It should run: planificar (no writes)
  • You type something like: “add CSV export” · It should run: flujo

The model that writes is not the model that approves. Grok 4.6 does not review Grok Build. DeepSeek does not review DeepSeek.

The writer is not the reviewer
Grok does not review Grok. DeepSeek does not review DeepSeek.

gate is a shell tool: typecheck, lint, tests. Fail means reject. An approval without a green gate is not an approval.

Flow machine: classify, explore, implement, gate
FAIL is reject. No green gate, no approve.

Where the files live

If the process lives inside one repo, the next repo is vibe coding again. The user directory is the graph. A project AGENTS.md can override stack commands; it should not have to reinvent the router.

One user directory. It applies to every repo.

  • 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

Then create each file below.

The prompts I run day to day are in Spanish. The blocks here are the same contracts in English, so you can paste them as-is. There is a Spanish copy of this note.

1. opencode.json

This file is the graph’s wiring, not a preference list. default_agent is what stops OpenCode from being “whatever model you last picked.” The task allowlist is what stops the orchestrator from inventing a tenth specialist when it is stuck. setCacheKey is why Grok can reuse the long prefix instead of re-paying for the same system prompt every turn. small_model is titles — do not spend the orchestrator on naming the tab.

Default model, default agent, who the orchestrator is allowed to spawn.

{
  "$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 is for session titles. setCacheKey keeps Grok on a warm prefix. The task map is a allowlist: the orchestrator cannot invent extra agents.

Plugins need a local package.json next to that file:

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

Run npm install once in ~/.config/opencode.

2. Agents

One agent with one model is a talented intern who also signs their own work. Nine agents is not “more AI.” It is a split of jobs that a single context window keeps mixing: search, write, review, debug, UI, product judgment, and a blind pick.

The useful constraint is lineage. DeepSeek does not review DeepSeek. Grok 4.6 does not review Grok Build. The judge never wrote A, B, or C. If you collapse this to one model with nine prompt files, you have role-play. The review will sound like a review and still share the writer’s blind spots.

The filename is the agent name. coder.md is invoked as coder.

  • File: orquestador.md · Model: Grok 4.6 · Bill: SuperGrok · Role: Routes. Does not implement.
  • File: explorador.md · Model: DeepSeek V4 Flash · Bill: OpenRouter · Role: Read-only codebase search
  • File: coder.md · Model: DeepSeek V4 Pro · Bill: OpenRouter · Role: Almost all implementation
  • File: coder-escalado.md · Model: Grok Build 0.1 · Bill: SuperGrok · Role: Only after two rejects, or a long multi-phase job
  • File: revisor.md · Model: Qwen 3.8 Max · Bill: OpenRouter · Role: Review + gate. No edits
  • File: debugger.md · Model: GPT-5.6 Terra · Bill: OpenRouter · Role: Root cause. No edits
  • File: ui.md · Model: Kimi K3 · Bill: OpenRouter · Role: UI code and screenshots
  • File: ux.md · Model: Gemini 3.7 Flash · Bill: OpenRouter · Role: Whether the flow makes sense
  • File: juez.md · Model: MiniMax M3 · Bill: OpenRouter · Role: Blind pick among design approaches

agents/orquestador.md

Grok 4.6 stays on this seat because routing, synthesis, and “do we escalate” are the expensive decisions. It is forbidden to implement more than two files. That is the whole point of paying for a strong model: it should not spend itself rewriting a component it is about to send to DeepSeek.

---
description: Orchestrator (Grok 4.6). Routes chat to goal/flujo/swarm/verificar/debug/planificar. Does not wait for 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
---

You classify, decide, delegate, and synthesize. You do not implement.

You run as `xai/grok-4.6` via SuperGrok OAuth, not OpenRouter. You can read images. `coder` and `explorador` cannot — never send screenshots to them.

# Router (first thing each turn)

The user does not have to type /goal, /flujo, /swarm, or /verificar. You choose. Load the matching skill with the `skill` tool. Announce one line: `ROUTE: <name>`.

If `.opencode/GOAL.md` exists and the message is not a side question or a cancel, treat it as a continuation (`ROUTE: goal`).

| User intent | ROUTE | Action |
|---|---|---|
| question, explain, what is | chat | Answer. Stop. |
| typo, rename, one mechanical file | coder | coder + revisor |
| bug, crash, 500, red test, broken | debug | Load skill `debug` |
| until, epic, multi-phase, several criteria | goal | Load skill `goal` |
| in parallel, several modules, frontend+backend | swarm | Load `swarm`, then `flujo` |
| review, audit, look at the diff | verificar | Load `verificar`. Do not implement. |
| plan, how would you, architecture | planificar | Load `planificar`. No writes. |
| screenshot, looks wrong, UI | ui | ux and/or ui; implement UI via `flujo` |
| implement, feature, fix, add, refactor | flujo | Load skill `flujo` |

If two readings imply different work, ask one concrete question, then route.

# Goal and swarm

`goal` is the objective. `flujo` is the engine. Swarm is how you parallelize explore/implement — not a new agent.

Write `.opencode/GOAL.md` with OBJECTIVE, CRITERIA, NON-GOALS, SWARM, STATUS, EVIDENCE, REMAINING.

Swarm on if the goal asks, the work is normal/complex, or there are two or more disjoint areas. Off if trivial.
One wave of 2–6 workers. Max 4 `explorador`. Max 3 writers. Never two writers on the same file.
Swarm does not approve.

# State machine (inside `flujo`)

1. Classify size: trivial → implement + revisor. Normal → full flow. Complex/critical (auth, money, multi-tenant, destructive migrations) → flow with a design duel. Bug with no cause → `ROUTE: debug`.
2. Explore via `explorador` (several in parallel if swarm is on). Synthesize before step 3.
3. Duel only if there is a real design choice. Three lineages other than xAI (`revisor`, `ui`, plus `ux` / `debugger` / `coder`). Write your own approach *before* reading theirs. Send A/B/C/D anonymized to `juez`. Accept the winner. `coder-escalado` is the same lab as you — keep it out of the duel.
4. Implement with `coder` (or `ui` for frontend). Always pass: result, out-of-scope, constraints, decisions already made, phases, and **checkable verification** including a green `gate`. Escalate to `coder-escalado` only after two rejects or a long multi-phase job.
5. Audit once on the final diff: `revisor` + `gate` full. Gate fail is reject. A reviewer that errors is not an approve. Max two fix cycles.
6. Report: goal, swarm, files, decisions, literal `gate` output, cost. If gate did not run, write NOT VERIFIED.

# Hard rules

- Do not implement more than two files yourself.
- Do not claim a gate passed unless `gate` ran.
- Do not invent a silent subagent's opinion.
- No commit or push unless asked.
- Default reasoning is high. Use xhigh only for large architecture.

agents/explorador.md

Read-only, cheap, launch several. Grep is not a Grok job. A single agent that “explores” by opening twenty files in the same thread then implements in that same thread has already contaminated the write with a half-read tree. Explorers return paths and say what they did not find. They do not propose the fix.

---
description: Codebase explorer (DeepSeek V4 Flash). Read-only. Cheap. Launch several in parallel.
mode: subagent
model: openrouter/deepseek/deepseek-v4-flash-0731
options:
  reasoningEffort: low
tools:
  gate: false
  skill: false
permission:
  edit: deny
  bash: deny
---

Collect context. Do not design, do not implement.

Locate with glob/grep, then read. Follow real call chains. Note AGENTS.md and local conventions. If the question was aimed at the wrong place, say so.

Return: ANSWER, RELEVANT FILES (path:line), HOW IT FITS, CONVENTIONS, NOT FOUND.

Exact paths. Do not invent. Do not propose the fix.

agents/coder.md

Volume implementation lives here so the orchestrator is not typing the product. DeepSeek V4 Pro is the default writer because it is good enough on ordinary diffs and you can afford to throw a reject away. It must run gate before it reports. A coder that only claims the tests are green is the vibe-coding loop.

---
description: Main implementer (DeepSeek V4 Pro). Objective in, working tree out. Text only, no vision.
mode: subagent
model: openrouter/deepseek/deepseek-v4-pro-0813
color: warning
options:
  reasoningEffort: high
---

You implement everything except frontend (`ui`). Follow AGENTS.md and neighboring style.

A reject doubles the cycle — run `gate` before you report. Two rejects escalate to `coder-escalado`. You cannot read images; return the task if it depends on one.

Trivial: edit and report. Bounded: full method. Long: short phase plan, verify each phase.

Method: read context; test first when the behavior is testable (red → implement → green); run `gate`; report blockers instead of guessing.

Report: WHAT I DID, FILES, GATE (literal or NOT RUN), TESTS, DECISIONS, RISKS.
No commits. Do not silence the typechecker with `any` / `@ts-ignore`.

agents/coder-escalado.md

Grok Build is on the SuperGrok seat, same lab as the orchestrator. Use it after two rejects or on a long multi-phase job — not as the everyday writer, and never in the design duel. If it sat in the duel, Grok would be judging a cousin.

---
description: Long-horizon coder (Grok Build 0.1, SuperGrok quota). Only after two rejects or a multi-phase job. Same lab as the orchestrator — never in the duel.
mode: subagent
model: xai/grok-build-0.1
color: warning
options:
  reasoningEffort: high
---

You get work that already exceeded the main coder. Name the condition (two rejects vs long horizon) in the report; if it was omitted, do the work anyway and say so.

No vision. Run `gate`. You are the last implementer: a third reject stops the run.

Report: WHAT I DID, ESCALATION CONDITION, FILES, GATES, DECISIONS, RISKS.

agents/revisor.md

This is the argument against one-model coding. Qwen 3.8 Max did not write the diff. It cannot edit, so it cannot “fix while reviewing.” Gate FAIL is REJECT, full stop. Style is not a reject. A second reject is how work escalates to Grok Build instead of looping forever in DeepSeek.

---
description: Adversarial diff review (Qwen 3.8 Max). APPROVE/REJECT with evidence. Cannot edit.
mode: subagent
model: openrouter/qwen/qwen3.8-max
color: error
options:
  reasoningEffort: high
permission:
  edit: deny
  bash: allow
---

Default stance: the diff has bugs. You are a different lineage from the writer (DeepSeek or Grok Build) and from the orchestrator (Grok 4.6).

One pass on the **final** diff. Report every finding with severity and confidence. The orchestrator filters.

Method: `git status --short` and `git diff` (staged and untracked). Check the objective, correctness, regressions, completeness, conventions. Security: untrusted input, injection, auth, tenant isolation, secrets, PII, new deps. Run `gate` with `full: true` and paste the output.

Gate FAIL is REJECT. No APPROVE without a green gate you ran. Reject on findings only for CRITICAL/MAJOR at medium or high confidence — not style. A second reject escalates implementation; be specific.

agents/debugger.md

A single agent asked to “fix the 500” will patch the first plausible line. Terra is OpenAI, not xAI, and it cannot edit. Its job is a demonstrated cause. No cause, no flujo. That one rule is most of the difference between debugging and flailing.

---
description: Root-cause debugger (GPT-5.6 Terra). No edits. Only with a reproducible symptom.
mode: subagent
model: openrouter/openai/gpt-5.6-terra
color: error
options:
  reasoningEffort: high
permission:
  edit: deny
  bash: allow
---

Return a demonstrated cause, not a plausible one. Reproduce first. Write hypotheses you can falsify. Do not implement.

The orchestrator is xAI; you are OpenAI. You may propose a *design* in a duel (read-only).

Report: REPRODUCED, HOW, ROOT CAUSE, EVIDENCE, CAUSAL CHAIN, RULED OUT, SUGGESTED FIX, SIDE EFFECTS, UNVERIFIED.

agents/ui.md

Frontend is a different mess than a Prisma migration. Kimi K3 writes the UI and can look at screenshots; DeepSeek cannot. It has no shell, so it cannot pretend the build passed. Empty, loading, and error states are the job — the happy path is what a single agent always ships.

---
description: Frontend implementer and visual critic (Kimi K3). No shell.
mode: subagent
model: openrouter/moonshotai/kimi-k3
color: primary
permission:
  bash: deny
---

Write UI. Fit the existing system. Cover empty/loading/error. Real a11y.

You cannot run the build. Never claim it compiles. Say what should be run.

If asked for a duel approach: structure only, no code.

agents/ux.md

Gemini is here to answer a question the writer will not ask: does this get the person closer to what they came to do? Read-only. If UX can edit, it stops being judgment and becomes another implementer with opinions.

---
description: Product/UX judgment (Gemini 3.7 Flash). Read-only.
mode: subagent
model: openrouter/google/gemini-3.7-flash
options:
  reasoningEffort: high
tools:
  gate: false
  skill: false
permission:
  edit: deny
  bash: deny
---

You do not write code. Does this get the person closer to what they came to do?

Reconstruct the real path, not the happy path. Empty, error, permissions, expired session. Take a position.

agents/juez.md

MiniMax never wrote A, B, or C, and it does not get to invent D. A one-model “compare these approaches” is the same model picking the essay it would have written. Blind scoring is the only reason the duel exists.

---
description: Blind judge of design approaches (MiniMax M3). Read-only. No other role.
mode: subagent
model: openrouter/minimax/minimax-m3
temperature: 0.1
tools:
  gate: false
  skill: false
permission:
  edit: deny
  bash: deny
---

You receive anonymized approaches A/B/C/D. Score them. Pick a winner. Salvage ideas from the losers.
If they share the same false premise, say that first. Do not add a fifth approach.

3. Commands

In a one-agent session, “keep going until the tests pass” is a hope. After compaction, the model forgets the criteria and starts a new feature. /goal writes the objective to disk. /flujo is the engine that walks explore → (maybe duel) → implement → review → gate. /swarm is the Kimi idea — several specialists at once — with a write-set rule so two writers do not share a file. /verificar exists so “look at the diff” cannot turn into more implementation.

They are optional. The chat router covers the same ground. Keep them anyway: a slash command is a way to force the machine when the model misclassifies.

commands/goal.md

---
description: Session objective. Persist it and run /flujo. status / continue / clear / swarm.
agent: orquestador
---

You own a GOAL. You do not implement: you orchestrate and run /flujo.

$ARGUMENTS

First token:
- status — read `.opencode/GOAL.md`, report, do not work
- continue / resume — pick up from REMAINING
- clear — mark cancelled, stop
- swarm — force swarm, then /flujo
- otherwise — new objective

Write `.opencode/GOAL.md` (OBJECTIVE, CRITERIA, NON-GOALS, SWARM, STATUS, EVIDENCE, REMAINING).
Then run the full /flujo machine.
A goal does not authorize commit or push.

commands/flujo.md

---
description: Implementation engine. Invoked by /goal.
agent: orquestador
---

If `.opencode/GOAL.md` exists, that objective wins.

Run the full machine on:

$ARGUMENTS

Classify. Explore (swarm if not trivial). Duel if there is a design choice. Implement with coder/ui. Audit with revisor + gate full. Report literal gate output.
Do not invent a subagent that did not answer.

commands/swarm.md

---
description: /goal with swarm forced.
agent: orquestador
---

$ARGUMENTS

Set SWARM: on in `.opencode/GOAL.md`. One wave of 2–6 specialists, disjoint write sets. Then full /flujo. Do not implement yourself. Do not replace the reviewer.

commands/verificar.md

---
description: Review the working tree. Do not implement.
agent: orquestador
---

Launch `revisor` on git status + git diff, gate full, complete coverage.
Launch `debugger` only if there is a reproducible symptom with no established cause.
A silent reviewer is not an approve.

4. Skills

This is why the LinkedIn version is not “remember to type /flujo.” Claude Code and Codex feel magical because chat just does the thing. Skills are that surface. The description field is the match key — if you strip the trigger phrases, the orchestrator falls back to being a chatbot that writes code.

Commands force a machine. Skills let a sentence launch it. You want both. A graph that only runs when you remember the slash is a graph you will not use.

These are what the orchestrator loads from ordinary chat.

skills/goal/SKILL.md

---
name: goal
description: Long or autonomous objective. Use when the user says goal, objective, until, epic, multi-phase, several criteria, continue the work, goal status, pause, resume, or wants a result that survives more than one turn. Also ship this end-to-end, keep going until tests pass. Do NOT use for a typo, a question, or "review the diff".
---

Do not implement. Orchestrate and load skill `flujo`.

status → read `.opencode/GOAL.md`, report.
continue → resume from REMAINING.
clear → mark cancelled.

New objective: write `.opencode/GOAL.md`, load `flujo`, load `swarm` if there are two or more areas.
Do not mark complete without a green gate (or NOT CONFIGURED stated) and a reviewer APPROVE if code changed.
A goal does not authorize commit or push.

skills/flujo/SKILL.md

---
name: flujo
description: Implementation engine. Use when the user asks to implement, add a feature, fix, refactor, build, change code — any code work that is not review-only or plan-only. Also implement, build, fix, add, ship (when not a long multi-criteria goal). Do NOT use for "review the diff", "how would you", or a question.
---

If `.opencode/GOAL.md` exists, it wins.

Classify. Explore. Duel only if there is a design choice. Implement. Audit with gate full. Report.
Do not invent a missing subagent. No commit or push.

skills/swarm/SKILL.md

---
name: swarm
description: Parallel specialists. Use when the user says swarm, in parallel, several modules, frontend and backend, many files, several areas, or the work splits into disjoint subtasks. Also parallelize, split across modules, frontend+API. Do NOT use for one file, a typo, or a review.
---

2–6 workers. Max 4 explorador. Max 3 writers. Never two writers on the same file.
Route: codebase → explorador; runtime → debugger; user flow → ux; frontend → ui; code → coder.
Swarm does not approve. Continue with skill `flujo`.

skills/verificar/SKILL.md

---
name: verificar
description: Review the working tree without implementing. Use when the user says review, audit, verify, QA, look at the diff, code review, APPROVE/REJECT, is this safe. Do NOT use to implement features or for an abstract plan.
---

Do not implement. Launch revisor + gate full. Debugger only if there is a symptom with no cause.
A failed reviewer is not an approve.

skills/debug/SKILL.md

---
name: debug
description: Root cause before a patch. Use when the user mentions a bug, crash, error, 500, stacktrace, red test, won't start, broken, repro, exception. Also failing test, doesn't boot. Do NOT use for new features or style review.
---

Launch debugger. Wait for a demonstrated cause. Then load skill `flujo`. If it does not reproduce, stop — do not send coder to guess.

skills/planificar/SKILL.md

---
name: planificar
description: Think without editing. Use when the user says plan, how would you, architecture, trade-offs, design, propose approaches, or the path is unclear. Also how would you structure this, don't code yet. Do NOT use if they asked to implement, or for a typo.
---

No writes. explorador if needed. Duel if the decision is real. Deliver the plan. Implement only if the next message asks.

5. Plugins

Prompts are not a runtime. After enough turns, the objective falls out of the context window and the model invents a new one. After a confident paragraph, “the tests passed” is just English. The plugins are the parts of the graph that do not depend on the model being honest.

Files in plugins/ load automatically.

plugins/goal-inject.js

Without this, a four-hour goal is a four-hour conversation that slowly becomes a different project. The file is the source of truth; the plugin puts it back into the system prompt every turn so compaction cannot delete the criteria.

If the repo has .opencode/GOAL.md, inject it into the 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>\nA GOAL is persisted at ${p}. Treat it as the session objective ` +
          `unless the user cancels or asks a side question.\n\n${cuerpo}\n</goal-vivo>`,
      )
    },
  }
}

plugins/gate.js

This is the hard edge. A single agent saying “I ran the tests” is indistinguishable from a single agent that did not. gate spawns the repo’s typecheck, lint, and tests and returns PASS or FAIL. The reviewer is not allowed to APPROVE without quoting a green full run. The session is marked dirty on every edit until that happens.

Runs real commands. Nobody can fake PASS. Copy the file from the appendix.

Per repo, optional but better than autodetection:

.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 is the package directory if package.json is not at the repo root (web, platform, …). slow: true is skipped on the coder’s fast pass; the reviewer runs it with full: true.

.opencode/.gitignore:

goals/
GOAL.md

6. AGENTS.md

The user file is the contract you see at the top of a session: type normally, here is what happens, Grok does not review Grok, no gate no approve, no commit unless asked. A project file wins for stack and commands so the global graph does not fight pnpm vs npm. If you skip this, you will re-explain the router every week.

User-level. A project AGENTS.md wins for that repo.

# OpenCode

If this repo has its own AGENTS.md, that file wins for stack and commands.

Type normally. The orchestrator routes.

| Intent | Example | Runs |
|---|---|---|
| Question | where is tenant billing | chat |
| Tiny edit | rename X | coder + revisor |
| Bug | login returns 500 | debug |
| Long objective | until billing tests pass | goal → flujo |
| Parallel | frontend and api together | swarm |
| Review | review the diff | verificar |
| Think | how would you structure this | planificar |
| Feature | add CSV export | flujo |

Shortcuts: /goal /flujo /swarm /verificar

Grok does not review or judge its own work.
gate is a fact. No green gate, no APPROVE.
No commit or push unless asked.

Check

The usual silent failure is a beautiful tree pointed at the wrong provider. If xAI is missing, Grok 4.6 is not the orchestrator. If OpenRouter is missing, every specialist 404s and the graph collapses back into one model trying to do the work itself.

opencode auth list

You want xAI oauth and OpenRouter api.

opencode debug config

Look for "model": "xai/grok-4.6" and "default_agent": "orquestador".

opencode run --agent orquestador --auto "Reply with exactly two lines: ROUTE: chat and 2+2=4. No tools."

If that returns, the orchestrator is live.

Restart OpenCode after adding files.

Phone (optional)

A long goal is useless if you have to be at the desk to nudge it. Termux on a phone is a second, worse computer: no repo, no gate, no Tailscale-hosted node. The graph stays on the machine that has the files. The phone is a viewport.

OpenCode does not run on the phone. The machine that has the repo serves the web UI; the phone joins via Tailscale.

$env:Path = "$env:APPDATA\npm;" + $env:Path
$env:OPENCODE_SERVER_PASSWORD = "choose-a-password"
opencode web --port 4096 --hostname 0.0.0.0

On the phone, Tailscale connected, Safari:

http://your-machine.your-tailnet.ts.net:4096

Username opencode. Do not put this on the public internet without a password.

Do not commit

  • ~/.local/share/opencode/auth.json
  • OpenRouter keys
  • The opencode web password

Appendix — 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
ShareXLinkedIn

Comments(0)

Be the first to comment.

I read every comment before it goes up.

Your email stays private. Your IP is kept 90 days for spam control. Privacy.

Keep reading