Paul Shynin

Developer tooling

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.

Where AgentGrid sitsCoding agents a, b and c, started by whatever runs them, each register claims with AgentGrid and read its answers as JSON and exit codes. AgentGrid is one Go binary with no daemon or server. Its rules live in a pure package that does no input or output, and its state is one SQLite file. It reads git with rev-parse, merge-base, rev-list and diff, and never writes to it.Coding agents, started by whatever runs themagent aagent bagent cclaims inJSON and exitcodes outAgentGridone Go binary; no daemon, server or cloudPolicy rulespure package, no I/OStateone SQLite filereads onlygitrev-parse, merge-base, rev-list, diff
Drawn from the behaviour verified at commit e04138e. It does not launch agents or manage worktrees, embeds no model logic, and never merges, force-pushes or rewrites history.

Claims: a conflict is refused before the work

Two agents’ claims overlap. A claim is a literal path or a glob, with an intent: read or edit.
First agent’s intentother readsother edits
readsAllowed, without a warningRefused: exit 3, nothing written
editsRefused: exit 3, nothing writtenRefused: 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: 0
Captured from AgentGrid built at commit e04138e. Agent billing holds pkg/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.

Files changed on the base branch match the agent’s claims.
Matching claimsRecommendation
Edit onlyrebase
Read onlyreview
Bothre-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: 0
Captured from AgentGrid built at commit e04138e. main changes a file under billing's edit claim (rebase), then a file under both agents' read claims (re-plan for billing, review for auth). After auth is rebased onto main, the next refresh reports it clean. […] marks commands left out of this excerpt.

Diff-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: 0
Captured from AgentGrid built at commit e04138e. A one-file change inside billing's claims scores LOW. After a commit that adds a forbidden path, a file outside its claims and a binary file, it scores HIGH, and the level is the highest reason's severity. diff-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: 0
Captured from AgentGrid built at commit e04138e. Auth has been fast-forward merged into main; billing is stale and high-risk. The last command compares refs, HEAD and git 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 status and refresh fail for every agent.
  • Diff-risk counts a read claim 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
}
Excerpt from internal/policy/stale.go at commit e04138e. A changed file under a read claim still flags the agent: it declared it would only read the file, but its plan was built on it.
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

Next case study
Cart-Checkout Platform