# Agent Sync

> Use when several coding agents work one repository at the same time and must not collide - claiming a task, reserving the next decision/question/ticket id, journaling a run, moving a cross-repo dependency, regenerating the board, or finishing work across a parent repo and its submodules - clean, pushed, and pointed at. Triggers - 'claim this task' / 'возьми задачу', 'who is working on' / 'кто сейчас делает', 'reserve an id' / 'зарезервируй id', 'sync the board' / 'обнови доску', 'set up agent coordination' / 'настрой координацию агентов', 'the submodule is pushed, the parent points at the old commit' / 'сабмодуль запушен, родитель на старом коммите', /agent-sync. Use it BEFORE editing any shared registry file (decisions, open questions, roadmap, workstreams, dependencies) in a project that has .claude/agent-sync.json, even when the user never mentions coordination - an unclaimed edit to those files is how two agents overwrite each other.

- Skill: `ssheleg/agent-sync` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add ssheleg/agent-sync`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ssheleg/agent-sync/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- License: MIT
- Author: ssheleg (https://skillmd.com/u/ssheleg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ssheleg/agent-sync

---


# agent-sync — one project, many agents, no collisions

Two planes, and one rule between them:

> **Git is the record plane. The cloud is the coordination plane.**
> A fact that must survive is written to git first and referenced from the cloud.
> A fact about *who is doing what right now* lives in the cloud and expires.

No cloud object is ever the only home of a durable fact. Everything below exists to
keep that true while several agents write at once.

## Four traps — read these before anything else

**1. The knowledge base never decides a lease.** It cannot: twelve concurrent appends to
one Outline document returned twelve successes and left **three** lines. Exclusion comes
from something with real compare-and-swap. The plane carries the record and nothing else.

**2. Know which lease you have, and say so.** `leaseBackend: "local"` is an atomic file
create — exclusive between processes on one filesystem, **advisory across machines**.
`leaseBackend: "git"` pushes a ref, and the remote's non-fast-forward rejection **is** a
compare-and-swap — exclusive across machines. `acquire` prints which. A pretended lease is
worse than no lease: the other agent stops checking.

**`acquire` also writes the claim through to the roadmap**, and `release` restores exactly
what was there — one row, one cell, refused on ambiguity, `git diff` empty after a
round-trip. **Read `references/roadmap.md`** before configuring `claimTags` or closing a
task; closing is a statement about the work and stays yours.

**Work on a branch; the integration branch is somebody else's stable base.** `acquire`
writes the claim through **only** there — on any other branch the holder stays in the
coordination plane, where `status` shows it to every agent. Committed to a branch, a claim
is invisible until the merge and turns the shared roadmap into a file two branches both
edit. Land work with `merge`: conflicts computed **before** anything is touched, the merge
recorded in `docs/MERGES.md`, the `--key` lease released; `merges` says what landed while
you were away. **Read `references/branching.md`** before merging.

**3. Hooks exist only in Claude Code.** Elsewhere nothing blocks a guarded edit: run
`guard` yourself and record the run as `ungated`. A mode's five capability fields
live in ONE place — `references/adapter-contract.md` → *The status capability
contract*, never restated here. Do not describe a project as protected when it is not.

**4. Parse liberally, and never call an unreadable log a lost race.** The store rewrites
what you wrote — Outline turns a `- ` bullet into `* `. Emit `- `, accept `-`/`*`/`+`,
count anything entry-shaped that fails, and **fail loudly** past 2% unparseable. Reporting
`lost` when the truth is *unreadable* names a holder who does not exist. Watch for a
silent pre-filter: a `continue` before the regex hides bad lines from the counter.

## Bringing this into ANY project — the whole chain

```
scaffold  → create the documentation architecture, only where it is absent
adopt     → read the repository, propose a config, write nothing
init      → write the approved config + the gitignored env file
            (operator pastes the token — never you)
reconcile --set-baseline    → make history a counted backlog, once
setup     → generate the snapshot that describes this project's wiring
check     → validate the whole thing; non-zero if it is not healthy
```

**`check` is what makes the skill self-sufficient.** It refuses to call a setup healthy on a
rule that protects nothing (a register, guard glob, claim pattern, gate or mirror source
pointing at what is not there), on missing credentials, on an env file **tracked by git** —
the one unrecoverable mistake here — on a stale snapshot, or on a register with no baseline.
It names each one; every one failed for real during this tool's own adoption. Run it after
adopting, after changing the config, and in CI.

**`scaffold` never overwrites.** It seeds a decision register and an `AGENTS.md` pointing at
the snapshot, and leaves every existing file untouched.

## Existing project: start with `adopt`

Run `adopt` before `init`. It reads the repository and prints what it found — id
registers, registry files, gates — plus the decisions it **refuses to make for you** —
then proposes a config.

```bash
python3 "$SKILL_DIR/scripts/agent_sync.py" adopt
```

Confirm the registers and guarded files with the operator first: a register pointed at the
wrong file makes every later check confidently wrong, and a guarded list that misses a shared
file leaves the one place collisions happen unprotected. In a submodule it declares no
registers — decisions belong to the parent. Then take the chain above from `init`.

## First command in a project: `init`

**Never run anything else against an uninitialised project.** `init` asks and answers
the storage question once, and writes it down.

**Ask the operator these two things in chat — do not guess, do not pick a default:**

1. **Where should coordination state live?**
   - a knowledge cloud — `outline`, hosted or self-hosted, or `notion` — the shared
     record, awareness and board across machines — **neither decides leases** (trap 1);
   - or local files (`fs`) — no credentials, and no visibility to an agent on another
     machine — no shared awareness, cross-repo signal or board.

   The lease is decided separately by `leaseBackend` (trap 2), and **`gated` follows that
   choice, never the record plane** — report the guarantee you actually have.
2. **If cloud: where.** Outline needs its instance URL; Notion needs the id of the page
   the container goes under. Both are configuration, not secrets, so you may write them.
   The **token is not** — you never ask for it in chat, never read it back, and never
   place it yourself.

Then run it with their answers:

```bash
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend outline --url https://<their-instance>
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend notion
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend fs
```

`init` writes `.claude/agent-sync.json` (shape, committed) and `.env.agent-sync` with an
**empty** token line (identity, mode 600), gitignores both, and prints the one step that is
the operator's — creating the token and pasting it into that line. Never overwrites either
file without `--force`. **Relay those closing instructions verbatim**; the design depends on
the token staying theirs.

## Then, before every session

```bash
python3 "$SKILL_DIR/scripts/agent_sync.py" status
```

Idempotent. Inspects, repairs, reports, names exactly ONE next action — and carries
`check`'s verdict, so the command every session runs and the one that validates the setup
cannot give two answers about one project.

**Read the two awareness sections it prints — they are the point, not decoration.**

- **Other runs working this project right now.** Who holds what, this minute. Do not take
  those on, and do not "just look at" the files they cover.
- **New since you last looked.** Cross-repo dependency moves that landed while you were
  away — watermarked per run, so it stays quiet until something changes. A dependency that
  moved may unblock what you planned, or invalidate it.

`status` also decides: no credentials → degraded mode; `task-pipeline` absent → it
prints the install line and stops. Absence is checked across every host layout or an
explicit `pipelinePath` — no host's copy masks or is missed. The lease core
(`acquire`/`renew`/`release`) needs a backend + lease, not the binding.

```bash
npx sshlg-skills install
```

## The commands

| Command | Does |
|---|---|
| `init` | **Run first.** Ask where state lives, write config + gitignored env file, print the operator's step |
| `status` | Inspect, repair, report, name one next action |
| `bootstrap` | Create the cloud container and print the id to paste into the env file |
| `acquire <KEY>` | Take the lease on a task id. Prints `won` or `lost <holder>` |
| `renew <KEY>` | Extend the lease. The `PostToolUse` hook does this for you |
| `release <KEY>` | Give the lease back. Always do this, including on failure |
| `reserve <REG> [--key K] [--offline]` | Reserve the next id in a register (`DEC`, `OQ`, `DEP`, …); prints it. `--key` makes a retry idempotent (one key, one number); `--offline` issues a namespaced `REG-o-…` id with no global authority |
| `map-offline <REG> <ID> <N>` | Bind an offline id to a properly reserved number — append-only, never rebound |
| `release-id <REG> <ID>` | Return an id you did not end up writing to git |
| `journal <text>` | Append one line to this run's journal |
| `record <text>` | Append what you **actually built** — `--decision DEC-…`, `--files a,b` |
| `reconcile` | Intent (git) vs as-built (cloud). `--set-baseline` once per project |
| `signal <DEP-ID> <state>` | Move a cross-repo dependency: `filed`/`accepted`/`delivered`/`closed`/`refused` |
| `guard <path>` | Answer whether this run may write that path. Exit 0 = yes, 2 = no |
| `board` | Regenerate the shared board and this repo's page. `--mirror` also renders the configured git docs into the plane |
| `whoami` | Print this run's id and its held leases |
| `residue` | What a run left behind in BOTH planes: expired locks on disk, and claim tags with no live lease behind them |
| `reap [KEY…]` | Clear only what this run **proves** it owns and has spent, then re-read the directory to confirm it went |
| `setup` | Write the generated snapshot of how **this** project is wired, for agents to read |
| `adopt` | Inspect an existing project and **propose** a config — writes nothing |
| `merge` | Land this branch: target fast-forwarded, conflicts checked **before** anything is touched, merge log written, the `--key` lease released. `--summary`, `--dry-run`, `--push` |
| `merges` | What landed while you were on your branch. `--all` includes the compacted tail |
| `check` | Validate the whole setup end to end. Non-zero when it is not healthy |
| `scaffold [--full]` | Create only what is missing, never a line over what exists. `--full` also seeds the question register, index, dependency board, data model and docs gate |
| `finish [--gates]` | Is the **work** finished — every repository clean, pushed and pointed at, nothing left held. `check` answers whether the project is wired right; this, whether you are done |

`$SKILL_DIR` is this skill's own directory: `${CLAUDE_PLUGIN_ROOT}/skills/agent-sync` under
the Claude Code plugin, `~/.agents/skills/agent-sync` elsewhere. Resolve it once per session
and reuse it. Every command reads `.claude/agent-sync.json` from the project
root and needs no arguments beyond those listed.

## One identity per session, and how it is decided

A lease is only a lease if two agents get two identities, and both ends have bitten:
`CLAUDE_SESSION_ID` alone gave **one session two identities** — it acquired as one and was denied by
its own guard as the other — and one id per checkout gave **two sessions one identity**, silently,
so `release` took a lease the caller never had.

The order is: `AGENT_SYNC_RUN_ID` · `CLAUDE_SESSION_ID` · **the session that started this shell** ·
shared. The third exists because a plain shell command has no session id and a hook does, so
`SessionStart` stamps `.agent-sync/sessions/<CLI pid>` and a later command walks its own process
ancestry to it. Why that and not command-line parsing: `references/earned-rules.md`.

When none of the four can be established the run says so — *"this identity is shared with any other
session in this checkout"* — rather than presenting a shared entry as separation.

## Claiming — the shape that matters

```
acquire → do the work → release
```

Never skip `release`, including on failure: an abandoned lease blocks the task until its
TTL expires, and the next agent cannot tell "in progress" from "crashed an hour ago".

**The lease is not the claim.** The lease says who holds it *now* and expires; the durable
claim is the tag in git, written through by `acquire` and cleared by `release`. One fact,
one home — do not invent a third place that records ownership.

**One notion of held, consulted by both planes.** The TTL ends the lease, so a tag naming a
run whose lease has ended is residue: reported by `status`, `residue` and `reconcile`, and
cleared by `release <KEY>`, which says whose it was. A tag whose lease is still live is
never touched by another run.

**Read `references/lease-protocol.md`** before changing acquisition, expiry, stealing or
id allocation.

## Guarded files

The config lists registry files several agents write. Before editing one:

```bash
python3 "$SKILL_DIR/scripts/agent_sync.py" guard docs/DECISIONS.md
```

**Exit 2 is about *this run*: it holds no lease** — not that somebody else holds that file.
A denial names the other run **and its key**, because "r-x holds a lease" beside a path gets
repeated as "r-x holds this file". Do not edit anyway, and do not "just fix one line" — a
clobbered decision looks exactly like a decision.

**Two write modes, and the tool does not promise the wrong one.** A task lease authorizes
the TASK, never the file — so a guarded write also takes the FILE's own **resource claim**
(`res--<repo>--<canonical path>`, SY-04). That is the **short transaction lock**: two agents
on one shared registry serialize on it, independent files never serialize, honest
cross-machine only under `leaseBackend: "git"` (advisory otherwise). The other mode is
**isolated worktree + merge** — each agent a private checkout, a merge policy reconciling
them — for when writes overlap so heavily a lock would just queue everyone. What the guard
does NOT promise is enforcement from a single task owner: holding *a* lease was never
holding *this* file.

Claude Code's `PreToolUse` hook runs this for you. Elsewhere nothing does.

## Reserving an id

Reading a "Next free ID" line is not reserving it — two agents read the same number and both
use it.

```bash
python3 "$SKILL_DIR/scripts/agent_sync.py" reserve DEC   # → DEC-0216
```

Allocation is positional over the **merged** log — every shard, never just this run's — so
every agent computes the same answer. Reserved and not written to git? `release-id` it, or the
number is a hole the board reports as a leak.

## Nothing in a log is ever edited or deleted

Logs are **replayed in order**, so an edit silently rewrites a conclusion other agents already
acted on. Correct by **appending**: release a lease, `release-id` an unused id, supersede a
wrong as-built entry with a later one. Generated pages are the only exception. Lifetimes and
the generated-object contract: `references/two-sources.md`.

## Two documentation sources, and the duty to reconcile them

Git docs answer **how it should be**; the as-built record answers **how it actually is**.
Neither outranks the other, and **the gap between them is the finding**, not a defect. The
duty runs at both ends of a task: `reconcile` before starting, `record` and `reconcile`
after finishing — and `reconcile` is mechanical, so treating its green as agreement is how
a divergence survives both ends.

**Read `references/two-sources.md`** before the first reconcile: it holds the full duty,
what `reconcile` refuses to judge, the generated `setup` snapshot, where a document
belongs, and why nothing is deleted.

## Binding to task-pipeline

This skill supplies stages; the names are `task-pipeline`'s own.
Five of the eleven stages carry an ordering rule: **0** `acquire` before the brief
commits; **1** `reconcile` before code; **3** `reserve` ids before git; **9** the main
write; **10** `merge`/`release` every lease.

**Read `references/pipeline-binding.md`** when wiring `pipeline.json` — per-stage
reasoning, `skills[]` entries, what to guard, and the gate expressions.

## Configuration

Two files, and the split between them is the whole security model.

**`.claude/agent-sync.json`** — *shape*, committed: which backend, TTLs, which files
are guarded, which registers exist, which gates to run.

**`.env.agent-sync`** — *identity*, written by `init` with the keys already in it, mode
600, gitignored. The operator fills the token line and nobody else; `bootstrap` prints
the collection id to paste beside it. Found here, or in a superproject, or wherever
`AGENT_SYNC_ENV` points — `check` prints which. Load it before running agents:

```bash
set -a && . ./.env.agent-sync && set +a
```

Never write a host name or token into the config, a test, an example or a commit; never
handle, echo or `argv`-pass a token value. Offered one in chat, point at that file instead.

**A submodule's config declares only its own registers.** Cross-repository facts belong to
the parent; a service repo listing the parent's decision register is a config defect.

## Non-negotiables

- Append, read back, then act. Never rewrite a coordination document.
- A generated object without its `agent-sync:generated` marker is **refused**, not
  overwritten — a human took it over. Contract: `references/two-sources.md`.
- `release` what you `acquire`, on every path including failure.
- A run reports what it leaves behind: expiry ends a lease, not the file — `residue`.
- Credentials never reach `argv`, a log line, or the repository.
- Degrade out loud. `ungated` is an acceptable state; a false claim of enforcement is not.
- Two agents in one checkout are two identities, or the lease is decoration.
- A submodule commit is unfinished until the parent points at it — run `finish`.
- Everything the cloud holds about a durable fact is a link to git, never a substitute.
- Boundary: `.claude/agent-sync.json`. None, or one agent alone — route past. The
  operator's waiver: "no coordination" / «без координации».

## References

Each file is loaded on its own trigger, not by default.

| File | Read it when |
|---|---|
| `references/adapter-contract.md` | adding or auditing a knowledge backend — six primitives, the capability flags, an honest degradation path |
| `references/lease-protocol.md` | changing acquisition, expiry, stealing or id allocation |
| `references/backend-outline.md` | making any Outline API call, or debugging one |
| `references/backend-notion.md` | making any Notion API call, or debugging one |
| `references/backend-fs.md` | running without a cloud backend, or explaining degraded mode |
| `references/pipeline-binding.md` | wiring `pipeline.json`, or adding a stage hook |
| `references/hooks.md` | installing, debugging or removing the Claude Code hooks |
| `references/two-sources.md` | before the first reconcile, deciding where a document belongs, or writing a generated object |
| `references/roadmap.md` | configuring `claimTags`, taking or closing a task, or re-planning a board |
| `references/branching.md` | starting work that will produce commits, merging a branch, or asking what landed while you were away |
| `references/earned-rules.md` | asking why identity resolves the way it does, or why `finish` exists |

Missing `references/`? Fetch from `https://raw.githubusercontent.com/ssheleg/agent-sync/main/plugins/agent-sync/skills/agent-sync/references/<file>`.

