DEV Community

Cover image for Splitting Planning and Execution in a Claude Code and DeepSeek Pipeline
Heitor Vasconcelos
Heitor Vasconcelos

Posted on

Splitting Planning and Execution in a Claude Code and DeepSeek Pipeline

Somewhere around the third feature request in a row, I noticed I was burning a full Claude Code session on work that did not need it. Adding a pagination parameter to an endpoint. Renaming a serializer field across four files. Tasks with a clear answer and almost no judgment involved, running through the same model I use to decide whether a migration is safe or whether a query pattern will lock a table in production.

That mismatch is what pushed me toward a two model pipeline: Claude Code handles planning and review, where judgment actually matters, and OpenCode running DeepSeek handles the mechanical part of writing the diff. This article walks through the architecture, the one gotcha that will silently break the whole thing, and the review loop that makes the split safe enough to trust.

Why split the roles at all

The instinct is to assume more capable equals better for every step, but planning, implementing, and reviewing are different kinds of work. Planning means reading a codebase and deciding what actually needs to change, which requires holding context and making judgment calls about side effects. Reviewing means catching the same kinds of mistakes: a serializer that silently drops a field, a queryset that N plus ones under load, a migration that is not reversible.

Implementing, once the plan is explicit and unambiguous, is closer to transcription. Take file A, add field B, follow the pattern already used in file C. A weaker or cheaper model can do this reliably as long as the instructions leave nothing to interpretation. DeepSeek V3 through OpenCode's free tier is good enough for that job, and it costs nothing in the cases where Claude Code's context window would otherwise get spent on boilerplate edits.

The trade is not quality for cost. It is deciding which step in the pipeline actually needs the more expensive judgment, and only paying for that.

The architecture

The pipeline is a Python orchestrator with three stages: plan, execute, review. Each stage shells out to a CLI tool and passes structured text between them.

agent-pipeline/
├── pipeline.py          # main loop
├── agents/
│   ├── planner.py       # calls claude -p to generate the plan
│   ├── executor.py      # calls opencode run to implement it
│   └── reviewer.py      # calls claude -p to review the diff
├── utils/
│   ├── git.py           # git diff and git status helpers
│   └── output.py        # stage separators for readable logs
└── config.py
Enter fullscreen mode Exit fullscreen mode

The main loop is intentionally small. It does not try to be clever about parsing the plan, it just passes it along as text and lets each stage do one job.

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

from agents.executor import run_executor
from agents.planner import run_planner
from agents.reviewer import ReviewResult, run_reviewer
from utils.git import get_diff


@dataclass
class PipelineConfig:
    project_path: Path
    max_retries: int = 3


def run_pipeline(request: str, config: PipelineConfig) -> None:
    plan: str = run_planner(request, config.project_path)
    print(f"PLAN:\n{plan}\n")

    feedback: str | None = None
    for attempt in range(1, config.max_retries + 1):
        prompt: str = plan if feedback is None else f"{plan}\n\nFix these issues:\n{feedback}"
        run_executor(prompt, config.project_path)

        diff: str = get_diff(config.project_path)
        result: ReviewResult = run_reviewer(diff, config.project_path)

        if result.approved:
            print("RESULT: approved")
            return

        print(f"REVIEW attempt {attempt}: issues found\n{result.issues}")
        feedback = result.issues

    print("RESULT: max retries reached without approval")
Enter fullscreen mode Exit fullscreen mode

The planner and reviewer both call claude -p, Claude Code's non-interactive print mode, which returns a single response instead of opening a session. The executor calls opencode run --agent autoexec --model deepseek/deepseek-chat-v3-0324, passing the full plan as the prompt.

The gotcha: headless mode still asks for permission

The first time I ran this end to end, it hung. Not crashed, just sat there. OpenCode's default agent, even invoked through opencode run in a script, still pauses on write and delete operations to ask for confirmation. In an interactive terminal that is a reasonable default. In a subprocess call with no one watching the terminal, it is a deadlock that looks like a frozen process with no error message.

The fix is a custom OpenCode agent with permissions granted up front, so it never needs to ask.

---
name: autoexec
permissions:
  write: allow
  edit: allow
  delete: allow
  bash: allow
---

You implement the plan exactly as given. Do not ask for confirmation
before writing, editing, or deleting files within the project directory.
Enter fullscreen mode Exit fullscreen mode

Saved at ~/.config/opencode/agents/autoexec.md, this agent runs to completion without blocking. It is worth being deliberate about scope here: full write and delete permissions inside an automated loop only make sense if the executor's target directory is a project you can safely revert with git, and if the plan it receives is specific enough that there is not much room for it to improvise its way into deleting something you needed.

The review loop and why it needs a strict protocol

The reviewer's job is to look at the diff and decide, not to have a conversation about it. That means its output needs to be machine parseable, not a paragraph of hedged observations. I settled on a fixed protocol: the reviewer must respond with either APPROVED or ISSUES: followed by a list.

from __future__ import annotations

import subprocess
from dataclasses import dataclass
from pathlib import Path


@dataclass
class ReviewResult:
    approved: bool
    issues: str


def run_reviewer(diff: str, project_path: Path) -> ReviewResult:
    prompt: str = (
        "Review this diff for correctness, side effects, and style consistency "
        "with the rest of the codebase. Respond with exactly one of two formats: "
        "'APPROVED' on its own, or 'ISSUES:' followed by a numbered list.\n\n"
        f"{diff}"
    )
    result = subprocess.run(
        ["claude", "-p", prompt],
        cwd=project_path,
        capture_output=True,
        text=True,
        timeout=120,
    )
    output: str = result.stdout.strip()
    if output.startswith("APPROVED"):
        return ReviewResult(approved=True, issues="")
    return ReviewResult(approved=False, issues=output.removeprefix("ISSUES:").strip())
Enter fullscreen mode Exit fullscreen mode

Capping retries at three matters more than it seems. Without a ceiling, a plan that was ambiguous in a way neither model catches will loop indefinitely, each iteration burning a Claude Code call for review. Three attempts is usually enough to distinguish a fixable implementation slip from a plan that needs a human to rewrite it.

Common pitfalls

OpenCode has no memory between invocations, so the plan handed to the executor has to be fully self-contained. It cannot say "as discussed above," because there is no above from OpenCode's perspective. Every file path, every exact instruction, has to be spelled out as if this were the executor's first and only message, because it is.

The diff you pass to the reviewer needs to be the actual git diff output, not a summary of what changed. A summary written by the executor is exactly what you are trying to verify, so feeding it back as the thing being reviewed defeats the point.

DeepSeek's free tier on OpenCode also has rate limits that show up as a stall rather than a clear error in some client versions, which looks identical to the permission deadlock described above. Worth checking opencode logs directly if the executor step hangs after the agent config is already fixed.

Where this leaves the pipeline

Stage Model Job
Plan Claude Code Read the codebase, decide what changes
Execute DeepSeek via OpenCode Write the diff exactly as planned
Review Claude Code Catch mistakes, approve or send back

The next step worth adding is cost and token tracking per stage, so the split pays for itself in something more concrete than intuition. Once that is in place, the same pattern extends past a single request, feeding a small backlog of tickets through the loop overnight and reviewing only the ones that came back with unresolved issues.

Top comments (0)