Skip to content
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,23 +222,27 @@ edit (locks); there is no mid-tool-call interrupt — see [Limitations](#limitat
| --- | --- |
| `agent-sync init` | Create the database and tables (auto-runs on any command). |
| `agent-sync register --name N [--role R]` | Register / update the current agent. |
| `agent-sync heartbeat` | Mark the current agent active now. |
| `agent-sync status [--compact]` | Show agents, tasks, locks, messages, activity. |
| `agent-sync tasks` | List all tasks. |
| `agent-sync create-task "T" [--description D] [--file P ...] [--priority N]` | Create a task. |
| `agent-sync claim-task T` | Claim a task by id or title. |
| `agent-sync claim-next` | Auto-claim the next available task (highest priority first; reclaims tasks abandoned by crashed sessions). |
| `agent-sync complete-task T` | Mark a task done. |
| `agent-sync whoami [--json]` | Show your resolved agent id and how it was determined. |
| `agent-sync heartbeat` | Mark the current agent active now (use during long quiet stretches to avoid going stale). |
| `agent-sync status [--compact] [--json]` | Show agents, tasks, locks, messages, activity (`--json` for machine-readable state). |
| `agent-sync tasks [--json]` | List all tasks. |
| `agent-sync create-task "T" [--description D] [--file P ...] [--priority N] [--depends-on T ...]` | Create a task (optionally blocked on other tasks). |
| `agent-sync claim-task T [--lock] [--force]` | Claim a task by id or title (`--lock` locks its files; `--force` overrides unmet dependencies). |
| `agent-sync claim-next [--lock]` | Auto-claim the next available task (highest priority first; skips dependency-blocked tasks; reclaims tasks abandoned by crashed sessions). |
| `agent-sync complete-task T` | Mark a task done (reports any dependents it unblocks). |
| `agent-sync block-task T --reason R` | Mark a task blocked. |
| `agent-sync lock FILE [--reason R] [--ttl MIN]` | Lock a file (default TTL 60 min). |
| `agent-sync unlock FILE [--force]` | Release a lock (owner only, unless `--force`). |
| `agent-sync locks [--all]` | List live locks (`--all` includes expired). |
| `agent-sync send --to R --message M` | Send to an id, name, role, or `all`. |
| `agent-sync inbox [--all]` | Show unread (or all) messages addressed to you. |
| `agent-sync lock FILE [--reason R] [--ttl MIN] [--wait[=SEC]]` | Lock a file (default TTL 60 min; `--wait` blocks until free). |
| `agent-sync lock --resource KEY [...]` | Lock an arbitrary named resource instead of a file path. |
| `agent-sync unlock FILE \| --resource KEY [--force]` | Release a lock (owner only, unless `--force`). |
| `agent-sync append FILE [--content T] [--wait[=SEC]] [--no-newline]` | Atomically append to a shared file under a lock (body from `--content` or stdin). |
| `agent-sync locks [--all] [--json]` | List live locks (`--all` includes expired). |
| `agent-sync send --to R --message M [--reply-to ID]` | Send to an id, name, role, or `all` (optionally threaded). |
| `agent-sync inbox [--all] [--json]` | Show unread (or all) messages addressed to you. |
| `agent-sync read-message ID` | Show a message and mark it read. |
| `agent-sync ack ID` | Acknowledge a message so its sender knows it was handled. |
| `agent-sync decision "..."` | Record a shared decision. |
| `agent-sync log --type T --message M [--file P]` | Append an activity entry. |
| `agent-sync gc` | Re-status stale agents and drop expired locks. |
| `agent-sync log "message" [--type T] [--file P]` | Append an activity entry (message is positional; `--message` still accepted). |
| `agent-sync gc` | Re-status stale agents and drop expired locks (also runs automatically on `SessionStart`). |
| `agent-sync console [--interval S] [--name N]` | Live operator console: stream activity and steer agents (needs the `tui` extra). |
| `agent-sync hook {session-start,user-prompt-submit,pre-tool-use,post-tool-use,stop,session-end}` | Hook entry points (read JSON from stdin). |

Expand All @@ -252,7 +256,7 @@ into your repo's `.claude/settings.json` (or run an installer with

| Event | Matcher | Behaviour |
| --- | --- | --- |
| `SessionStart` | (all) | Register/heartbeat the agent; inject compact status into context. With `AGENT_SYNC_AUTO_CLAIM=1`, also hand a free agent its next task (`claim-next`). |
| `SessionStart` | (all) | Garbage-collect stale agents/expired locks; register/heartbeat the agent; inject compact status into context. With `AGENT_SYNC_AUTO_CLAIM=1`, also hand a free agent its next task (`claim-next`). |
| `UserPromptSubmit` | (all) | Push any undelivered messages (directed + broadcast) into context for this turn. |
| `PreToolUse` | `Edit\|Write\|MultiEdit` | **Block (exit 2)** if the target file is locked by another active agent. |
| `PostToolUse` | `Edit\|Write\|MultiEdit` | Log the successful edit to the activity feed. |
Expand All @@ -273,15 +277,30 @@ environment variables where the hooks run (for example an `"env"` block in
- `AGENT_SYNC_AUTO_RELEASE_LOCKS=1` — `SessionEnd` releases the agent's locks
immediately instead of leaving them to expire.

**Tuning.** A couple of thresholds can be overridden via the environment:

- `AGENT_SYNC_ID` — act as a specific agent id (set a distinct value per parallel
subagent so their locks stay mutually exclusive; otherwise identity is
auto-detected from the Claude Code session). `agent-sync whoami` shows the
resolved id and its source.
- `AGENT_SYNC_STALE_MINUTES` / `AGENT_SYNC_OFFLINE_MINUTES` — how long without a
check-in before an agent is considered stale (default 15) / offline (default
120). Lower them for short-lived sessions, raise them for long quiet ones.
- `AGENT_SYNC_ROOT` — force the coordination root directory (otherwise the repo
root is auto-discovered).

## Data storage

All state lives in **`.claude/coordination/state.sqlite`** inside the target
repo, created automatically on first use. Tables:

- `agents` — id, name, role, session, cwd, status, current task, timestamps.
- `tasks` + `task_files` — the task board and the files each task touches.
- `locks` — one row per locked path, with owner and `expires_at` (TTL).
- `messages` — sender, recipient (id/name/role/`all`), body, read state.
- `tasks` + `task_files` + `task_deps` — the task board, the files each task
touches, and dependency edges between tasks.
- `locks` — one row per locked path or named resource, with owner, `kind` and
`expires_at` (TTL).
- `messages` — sender, recipient (id/name/role/`all`), body, read/ack state,
optional reply-to thread parent.
- `message_deliveries` — per-(message, agent) record of which messages have been
pushed into which agent's context (so a broadcast reaches each agent once).
- `decisions` — recorded decisions.
Expand Down
8 changes: 8 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ file lock:
Locks reliably prevent *honest* collisions between cooperating agents; they do
not contain a session that ignores them.

## `append` writes to your working tree

Almost every command only touches the coordination database. The exception is
`agent-sync append`, which **writes to a file in your working tree** (after
taking the lock on it). Like any agent action that edits files, only run it
against paths you intend to modify; it appends arbitrary content (from
`--content` or stdin) to the named file and creates parent directories as needed.

## Reporting a vulnerability

If you discover a security issue, please **do not open a public issue**. Instead:
Expand Down
93 changes: 85 additions & 8 deletions skills/agent-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ so nobody clobbers anybody else's edits.
**Always run `agent-sync status --compact` before you start working** and treat
the result as authoritative about who else is active and which files are locked.

## TL;DR — the loop (the 90% case)

1. `agent-sync register --name <you> --role "<what you do>"` — once per session.
2. `agent-sync claim-next --lock` — take the next task *and* lock its files.
3. Do the work. For a file **many** agents write to, use `agent-sync append`
instead of editing it directly.
4. `agent-sync complete-task <id>` — finish it (and auto-unblock dependents).

If a file you need is locked by someone else: `agent-sync lock <file> --wait=60`
— it blocks until the lock frees, then succeeds. During long stretches without
edits, run `agent-sync heartbeat` so you are not mistaken for crashed. The
numbered rules below cover the details and the multi-agent edge cases.

When the coordination hooks are installed, messages from other agents are
**pushed to you automatically**: any new ones are injected into your context at
the start of each turn (`UserPromptSubmit`), and a message addressed to you
Expand All @@ -26,7 +39,21 @@ to obey. When one calls for a reply, answer with `agent-sync send`.
Your identity is **detected automatically** from the active Claude Code session
(via the `CLAUDE_CODE_SESSION_ID` it exports), so every command you run below
already acts as *this* window's agent — you do not need to set `AGENT_SYNC_ID`.
A `register` once per session just gives you a friendly name and role.
A `register` once per session just gives you a friendly name and role. Run
`agent-sync whoami` any time to confirm which id you are acting as and how it was
resolved.

> **Fanning out parallel subagents?** Subagents spawned from one session inherit
> the *same* `CLAUDE_CODE_SESSION_ID`, so by default they collapse into a single
> agent and their locks are **not** exclusive of each other. If you dispatch
> parallel subagents that lock/edit files, give **each one a distinct**
> `AGENT_SYNC_ID`:
> - Set it once at the top of each subagent: `export AGENT_SYNC_ID=sub-frontend`.
> If your shell does not persist env between commands, prefix **every** call
> instead: `AGENT_SYNC_ID=sub-frontend agent-sync lock ...`.
> - Each subagent can run `agent-sync whoami` first to verify it is a *distinct*
> agent (the `resolved via` line should say `AGENT_SYNC_ID env var`). If two
> subagents show the same id, their locks will not protect them from each other.

## Current coordination state

Expand All @@ -52,12 +79,38 @@ agent-sync status --compact
across sessions without a human dealing it out.
- `agent-sync create-task "Title" --description "..." --file path/a --file path/b`
- `agent-sync claim-task "Title or task-id"` — when you want a *specific* one.
- Add `--lock` to `claim-task`/`claim-next` to also lock the task's `--file`
list in one step, closing the gap between owning the task and owning its files.
- Express ordering with `--depends-on`: a task with an unfinished dependency is
skipped by `claim-next` and refused by `claim-task` (until you pass `--force`),
and becomes claimable automatically when the dependency completes:
- `agent-sync create-task "Wire up UI" --depends-on "Backend API" --file src/ui.js`
3. **Lock files before editing them:**
- `agent-sync lock path/to/file --reason "what you're changing"`
- Locks have a 60-minute TTL by default and auto-expire.
4. **Never edit a file that is locked by another *active* agent.** If the
`PreToolUse` hook is installed it will block you with exit code 2; even
without it, respect the lock shown in status.
- Lock a non-file shared resource (a migration run, a release process, a
codegen step) by key instead of path: `agent-sync lock --resource db-migrations`.
- **Which to use:** `lock` + your editor when *you* own and rewrite a file;
`append` for a file **many** agents add lines to (a shared log, changelog,
aggregated output) — **do not improvise with `>>`.** `append` locks, appends
and unlocks in one atomic step, so concurrent writers never interleave:
- `agent-sync append CHANGELOG.md --content "- did X" --wait=30`
- or pipe the body: `some-command | agent-sync append build.log --wait=30`
4. **If a file is locked by another *active* agent, do NOT edit it. Follow this
protocol instead** (the `PreToolUse` hook also blocks such edits with exit 2):
1. Wait for it: `agent-sync lock path/to/file --wait=60` blocks until the lock
frees (the holder unlocks, goes stale, or the TTL expires), then succeeds.
Bare `--wait` waits 30s. **This is how you wait — do not sleep or busy-retry
yourself; the command blocks for you.**
2. If it still fails, message the owner and pick up other work meanwhile:
`agent-sync send --to <owner> --message "need to edit path/to/file — ping me when free"`
then `agent-sync claim-next` for something else.
- The same applies to `agent-sync append`: it takes the lock for you, so on a
busy file pass `--wait`; if it still times out (exit 2), fall back the same
way (message the owner, do other work, retry later).
- To decide programmatically whether to wait or move on, read structured state
with `agent-sync locks --json` / `agent-sync status --json` rather than
parsing the human text.
5. **Communicate changes that affect others.** Send a message whenever you
change an API contract, a shared file, a migration, a config, or make an
architecture decision:
Expand All @@ -73,9 +126,32 @@ agent-sync status --compact
automatically when the hooks are installed, but you can also pull them — check
when status reports unread messages, and reply to anything that needs an answer:
- `agent-sync inbox` then `agent-sync read-message MESSAGE_ID`
- `agent-sync send --to <sender> --message "..."` to reply.
- `agent-sync send --to <sender> --message "..." --reply-to MESSAGE_ID` to reply
in-thread.
- `agent-sync ack MESSAGE_ID` to confirm to the sender you have handled it
(distinct from just reading it).
9. **Prefer git worktrees** for large parallel features so each agent edits an
isolated checkout; still lock shared/generated files (lockfiles, schemas).
isolated checkout. All worktrees of one repo share a single coordination
database (it resolves to the main worktree), so agents across worktrees see
each other; still lock shared/generated files (lockfiles, schemas).

## Staying "alive" during long work

An agent that has not checked in for ~15 minutes is treated as **stale**, and a
stale agent's locks and claimed task can be taken over by others (this is what
lets a crashed session's work be reclaimed). Every `agent-sync` command — and,
when the hooks are installed, every file edit — counts as a check-in, so during
normal active work you never go stale. But if you will be quiet for a while
(long reasoning, a big non-editing build/test run), send a heartbeat so you keep
your locks and task:

```bash
agent-sync heartbeat
```

If you *did* go stale and lost a lock, just re-acquire it (`agent-sync lock ...`)
before continuing. The thresholds can be tuned per environment with
`AGENT_SYNC_STALE_MINUTES` / `AGENT_SYNC_OFFLINE_MINUTES`.

## Common workflows

Expand Down Expand Up @@ -121,5 +197,6 @@ agent-sync unlock src/login.tsx
order.
- Do **not** put secrets (tokens, passwords, keys) into task titles, messages or
decisions — they are stored in plaintext and meant to be read by every agent.
- If a lock is stale because an agent crashed, run `agent-sync gc` to clear
expired locks and re-status inactive agents.
- Stale state is cleaned up automatically: the `SessionStart` hook runs a `gc`
pass each time a session starts, so a crashed agent's expired locks do not block
the next one. You can still run `agent-sync gc` manually any time.
Loading