AgentGrid: coordination for parallel coding agents
A local Go tool that coordinates coding agents working in parallel. It does not launch or run them: it tracks what each one has claimed, refuses overlapping edit claims, detects when a plan has gone stale, and scores how risky a branch has become.
- Version
- v0.1 MVP, no tagged release yet
- Users
- One: me, on my own projects
- Tests
- 107 at commit e04138e, run in CI on Linux and macOS
- Shape
- A single Go binary and one SQLite file; no daemon, server or cloud component
The problem
One coding agent in one terminal is easy. Several agents across branches, worktrees and terminals is a coordination problem: two agents editing the same file without knowing it, an agent growing a 4,000-line diff nobody can review, or an agent touching migrations/ while attention is elsewhere.
A better prompt does not stop two agents from editing the same file, and a smarter model does not know that main moved underneath the plan it made twenty minutes ago. What limits how many agents I can run at once is whether I can trust the combined result, which is a coordination problem.
The design principle. The tool embeds no prompts, no planner, and no model logic, and it does not launch agents or manage worktrees. Anything that reasons about code changes quickly; the rules about who may touch which files do not. Keeping them separate means the policy layer survives changes in the agent tooling underneath it.
Claims: a conflict is refused before the work
| First agent’s intent | other reads | other edits |
|---|---|---|
| reads | Allowed, without a warning | Refused: exit 3, nothing written |
| edits | Refused: exit 3, nothing written | Refused: exit 3, nothing written |
An agent’s own overlapping claims never conflict. Registering an agent with its claims is atomic, and claim check runs the same test as a dry run that writes nothing.
$ agentgrid claim add auth 'glob:pkg/billing/**:edit'
--- stdout: (empty)
--- stderr
error: claim glob:pkg/billing/**:edit conflicts with existing claims:
billing holds: glob pkg/billing/** edit
--- exit: 3
$ agentgrid claim add auth path:pkg/shared/money.go:edit
--- stdout: (empty)
--- stderr
error: claim path:pkg/shared/money.go:edit conflicts with existing claims:
billing holds: glob pkg/shared/** read
--- exit: 3
$ agentgrid claim list --agent auth
--- stdout
AGENT KIND PATTERN INTENT
auth glob pkg/auth/** edit
auth path pkg/shared/money.go read
--- stderr: (empty)
--- exit: 0pkg/billing/** for edit and pkg/shared/** for read, so both of auth's edit claims are refused with exit 3, and auth's claims are unchanged afterwards.Stale: the base branch moved under the plan
An agent reads a module, forms a plan, and starts working. Something else lands on the base branch touching those same files. The agent is now working from a picture of the repository that no longer exists, and nothing about its own branch looks wrong.
So refresh takes the files that changed on the base branch since the agent's current merge-base and checks them against the agent's claims, read and edit alike. Files the agent changed without claiming are not part of this check. Because the merge-base is recomputed each time, rebasing past the change clears the stale mark on the next refresh.
| Matching claims | Recommendation |
|---|---|
| Edit only | rebase |
| Read only | review |
| Both | re-plan |
$ printf "// changed on main\n" >> pkg/billing/types.go && git commit -q -am "main: billing change"
--- stdout: (empty)
--- stderr: (empty)
--- exit: 0
$ agentgrid refresh
--- stdout
refreshed 2 agents, 1 stale
billing: stale (rebase) — 1 file
auth: clean
--- stderr: (empty)
--- exit: 0
[…]
$ printf "// changed on main\n" >> pkg/shared/money.go && git commit -q -am "main: shared change"
--- stdout: (empty)
--- stderr: (empty)
--- exit: 0
$ agentgrid refresh
--- stdout
refreshed 2 agents, 2 stale
billing: stale (re-plan) — 2 files
auth: stale (review) — 1 file
--- stderr: (empty)
--- exit: 0
[…]
$ git rebase -q main feat/auth && git checkout -q main
--- stdout: (empty)
--- stderr: (empty)
--- exit: 0
$ agentgrid refresh
--- stdout
refreshed 2 agents, 1 stale
billing: stale (re-plan) — 2 files
auth: clean
--- stderr: (empty)
--- exit: 0Diff-risk: the highest reason, never a sum
The third mechanism scores how risky a branch's committed changes have become: files and lines changed against three configured tiers each, whether it touched a forbidden path such as migrations/, whether it touched binary files, and whether it modified files outside its claims or changed anything with no claims registered. Thresholds and forbidden paths come from a config file that is validated on every command, and each score is saved as a snapshot that diff-risk --no-refresh reads back without touching git.
Severity is the maximum across firing reasons, never a sum. Summing small signals produces a score with no single explanation behind it, while the maximum always points at one specific reason a branch is rated high. Every reason is emitted as a structured code, so the output is scriptable.
$ agentgrid diff-risk billing
--- stdout
agent: billing
branch: feat/billing
head: b2a1584
files: 1
lines: +1 -0
risk: LOW
reasons: (none)
--- stderr: (empty)
--- exit: 0
$ git checkout -q feat/billing && mkdir -p vendor assets && printf "package vendor\n" > vendor/lib.go && printf "package auth\n" > pkg/auth/added.go && printf "\000\001\002\003" > assets/logo.bin && git add vendor/lib.go pkg/auth/added.go assets/logo.bin && git commit -q -m "wide diff" && git checkout -q main
--- stdout: (empty)
--- stderr: (empty)
--- exit: 0
$ agentgrid diff-risk billing
--- stdout
agent: billing
branch: feat/billing
head: a72f2cf
files: 4
lines: +3 -0
risk: HIGH
reasons:
HIGH forbidden_path_touched 1 forbidden path(s)
vendor/lib.go
HIGH claim_violation_repeated 3 files modified outside claimed scope
assets/logo.bin
pkg/auth/added.go
vendor/lib.go
MEDIUM binary_files_touched 1 binary file(s)
assets/logo.bin
--- stderr: (empty)
--- exit: 0diff-risk exits 0 whatever it finds.Status, and git left as it was
status puts every agent on one table: ahead and behind, the latest risk level, the stale flag, and whether the branch has merged. Every command that reads state also takes --json.
The tool only reads git. Across refresh, stale, diff-risk, status and claim check, the refs, HEAD and the working tree stay exactly as they were.
$ agentgrid status
--- stdout
NAME BRANCH BASE AHEAD/BEHIND RISK STALE MERGED
billing feat/billing main 2/3 high yes no
auth feat/auth main 0/0 - no yes
--- stderr: (empty)
--- exit: 0
[…]
$ git for-each-ref --format="%(refname) %(objectname)" > ../refs-after.txt && git rev-parse HEAD > ../head-after.txt && git status --porcelain > ../status-after.txt && diff ../refs-before.txt ../refs-after.txt && diff ../head-before.txt ../head-after.txt && diff ../status-before.txt ../status-after.txt && echo "refs, HEAD and working tree unchanged"
--- stdout
refs, HEAD and working tree unchanged
--- stderr: (empty)
--- exit: 0git status from before and after five AgentGrid commands. […] marks commands left out of this excerpt.Limits in this version
- Claims never expire. There is no command yet to release one, so a finished agent's edit claims keep blocking until the state database is reset.
- Claims are voluntary. Nothing stops an agent from editing a file it never claimed; diff-risk reports it only after the commit.
- Uncommitted work is not scored: diff-risk looks at commits only.
- Overlap between two globs is checked with a heuristic, so some real overlaps are missed.
- Deleting a registered agent's branch makes
statusandrefreshfail for every agent. - Diff-risk counts a
readclaim as covering edits to that file.
Detail
Scope and shape
An orchestrator that launches agents, hands them tasks, and supervises results was the other option. I built a layer that sits beside git and answers questions about what is happening, and left process management to whatever runs the agents.
The rules themselves live in a pure package that does no I/O, covered by table-driven tests (71 subtests).
It shells out to git instead of reimplementing it, and only to read: rev-parse, merge-base, rev-list and diff. State is a few tables in one SQLite file (agents, claims, stale marks, diff-risk snapshots), so the current state can be inspected with sqlite3 alone.
Claims, in full
An agent registers what it intends to touch before it starts. A claim is a literal path or a glob, and each carries an intent, read or edit. Overlap is then a deterministic computation.
The rule is asymmetric: two agents reading the same files are allowed through without a warning, while any pairing involving edit is a conflict and is refused. Registration is atomic: if any of a new agent's claims conflicts, nothing is written. claim check runs the same test as a dry run. An agent's own overlapping claims never conflict with each other. Blocking at claim time is what keeps both agents from doing work that has to be thrown away.
The stale rule, in code
var rec core.Recommendation
switch {
case hasEdit && hasRead:
rec = core.RecommendReplan
case hasEdit:
rec = core.RecommendRebase
case hasRead:
rec = core.RecommendReview
default:
// Reachable only if a claim has an unknown intent; treat as re-plan
// rather than silently dropping the conflict.
rec = core.RecommendReplan
}Exit codes, JSON, and what it will not do
Every command that reads state takes --json, and the exit codes are a contract: 0 for success, 1 for an error, 3 for a policy refusal. stale and diff-risk report rather than block, so they exit 0 whatever they find, and a hook reads the JSON. status puts every agent on one table: ahead/behind, latest risk level, stale flag, and whether the branch has merged.
It never merges, force-pushes, or rewrites history, and it does not auto-resolve anything. The only thing it blocks is a conflicting claim; the merge stays with the person running it, because a tool that takes irreversible git actions on its own could destroy the work it exists to protect.
What it gives me today
- Coordination failures are made visible: overlapping edit claims refused at claim time, plans whose claimed files changed on the base branch surfaced with a recommended action, and oversized or off-limits commits flagged before review.
- Every read command emits stable
--json, so the checks compose into scripts and hooks instead of requiring a person to watch a dashboard. - 107 tests at commit e04138e, run in CI on Linux and macOS.
- A deliberately small surface: a dozen commands, each answering a question an engineer would otherwise answer by hand.
- This is the v0.1 MVP, with no tagged release yet, used by one person on my own projects. There is no throughput benchmark behind it.
Tech: Go, SQLite, git, CLI design, policy engine