DEV Community

Cover image for Agentic Core, Deterministic Shell
Alexander Heerens
Alexander Heerens

Posted on Edited on Originally published at levelup.gitconnected.com

Agentic Core, Deterministic Shell

My coding agent said it read our coding standards. It read 80 lines. A pattern for running coding agents you don't have to watch.

Three weeks ago I let Claude review a change. The review came back clean, and Claude made a point of telling me it had read our coding standards and checked the change against them. Then I checked the logs: head -80 on the standards file. It read the first 80 lines, and that was "reading the coding standards".

The agent did not lie, at least not on purpose. It decided for itself what "read" means, and 80 lines felt like enough.

The fix took me one field in one place. But it was only that easy because of where I had ended up after about five months of working on dev loops with coding agents. And I think the way there is more interesting than the fix, because a lot of developers are somewhere on that same way right now.

How I got here

five months, four steps: one big skill, small skills, scripts out, turned around

Step 1: one big skill

It started with a very big skill, my next-work skill. It was huge. It did the whole loop: pick the next issue, implement it, delegate to the verification, fix what came back. And everything in there was in English. No scripts at all.

It was a bit like a huge shell script. It reminded me very much of the shell scripts we wrote to deploy services: huge, with lots of cases in there for different behaviors, frontend tests here, backend tests there, some conditions. Only that here it was all text.

And yes, mostly it got followed. But it was hard to maintain, and hard to measure what was getting better. Sometimes it ran the UI tests twice, and we couldn't really figure out why. We corrected the prompt and it was better. Then we wanted to change something in the interaction with beads, our issue tracker, and we had to find all the places where it lived. All these commands were scattered across one big skill.

Of course you can do that with the help of AI too, and get everything consistent again. But why? Why take the risk of not controlling your development flow, if you can spend the same amount of prompts on controlling it?

Step 2: lots of small skills

So the next step was to split the one big skill into lots of sub-skills, called one after the other. Implement, verify, fix, merge: basically the steps of a pipeline, each one a skill of its own. Overall it was easier to manage, but there was still room for improvement.

Step 3: get the scripts out

But the sub-skills still did a lot of small work inline: running a guard, checking the length of the documentation, reading out the beads issue. All of that the agent had to understand and do itself. So we refactored a lot of it out of the skills into little helper scripts. That saved tokens and also sped up the whole skill, because a script just gets executed and doesn't have to be understood by the agent. The agent only processed the result.

That was good. But the scripts were small helpers within the flow, not the flow itself. Everything was still wrapped in little step skills, and the flow still lived in the prompts.

Step 4: turn it around

I still thought that's not the best way. Maybe we can turn it around: put the deterministic part outside, around everything, and let the agent only do the work inside one step.

If you have a skill today that calls other skills, you are probably somewhere around step 2 or 3. That's not a bad place. But it is worth asking which decisions your agent still makes that a few lines of code could make for it.

The pattern

Gary Bernhardt coined "functional core, imperative shell" years ago: keep the logic in a pure core and push everything messy, like I/O, to a thin shell around it. For coding agents I'd turn that around, because there the messy part is in the middle:

Agentic core, deterministic shell.

the agent inside the shell; prepare and eval on the shell, input in, route out

  • The core is the agent. It does the creative work in one step: implement an issue, review a diff, fix the findings. Inside that step it can be as clever as it wants.
  • The shell is plain code and configuration. It decides what the agent gets as input, checks what comes back, and picks the next step. It never asks the agent what happens next.

If that sounds like CI, that is exactly the idea. Nobody asks a developer "do the tests pass?" and merges on a yes. We let the pipeline run the tests. A coding agent deserves the same treatment, it just never got a pipeline.

With the shell around the agent, three patterns become possible. None of them is new. But you can only use them reliably when code holds control.

Output-driven behavior. Don't tell the agent what to do, tell it what it has to hand back. Every claim that matters becomes a field. To fill the field, the agent has to do the work. And because the shell knows the input, it can check the output against it. If it doesn't match, the step fails, the agent gets the log and tries again. That's the back pressure.

Need-to-know context. The step gets only what it needs, and the shell prepares it. That puts you much more in control of the context. For example, depending on the stack: if the changes are in the frontend, you point the agent to the documentation and coding rules for the frontend. If they're in the backend, you point it to the backend docs and ADRs, instead of relying on the agent to discover them on its own. And because you prepare the context, you can test it and iterate on it. You can even A/B test it: take the one step, run it with three versions of its input, and compare the outputs side by side. That's how I found the right inputs, and sometimes even the right outputs.

Lazy agent. Sometimes the best choice is not to run the agent at all. The deterministic shell decides whether an agent needs to run or not. For example, a simple git merge can run through, or it can run into a conflict. A conflict is complicated, so the agent takes over and resolves it. If the merge was clean, we just go on.

Back to the review

The head -80 review happened after step 4. The review was already a step of its own, with the shell around it. The shell routed it correctly: the review passed, so the run went on. But it could only check what the step gave back, and "I read the coding standards" was a sentence in the agent's answer. Still the agent's word.

So the fix was one change in the step's output. Next to the findings, the agent now has to list the coding standards it applied. That alone did it: to fill the list the agent has to read all of them, there is no way around it with 80 lines. And because the list is data and not a sentence, the shell can check it too. Our standards have 21 rules, so the eval checks that the list has 21 entries. If not, the step failed.

That's output-driven behavior: the question "did you read it?" is no longer answered by the agent's word. It is a field you can check.

What step 4 looks like: sous

The shell I built for this is sous, and it is open source. You describe your development flow as a graph in YAML, like a GitHub Actions workflow, and sous runs it on your machine with the claude or codex CLI you already have.

Here is a shortened part of the task flow from the starter:

- id: review
  uses: review
  agent: { kind: claude, model: claude-opus-5-5 }
  with:
    diffRange: ${{ steps.implement.commits[0] + "^..HEAD" }}
    criteria:  ${{ ctx.work.body }}
  routes:
    - to: sync
      when: res.ok
    - to: blocked
      when: steps.review.visits >= 3
    - to: fix
      else: true
Enter fullscreen mode Exit fullscreen mode

If the review passes, the run goes on to sync with main; if not, it goes to fix and back, until the third review stops it. None of these decisions is made by the agent.

the task flow in  raw `sous ui` endraw  on a spec sheet: agent step, review running, eval gate, possible routes, module without agent, route taken, agent and model per step, prepare

Each step is a folder in your repository: a Markdown prompt, a JSON Schema for the output and, optionally, two small pieces of TypeScript around it. prepare runs before the agent and decides if the agent runs at all, that's the lazy agent. eval runs after the agent and judges the work. It can be a function or just npm test. If it fails, the agent gets the log and tries again, up to a cap. After the cap the run stops blocked instead of guessing.

What the shell gives you

Two more things come for free:

  • A second opinion by design. Each step names its own agent and model. Claude can implement and Codex can review, so the reviewer is not the same model checking its own work.
  • Runs you can resume. A blocked run keeps its worktree. You fix the problem by hand, run the same command again, and it continues at that step.

Try it

sous gives you a simple way to design a workflow for your agent. Try it on one issue in your own repository. You need Node 22, gh auth login, and claude or codex on your PATH:

npm install -D sous-factory
npx sous init
cd .sous
./sous validate
./sous run bench <issue>      # smoke test, touches nothing
Enter fullscreen mode Exit fullscreen mode

The code is on GitHub at github.com/sousfactory/sous, MIT licensed, and sousfactory.com has the overview.

If you are somewhere on this way yourself, with one big skill or a chain of small ones, I'd like to hear where you are and what made you move. And if your agent ever told you it read something it didn't, I'd like to hear that too.

I dictated this article and edited it with AI help. The experience, the opinions and the mistakes are mine.


Originally published in Level Up Coding on Medium.

Top comments (0)