Shoal is an agent harness in one binary. It runs an AI agent, and the
model behind it, on your own machine, starting the inference server and
keeping the model loaded. Point it at a LAN box, a rented GPU, or a cloud
model you hold a key for, and the model runs there instead. What runs
where is your call. A layer kept on your machine changes its cost,
version, or defaults only when you change them.
Machinery
Shoal enforces itself
The rules here are not a list, they are a loop. Hooks fire at prompt
build, during the turn, and at completion, so each turn opens updated.
01
Chart
During the turn, by tool call
Discovery without reading
A YAML adjacency graph of your codebase. Nodes are files with purpose lines lifted verbatim from their own leading comment, edges are imports, resolved for Go today, plus decomposition lineage and hand annotations. Discovery walks breadth-first from task-seeded nodes with fidelity decaying by distance, under a token budget.
Served by a tool call and never injected into a prompt. It appends at the tail of history, so it never moves a byte the server has already cached.
A map is only worth having while the code still has a shape worth mapping.So the write itself is measured on the way through, before the bytes settle.The same write path updates the chart on its way out, and every candidate is stat'd again at pack time, so correctness never depends on the hook having fired.
02
Bosun
During the turn, on the write path
Structural enforcement
Every write, edit, and patch is measured on the way through. At 500 lines the file is noted. At 600 a decomposition debt opens, and the model is asked once for a plan to pay it before the turn ends. At 700 a write that grows the file past the limit is refused and the previous content restored. The rule forbids compressing logic to duck the limit. Files decompose into domain-cohesive children and the parent keeps orchestration.
The hooks attach inside the registry constructor, so a delegated sub-agent inherits them and cannot write its way out.
Two decks need keeping, and Bosun keeps the first, the repo.The second is the context window, and it can be tidied only once the turn has stopped reading it.
03
Janitor
After the turn, in the idle window
Context hygiene
Cleanup runs outside the main loop and makes no model call, so the KV cache it exists to protect stays as the last turn left it. Any pass that would share the main loop’s berth is refused outright, so your working model keeps its berth to itself.
It reads history without writing to it and stages edits that apply at exactly one cache boundary, the next prompt build, refusing any stage whose prefix moved underneath it.
It applies nothing itself.The staged edits take effect at the next prompt build, so the following turn opens on a cleaned window and asks the chart again.
The three phases close on themselves. The janitor's staged edits leave
the idle window and take effect at the next prompt build, so the loop
returns to the chart with a cleaned window.
The next prompt build
1ChartDuring the turn, by tool call
2BosunDuring the turn, on the write path
3JanitorAfter the turn, in the idle window
Nothing here is a step in a pipeline. Every phase hands the next turn a
better starting state than the last turn had.
Breakwater // the wall
Shoal reaches only the hosts you name
Always, on every dial
Every HTTP client in the tree dials through one policy. Loopback is
always allowed. A destination off this machine is allowed only where
one of your moorings names it, and anything else is refused with the
host written into the refusal.
It is checked in two places, once on the request destination before the
transport runs and once on the resolved address inside the dial. That
second check is why a list of permitted URLs is not enough. DNS
rebinding and redirect following both walk straight through a list like
that, and neither one gets past a test on the address actually about to
be connected.
The wall covers Shoal's own Go code. The inference server it runs for
you, MCP hook servers and bash dial on their own account,
so those answer to your machine's rules rather than to this policy.
shoal doctor prints which state the wall is in and every
destination currently named. shoal config set breakwater open
is how you say this machine is not contained, and it is read from your
own settings rather than the project's, so a repository you clone
cannot lower the bar for you.
Ontology
Five parts in a chain
Each part exists because the one above it had to hand the work
somewhere.
01
Shoal
The product
One agent in one binary. The terminal, the VS Code extension, and the desktop window all run the same one.
DemoA session from launch to the first applied edit, start to finish.
One binary still has to run something.Underneath it there is a single engine holding the agent loop, the tools, and the machinery that manages the window.
02
Keel
The engine inside
Agent loop, context machinery, tools. The internal name power users know. Everyone else says Shoal.
The engine brings no model of its own, so you bring one.Bring your own model (BYOM) and it runs on this box. Bring your own key (BYOK) and it runs on an endpoint you name.
03
Dock
Where models tie up
An inference engine, wherever it runs. Shoal starts llama.cpp or MLX on this machine, and moors to vLLM, to a vendor's API, or to any server that speaks OpenAI chat, at an address you name.
Demoshoal up spawning an inference server, then the same session against a remote endpoint.
A dock is not one place, it is places.Room has to be counted before anything loads, so the dock is divided into spots that can be measured.The engine can sit on this machine, on one you name, or behind a vendor's key. Shoal sends a request off this machine only to an address you wrote down.
04
Berth
One spot at a dock
Holds exactly one moored model, never two. Allocated to fit, and memory-accounted before the model loads. What the ledger cannot measure it admits anyway, and says why.
A spot is only a spot until something is in it.Tie a model up, give it a name and a purpose, and you have the thing you talk to.
05
Mooring
A model, tied up and ready
The dock is the place and the mooring is the ship. It is also the named record you write, listing model, dock, and purpose, with an address and a key when the engine runs somewhere else. Several moorings run at once.
Demo/moor swapping the active model mid-session and naming the cache boundary out loud.
Local-first
Local-first is a guarantee about defaults and about who decides. Shoal
starts engines only on this machine, and a request leaves it only for
an address you wrote down, so a box on the LAN, a rented GPU, and a
vendor's API are all first class, and every one of them is your call.
Surfaces
Run Shoal your way
The same agent runs in your terminal, in VS Code, and in a window of its
own, on one engine. Pick the surface you work in, and the docs take you
from install to first run.
Benchmarks
Shoal measures itself
Six questions, each with its method written down and its bar set before
the run. The results fill in under each one.
Chart ablation
Method written
What does discovery cost with the chart, compared with searching for the same files?
Four retrieval arms over one corpus, from the chart alone through to an oracle that is handed the answer, counted in tokens spent and target files delivered. The ten tasks were written before the chart existed, so none of them can be shaped to flatter it.
What a run reports
arm tokens files
A. chart alone ·· ·· of 27
A+. chart, then view ·· ·· of 27
B1. oracle views ·· ·· of 27
B2. grep and view ·· ·· of 27
Memory ablation
Method written
What does a memory store cost at each of the three places it can be put, and does the cheapest one still find the right record?
Both halves, because either alone decides nothing. Any policy saves tokens by showing less, and a retrieval that returns the wrong record for a tenth of the price has stopped answering rather than won. Four falsification conditions are named up front.
What a run reports
policy standing per recall on write
in prompt ·· ·· ··
index ·· ·· ··
tool call ·· ·· ··
retrieval hit ·· chance ·· mean rank ··
Stowage ablation
Method written
Does a fidelity gradient over history keep more of what a task needs than the compaction cliff?
Deterministic, with no model and no GPU: histories built from hand-labelled tasks, scored on what a live turn still refers to. Written so the answer can refuse the feature it was built for, which is the only reason to run it.
What a run reports
live retention discrimination
gradient ·· ··
cliff ·· ··
driven to the cliff cost at any setting ·· of 10
Edit reliability
Method written
When an edit fails, which part of the harness failed?
Every edit-class tool carries a typed failure reason, and one row is written per tool call and per agent loop. The counters are a pure function over those rows, so a definition disputed later is re-derived from the saved file rather than re-measured on a GPU.
What a run reports
pass ·· of 20
edits landed first try ·· percent
anchor misses ··
ambiguity refusals ··
loops spent recovering ··
whole-file writes ·· (the guard)
Tool-call recognition
Method written
A model that writes tool calls into its prose can still be recognised. Does a pass then measure the model or the recogniser?
Every recognised call writes its own row naming the tool, the shape, and whether that shape is the one the model template renders. Any pass rate can then be re-derived with the recogniser subtracted.
What a run reports
model pass recognised native
gpt-oss-20b ·· of 20 ·· ··
qwen2.5-coder-14b ·· of 20 ·· ··
llama-3.1-8b ·· of 20 ·· ··
Output parity
Method written
Two models finish the same task. Is the work they leave behind the same standard of thing?
Three arms on one harness, a held-out suite the agent never sees, and six counters that need no judge. A pass rate says a task finished and says nothing about the standard of the work, so five falsification conditions are pre-registered, including the one that would put a caveat on every completion figure.
What a run reports
arm held out diff minimality collateral vet
L ·· ·· ·· ··
F ·· ·· ·· ··
N ·· ·· ·· ··
falsification F1 to F5 ·· fired
Roadmap
The whole build is on one board
An arc is one capability, and more are added as the work turns them up.
Each carries three states rather than one. Building it opens the other
two, so testing and the finish pass run alongside each other rather than
in sequence. Any row with a write-up opens on click.
24Arcs
23Built
1Closed
#
Arc
What it does
State
Build
Testing
Finish
01
The codebase uses the vocabulary this page teaches, one name per thing.
Built
In testing
Polishing
02
The terminal surface. Live output, real scrollback, steer a turn while it runs.
Built
In testing
Polishing
03
Tool calls come out well formed by construction rather than repaired after the fact.
Built
In testing
Waiting
04
Launch is one binary, and the server outlives a single run.
Built
In testing
Polishing
05
A reviewer that cannot write, enforced by Shoal rather than asked of the model.
Built
In testing
Waiting
06
File size limits stop being advice and become machinery on the write path.
Built
In testing
Waiting
07
The codebase becomes something the agent asks about instead of searches.
Built
In testing
Waiting
Why it exists
In one recorded run, a model called view on the same file 65 times. Speculative exploration is the most expensive thing a small context window does, because the model has no cheap way to see the shape of a repository. It greps, guesses, reads a whole file to learn it was the wrong file, then carries that file in the window for every remaining turn of the session.
How it works
Shoal keeps a YAML adjacency graph at .shoal/chart.yaml, YAML because you are meant to read and edit it. Nodes are files, each carrying a line count, a language, its defined names, and one purpose line lifted verbatim from the file's own leading comment. No model writes that line, and it is re-derived rather than remembered, so deleting the comment removes it from the chart. Edges are Go imports resolved through go/parser, decomposition lineage verified by Bosun, and annotations you draw by hand. Discovery walks breadth-first from the nodes your task seeds and spends its budget nearest-first, so pressure costs detail before it costs coverage. The chart is served by a tool call and never injected. No standing copy lives in any prompt, results append at the tail of history, and a session that never asks a discovery question never pays for one.
The shape on disk. Hand-editable on purpose.
# .shoal/chart.yaml
version: 1
root: ~/shoal
nodes:
internal/chart/chart.go:
lines: 412
lang: go
purpose: The chart graph, its nodes and edges, and the YAML on disk.
purpose_from: comment# lifted verbatim, never generated
symbols: [Chart, Node, Edge, adjacency]
edges:
- from: internal/chart/chart.go
to: internal/chart/build.go
kind: import# import | lineage | manual
Still open
Import edges are Go only. Other languages get nodes, purposes and defined names but no derived edges, because the fallback deliberately does not guess. In a graph you hand-edit, a guessed edge reads as a fact. There is no ranking inside a distance ring beyond edge kind and the identifier prior, which is where the missing 30 percent of coverage lives. Bash operates outside the write hook, so a shell redirect can change a file the chart never hears about, and the pack step is what catches it. No live inference has measured the chart yet, so nothing here shows a model completing a task better because of it.
08
Context hygiene as a background pass that never disturbs work in flight.
Built
In testing
Waiting
09
Models become named connections, and something checks one fits before it loads.
Built
In testing
Polishing
10
One typed stream out, one channel in. Every surface becomes a thin client.
Built
In testing
Waiting
11
Outside tools come through one door. Credentials live where the model never sees.
Built
In testing
Waiting
12
Studied and declined.
Closed
Closed
Closed
13
First run asks what you want to work on, on every surface, once.
Built
In testing
Polishing
14
Delegated work keeps its record past the session that spawned it.
Built
In testing
Polishing
15
Old tool results step down through outline, head, and label as the window fills. It ships switched off.
Built
In testing
Waiting
Why it exists
When a context window fills, Shoal compacts, dropping the oldest tool results whole to get back under the line. That is a cliff, and a cliff cannot tell a file your task depends on from a file you opened once and closed. The codebase already gets better treatment than that, since the chart hands a model a graded view of a file rather than all of it or none. The question this arc asked is whether history deserves the same gradient.
How it works
It does not, on the evidence, so it ships switched off. The gradient is built and tested. It is a fourth kind of janitorial edit that steps an old tool result down through outline, head, and finally a label that always names the tool and the file, so a condensed result stays identifiable and can be run again. It is off because a pre-registered measurement refused it, and refusing a feature you have already built is the only kind of refusal that costs anything. One number killed it. The gradient could not be driven to the compactor's token count on any of ten histories, because a result the live turn still refers to is exempt at every setting, so at the pressure where compaction actually happens the policy has no behaviour left. It keeps far more of what a task needs when cost is not matched, 0.83 against 0.34, and that is exactly the comparison that does not decide anything.
Still open
The arc's real finding was not the question it asked. Measured on this corpus, what the compactor keeps is anti-correlated with what the task needs, because its only ordering is recency. Ground truth gets read early, and the files that turned out not to matter get read late and survive on position alone. It has no relevance input at all, so read that as indifferent rather than working against you, and the sign is a property of this corpus rather than a law. Where to look next is a relevance signal that reads what the task is about instead of only what the last turn mentioned. The corpus is ten histories built over this repository, which is also the repository the code was written in.
16
Every tool that takes a path stays inside your project.
Built
In testing
Waiting
Why it exists
A coding agent that can read and write any path on the machine is trusting the model to stay inside your project. That is an honor system, not a boundary. One read-only git tool already held that line, written so a reviewer could not reach out of the workspace, and the arc was to make the same line hold everywhere a tool takes a path.
How it works
One check decides whether a path is inside your workspace, and view, glob, grep, write, edit and patch all call it. The path is resolved first, symlinks and all, and then compared, because a link inside your project pointing outside it walks straight through a check that only reads the spelling. An absolute path or one that climbs out with .. is refused by name, telling the model what happened and what would work, and it is never turned into a question for you to approve. Write and patch check before they create a directory, so a refusal cannot arrive after the directories already exist. The boundary is threaded from your project onto the tool registry and stamped at the moment a call is dispatched, both kinds of delegated agent inherit it, and nothing inside a session can widen it.
Still open
Bash is not contained and Shoal does not claim it is. A shell cannot be bounded by inspecting its arguments, so that is a separate piece of work with a different shape. Reading a secret through bash is still open for the same reason, though the file-reading route to it is now closed whenever your workspace is not your home directory.
17
Undo that belongs to Shoal, not to your commit history.
Built
In testing
Waiting
Why it exists
A wrong edit that costs nothing to revert is a retry. One that costs a manual cleanup is a reason to stop using Shoal. Local models carry higher per-turn variance than frontier ones, and in the measured corpus run three of four failures were the model working for twenty loops without converging. Undo built on your own commit history is the wrong shape. It can only reverse what Shoal itself committed, and it refuses when the last commit was somebody else's.
How it works
Shoal keeps a shadow worktree, a second git directory under the app data directory, seeded from your repository but never writing to it. A capture records a content-addressed tree and a restore puts named paths back, so your own history is never touched and your own commits are never made or unmade on your behalf. One constraint decides the shape. Bash operates deliberately outside the file writer, so a checkpoint built on write hooks would silently miss every file bash creates. The tree is captured at the turn boundary instead, not per write. Undo composes the two halves and says which ones it did. Rewinding the conversation alone tells you your files were not touched, and undoing to a point restores the files first and then cuts the history, so a restore that fails leaves nothing to explain away.
Still open
A restore is additive. It puts back what the capture held and never deletes, so a file you made after that point survives the undo and is named in the notice rather than removed behind you. That is the recoverable direction of a choice that has no safe answer, and it means undo is not a time machine. The cost of capturing a large tree has not been measured, and the capture runs at the start of every turn.
18
Long term priors, recalled on demand, earning their keep.
Built
In testing
Waiting
19
Named roles instead of one agent wearing every hat.
Built
In testing
Waiting
Why it exists
Every piece of work Shoal handed to a sub-agent went to the same agent. A fresh context, every tool the main loop has, and the same instructions. Handing a review to something that can rewrite the file it is reviewing is a boundary you are asked to trust the model to respect, and asking for a quick look at how something works costs you an agent equipped to change it.
How it works
A delegation can name a specialist and get that specialist's toolset and its own instructions instead of the default. Two ship. The reviewer reads code and reports what is wrong with it, worst first. The scout finds something out and reports the facts, including what it did not find, so you can tell an exhaustive search from a lucky one. Both are read only, and read only here means the tools to change a file are not in the set the child was handed, so a call to one of them is refused by Shoal at the moment it is dispatched rather than by instructions asking the model to behave. A specialist names a toolset; it cannot list one, so there is no way for a named role to end up with more reach than the role was given. Naming none is unchanged, so a delegation gets the full toolset and the main loop's own instructions. A name that does not exist is refused with the names that do, because falling back to the ordinary agent would make a typo look like a working delegation. The roster reports which specialist each child ran as, in the terminal and in both windowed clients, and the record survives the session.
Still open
Both specialists that ship today are read only, so the enforcement that would matter to a specialist allowed to write is carried and has never fired. The roster is small on purpose and neither entry prefers a particular model yet, though a specialist can name one. And the list is compiled into the build rather than read from a file, so adding a role is a change to Shoal rather than something you write yourself.
20
Shoal with no human attached.
Built
In testing
Waiting
Why it exists
Everything Shoal does assumes somebody is watching, ready to approve a write, read a diff, and steer a turn that wanders. Automation has none of that. A scheduled job, a check on every pull request, or a trigger from another system needs a Shoal that runs once, decides for itself, and exits with an answer a script can branch on.
How it works
shoal exec takes one prompt from an argument, a file, or standard input, runs it without asking anybody anything, and exits. Output is the final text, one result document carrying the stop reason and what it spent and which files it touched, or every engine event as JSON lines for something else to read as it happens. The exit code is distinct per outcome, so a scheduler can tell a completed run from a refused one without parsing anything. Approvals cannot ask a person, so they are answered by an autonomy ladder. One table maps what the engine already decided a call is onto the lowest rung that permits it. The floor is the default and it refuses anything that writes, and reports the refusal rather than dropping it quietly. Climbing takes a flag, and the top rung is not reachable by naming a level, only by asking for it outright.
Still open
The floor has one honest gap. What counts as read-only is decided before the ladder sees a call, so a shell command the permission engine already treats as read-only runs at the floor without the ladder weighing in. Tightening that is open work, and it is worth knowing before pointing an unattended run at a machine you care about. Session forking, isolated worktrees, queues and webhooks are deliberately not part of this.
21
Run the work on a machine that is not this one.
Built
In testing
Waiting
22
Edits land as written, and every miss is counted.
Built
In testing
Waiting
23
Know what is moored, and narrow what it is allowed to touch.
Built
In testing
Waiting
24
Any model you name. Know what it can do before it loads, recover when it falls short, and measure the difference.
Built
In testing
Waiting
Why it exists
A coding agent quietly takes the shape of whichever model its author had loaded. The tool-call format, the chat template, and the parser that reads a completion back all follow that one model's habits, and a different model then fails against them for reasons that look like the model's fault and are not. A twenty-task corpus run across four models on one machine found four distinct failures of exactly that kind, and every one of them was Shoal's to fix.
How it works
Shoal reads a model's tool-call support from its GGUF header before any weight loads, so shoal models and shoal doctor tell you when a model cannot call a tool and stop recommending one. When a dock refuses to parse a completion, Shoal retries once with the tools dropped instead of ending your turn, and counts the fallback in the tally as a degrade so the number is never hidden. A mooring can name the chat template llama.cpp renders with, which is the only way the two Mistral models are reachable at all. And a tool call a model writes into its message body is read back out of it, under a guard that is the point of the feature rather than a caveat on it. Every recognised call writes its own tally row, so a pass rate can always be re-derived with the recogniser subtracted. Measured live on three models, pinned and reproducible. gpt-oss-20b went 19/20 to 20/20 with zero recognised calls, which is the control saying the recogniser stays out of the way, and Qwen2.5-Coder 14B went 0/20 to 15/20 with all 138 of its tool calls read out of prose and none off the wire.
A mooring naming the template llama.cpp renders with. The flag forms are --chat-template and --chat-template-file, and llama-server only accepts an arbitrary template once --jinja is set.
Codestral and Devstral have both pieces they need, a named template and tools described in words, but neither has been loaded, so the claim that those two are reachable stays untested. Every result is one pass per model, so nothing here is a median. And the header did not predict what a model writes into its body. Qwen renders tool-call tags yet wrote fenced JSON 138 times out of 138, so the native count is zero across all three runs. That finding argues against the design that expected the header to steer the recogniser, and it is recorded rather than buried.
Sea trials // the release path
The arcs build Shoal. This path is what carries a finished build out
to people who are not the builder. Alpha opens when every arc has
cleared testing and its polish pass, which is a later bar than being
built.
AlphaInvite only. A named few, running it on their own work.
Closed betaA wider list, still by invitation, and large enough that the reports stop agreeing.
Open betaAnybody who wants it, with the known rough edges written down first.
Full releaseIt goes out under its own name, and there is no list to get on.