Skip to main content
Glama
README.md
# agent-semaphore

**A coordination layer for parallel coding agents.** Worktrees do not remove merge
conflicts — they postpone them to integration time. agent-semaphore closes the gap:
**intent-carrying claims** on scopes, **a warning at the moment of writing**, **conflict
prediction before anything is committed**, and a **serialized landing queue with a
mandatory test gate**.

Local-first: no daemon, no cloud, no account. A single SQLite file in the git common dir
is the whole rendezvous point, so every worktree of the repo sees it by construction.
Cross-vendor by design — Claude Code hooks and MCP, Codex CLI over MCP, everyone else
over a git `pre-commit` hook.

[![CI](https://github.com/alwh1te/agent-semaphore/actions/workflows/ci.yml/badge.svg)](https://github.com/alwh1te/agent-semaphore/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](LICENSE)
![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)

---

## The problem

Agent-written pull requests conflict at **27.7%** — against 10–20% for human ones
([AgenticFlict](https://arxiv.org/abs/2604.03551), 107K+ agent PRs). On co-active pairs
the split is **19.8% intra-agent vs 41.7% cross-agent**: agents have no horizontal
awareness of each other, and every product that ships coordination coordinates only its
own agents. A badly resolved conflict carries up to **~26x** the bug density of ordinary
code (EMSE 2020) — the expensive part is not the conflict, it is the quiet bad
resolution.

Isolation is solved and commoditized (a worktree or a container per agent — everyone
ships it). Prediction and integration are not: nobody runs `git merge-tree` between live
worktrees, and standalone local merge queues effectively do not exist.

## What it does

| Layer | Mechanism |
|---|---|
| **Claims** | Leases, never locks: TTL, renewal by activity, monotonic fencing epochs, a mandatory intent (`reason`). Atomic all-or-nothing acquisition of the whole scope set in canonical path order, so deadlocks are impossible by construction. `exclusive` / `shared` / `intent` modes. Stealing is legal only from a dead holder or a human, and it is audited. Releasing a scope **wakes the waiters** and tells them which branch to rebase onto. |
| **Enforcement** | A `PreToolUse` hook (read-only against the DB, p95 ≈ 16–47 ms) that sees every write: protected ("hot") classes are denied always, another agent's scope is denied once in `warn` mode and permanently in `strict`. The deny text is written **for the model** — it names the holder, its intent, its branch and the exact call to make next. A `PostToolUse` hook auto-claims whatever was written. A git `pre-commit` hook is the vendor-neutral floor for agents without hooks, and for humans. |
| **Radar** | Snapshots of *dirty* worktrees taken through a temporary index (never mutating the working tree), compared pairwise with `git merge-tree --write-tree`. The tree is built twice: if the two builds disagree the snapshot is reported `UNSTABLE`, never `CLEAN`. Statuses: `CLEAN` / `TEXTUAL` / `STRUCTURAL` / `HEAVY`, with ConE-style noise filters. |
| **Queue** | FIFO under `flock`, one entry in flight. Rebase in a scratch worktree, then a **mandatory** test gate, then fencing against the global high-water epoch, then a `git update-ref` CAS into a staging branch. A conflict bounces back to the author with instructions ("your context is freshest"), and after every landing everyone is told the target moved. |

Hard guarantees exist at exactly one place: the landing path. Hooks and `pre-commit` are
cooperative admission control and telemetry, not a security boundary — `ASEM_HOOK_OFF=1`
and `ASEM_OVERRIDE=1` are documented, audited escape hatches. This is stated up front
because a coordination layer that pretends to be a sandbox is worse than none.

## Quickstart

```bash
uv tool install git+https://github.com/alwh1te/agent-semaphore     # asem on PATH
# or, from a clone: uv tool install -e .

cd <your repo>
curl -O https://raw.githubusercontent.com/alwh1te/agent-semaphore/main/.agent-semaphore.toml.example
mv .agent-semaphore.toml.example .agent-semaphore.toml   # set the gate command, hot classes, target branch
asem init                                   # state in .git/agent-semaphore/
asem install --git-hooks                    # Claude Code hooks + .mcp.json + git pre-commit
asem doctor                                 # PASS checklist
```

```bash
asem claim src/api/ -i "refactor auth parsing" --ttl 30m   # exit 3 = held by someone else
asem check src/api/routes.py                               # who holds it, and what for
asem radar                                                 # conflicts between worktrees, before any commit
asem land feature-branch                                   # rebase -> gate -> CAS into the staging branch
asem notices                                               # messages addressed to you
asem status | asem queue status | asem doctor
```

Exit codes are part of the contract: `0` ok/free, **`3` held/conflict/bounced**, `2`
usage, `1` internal error — a script can tell "coordination said no" from "the tool
broke".

## Measured

Nobody in this space had measured whether claims actually reduce conflicts, so the repo
ships two benchmarks of its own.

**Scripted** ([`docs/benchmark.md`](docs/benchmark.md), 60 runs, deterministic agents,
compliance = 1 by construction): integration conflicts **60% → 0%**, human interventions
**9 → 0**.

**Live agents** ([`docs/bench-llm.md`](docs/bench-llm.md), 40 runs of two concurrent
`claude -p` agents, $17.95):

| mode | ICR | WME | did_work | caught-up | $/run | COR |
|---|---|---|---|---|---|---|
| no coordination | 40% | 2 | 100% | 0% | $0.34 | 1.00x |
| advisory claims | 20% | 1 | 100% | 40% | $0.50 | 1.72x |
| claims + radar | 10% | 2 | 80% | 40% | $0.46 | 1.93x |
| strict + queue | **0%** | **0** | 100% | 40% | $0.50 | 1.98x |

Three findings the scripted harness structurally could not produce:

1. **Conflicts are removed by the catch-up, not by the claim.** 10 out of 10 runs where
   an agent rebased onto its peer's branch merged clean; every conflicted coordinated run
   is one where both agents claimed politely and neither rebased. A claim serializes
   *writing* — it does not hand you the other agent's *result*. That finding is what
   produced the "release wakes the waiters and names the branch" feature.
2. **The hook never fired once in 40 runs.** With the protocol in the prompt, agents
   claim before editing and never write into a held scope, so enforcement turned out to
   be insurance that was not needed — not the working layer.
3. **Coordination can convert a conflict into work that never happened.** In two runs the
   blocked agent quoted the holder, its intent and its branch, and abandoned its task.
   Without the `did_work` column next to ICR, those runs read as a clean success — which
   is why the column is there.

Semantic drift (textually clean, semantically broken) survives every advisory layer in
both benchmarks and is caught **only** by the queue's mandatory gate.

## How it plugs in

- **Claude Code** — `asem install` writes project `.claude/settings.json` (PreToolUse +
  PostToolUse), adds `Bash(asem:*)` and `mcp__semaphore__*` to the allow-list, and
  registers the MCP server in `.mcp.json`. Committed wiring is host-portable (`$HOME`
  and a bare `asem`), so a repo shared across machines does not carry one host's paths.
- **MCP** (`asem mcp`, server key `semaphore`) — `claim`, `release`, `check`, `status`,
  `extend`, `report_intent`, `radar`, `enqueue_land`, `land_status`. Every response
  drains pending notices, so agents learn about steals, bounces and moved targets without
  polling.
- **Codex CLI** — same MCP server via `~/.codex/config.toml`, plus a protocol snippet for
  `AGENTS.md`. Headless Codex silently cancels MCP calls unless the tools are
  pre-approved; [`docs/integration.md`](docs/integration.md) has the working config.
- **Anything else** — `asem install --git-hooks` puts a `pre-commit` gate in the shared
  hooks dir (it chain-loads whatever hook was there before).

## Documentation

- [`docs/00-research.md`](docs/00-research.md) — the research dossier this design is
  built on: the measured problem, the tool landscape, the classic prior art worth
  stealing from, the 2024–26 papers, and the three confirmed gaps in the market.
- [`docs/adr/ADR-001-architecture.md`](docs/adr/ADR-001-architecture.md) — the
  architecture, plus a risk register and ten adversarial attacks from a blind review
  (eight of them changed the design).
- [`docs/adr/ADR-002-stack-and-state.md`](docs/adr/ADR-002-stack-and-state.md) — stack,
  naming, and where state lives.
- [`docs/benchmark.md`](docs/benchmark.md) / [`docs/bench-llm.md`](docs/bench-llm.md) —
  both benchmarks: method, results, and the limitations spelled out.
- [`docs/integration.md`](docs/integration.md) — installation and what is wired where.

## Status

v1 is implemented and dogfooded: the repository coordinates its own agents through it.
150 tests, a p95 hook-latency gate in CI, both benchmarks reproducible from the repo.

Known limits, stated plainly: promotion from the staging branch to `main` is still manual
and ungated (`asem promote` is the next feature); the queue never pushes; there is no
symbol-level scoping, no semantic conflict detection beyond the test gate, no LLM
auto-resolution (the published ceiling is ~55–60% correctness, which is not good enough
to run unattended); and multi-host is a v2 design, though the schema already carries the
`host` column.

## Development

```bash
uv run pytest -q                          # 150 tests
uv run ruff check . && uv run ruff format --check .
uv run python bench/hook_latency.py 200   # hook latency gate (p95 < 100 ms)
uv run python bench/runner.py --seeds 3 && uv run python bench/report.py
```

The `PreToolUse` hook script is vendored outside the package and must stay stdlib-only —
it runs on every write of every agent, so it has a latency budget rather than
dependencies. See [`CONTRIBUTING.md`](CONTRIBUTING.md).

## License

MIT — see [LICENSE](LICENSE).