@ssheleg/agent-sync 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +332 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/agent-sync.example.json +40 -0
- package/agent-sync.schema.json +144 -0
- package/bin/agent-sync.js +149 -0
- package/package.json +46 -0
- package/plugins/agent-sync/.claude-plugin/plugin.json +18 -0
- package/plugins/agent-sync/commands/agent-sync.md +16 -0
- package/plugins/agent-sync/hooks/_lib.sh +39 -0
- package/plugins/agent-sync/hooks/guard.sh +55 -0
- package/plugins/agent-sync/hooks/hooks.json +69 -0
- package/plugins/agent-sync/hooks/renew.sh +10 -0
- package/plugins/agent-sync/hooks/session-end.sh +14 -0
- package/plugins/agent-sync/hooks/session-start.sh +8 -0
- package/plugins/agent-sync/skills/agent-sync/SKILL.md +372 -0
- package/plugins/agent-sync/skills/agent-sync/references/adapter-contract.md +80 -0
- package/plugins/agent-sync/skills/agent-sync/references/backend-fs.md +57 -0
- package/plugins/agent-sync/skills/agent-sync/references/backend-outline.md +103 -0
- package/plugins/agent-sync/skills/agent-sync/references/hooks.md +99 -0
- package/plugins/agent-sync/skills/agent-sync/references/lease-protocol.md +145 -0
- package/plugins/agent-sync/skills/agent-sync/references/pipeline-binding.md +76 -0
- package/plugins/agent-sync/skills/agent-sync/references/roadmap.md +103 -0
- package/plugins/agent-sync/skills/agent-sync/references/two-sources.md +131 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/agent_sync.py +2454 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-sync
|
|
3
|
+
description: "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, filing or answering a cross-repo dependency, or regenerating the shared board. Triggers - 'claim this task' / 'возьми задачу', 'who is working on X' / 'кто сейчас делает X', 'reserve an id' / 'зарезервируй id', 'sync the board' / 'обнови доску', 'set up agent coordination' / 'настрой координацию агентов', /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."
|
|
4
|
+
compatibility: "Requires the task-pipeline skill for its stages (npx sshlg-skills install). Needs python3 3.9+ (stdlib only, HTTP included - nothing to pip install) and bash for the hooks. The knowledge backend is configured per project; with none configured it degrades to git-file leases. Enforcement hooks are Claude Code only - on other agents the same checks run as a self-check."
|
|
5
|
+
license: MIT
|
|
6
|
+
metadata:
|
|
7
|
+
version: "1.2.2"
|
|
8
|
+
author: appvillis-com
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# agent-sync — one project, many agents, no collisions
|
|
12
|
+
|
|
13
|
+
Two planes, and one rule between them:
|
|
14
|
+
|
|
15
|
+
> **Git is the record plane. The cloud is the coordination plane.**
|
|
16
|
+
> A fact that must survive is written to git first and referenced from the cloud.
|
|
17
|
+
> A fact about *who is doing what right now* lives in the cloud and expires.
|
|
18
|
+
|
|
19
|
+
No cloud object is ever the only home of a durable fact. Everything below exists to
|
|
20
|
+
keep that true while several agents write at once.
|
|
21
|
+
|
|
22
|
+
## Four traps — read these before anything else
|
|
23
|
+
|
|
24
|
+
**1. The knowledge base never decides a lease.** It cannot: twelve concurrent appends to
|
|
25
|
+
one Outline document returned twelve successes and left **three** lines. Exclusion comes
|
|
26
|
+
from something with real compare-and-swap. The plane carries the record and nothing else.
|
|
27
|
+
Full measurements in `references/lease-protocol.md`.
|
|
28
|
+
|
|
29
|
+
**2. Know which lease you have, and say so.** `leaseBackend: "local"` is an atomic file
|
|
30
|
+
create — exclusive between processes on one filesystem, **advisory across machines**.
|
|
31
|
+
`leaseBackend: "git"` pushes a ref, and the remote's non-fast-forward rejection **is** a
|
|
32
|
+
compare-and-swap — exclusive across machines. `acquire` prints which. A pretended lease is
|
|
33
|
+
worse than no lease: the other agent stops checking.
|
|
34
|
+
|
|
35
|
+
**`acquire` also writes the claim through to the roadmap**, and `release` restores exactly
|
|
36
|
+
what was there — one row, one cell, refused on ambiguity, `git diff` empty after a
|
|
37
|
+
round-trip. **Read `references/roadmap.md`** before configuring `claimTags` or closing a
|
|
38
|
+
task; closing is a statement about the work and stays yours.
|
|
39
|
+
|
|
40
|
+
**3. Hooks exist only in Claude Code.** Elsewhere nothing blocks a guarded edit: run
|
|
41
|
+
`guard` yourself and record the run as `ungated`. Do not describe a project as protected
|
|
42
|
+
when it is not.
|
|
43
|
+
|
|
44
|
+
**4. Parse liberally, and never call an unreadable log a lost race.** The store rewrites
|
|
45
|
+
what you wrote — Outline turns a `- ` bullet into `* `. Emit `- `, accept `-`/`*`/`+`,
|
|
46
|
+
count anything entry-shaped that fails, and **fail loudly** past 2% unparseable. Reporting
|
|
47
|
+
`lost` when the truth is *unreadable* names a holder who does not exist. Watch for a
|
|
48
|
+
silent pre-filter: a `continue` before the regex hides bad lines from the counter built to
|
|
49
|
+
expose them.
|
|
50
|
+
|
|
51
|
+
## Bringing this into ANY project — the whole chain
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
scaffold → create the documentation architecture, only where it is absent
|
|
55
|
+
adopt → read the repository, propose a config, write nothing
|
|
56
|
+
init → write the approved config + the gitignored env file
|
|
57
|
+
(operator pastes the token — never you)
|
|
58
|
+
reconcile --set-baseline → make history a counted backlog, once
|
|
59
|
+
setup → generate the snapshot that describes this project's wiring
|
|
60
|
+
check → validate the whole thing; non-zero if it is not healthy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**`check` is what makes the skill self-sufficient.** It refuses to call a setup healthy
|
|
64
|
+
on any of: a register file that does not exist or whose allocation pattern matches
|
|
65
|
+
nothing; a guard glob that matches no file (a rule that protects nothing); a claim-tag
|
|
66
|
+
pattern with nothing to look for; a gate command whose script is missing; a mirror source
|
|
67
|
+
that is not there; missing or empty credentials; a `.gitignore` that does not cover the
|
|
68
|
+
env file — or the env file being **tracked by git**, which is the one unrecoverable
|
|
69
|
+
mistake here; a missing, hand-edited or stale snapshot; **a snapshot no agent instruction
|
|
70
|
+
file links**, because agents that cannot find it will infer the pipeline instead; and a
|
|
71
|
+
register with no as-built baseline. Every one of those failed for real during this tool's
|
|
72
|
+
own adoption.
|
|
73
|
+
|
|
74
|
+
Run `check` after adopting, after changing the config, and in CI.
|
|
75
|
+
|
|
76
|
+
**`scaffold` never overwrites.** It seeds a decision register with an allocation line and
|
|
77
|
+
an `AGENTS.md` that points at the snapshot, and leaves every existing file untouched — a
|
|
78
|
+
tool that rewrites a project's own conventions on adoption is worse than one that does
|
|
79
|
+
nothing.
|
|
80
|
+
|
|
81
|
+
## Existing project: start with `adopt`
|
|
82
|
+
|
|
83
|
+
Run `adopt` before `init`. It reads the repository and prints what it found — id
|
|
84
|
+
registers, registry files, gates — plus the decisions it **refuses to make for you**,
|
|
85
|
+
then proposes a config. It writes nothing.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python3 "$SKILL_DIR/scripts/agent_sync.py" adopt
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Confirm the registers and guarded files with the operator before writing them. A
|
|
92
|
+
register pointed at the wrong file makes every later check confidently wrong, and a
|
|
93
|
+
guarded list that misses a shared file leaves the one place collisions actually happen
|
|
94
|
+
unprotected. In a submodule it declares no registers at all: decisions belong to the
|
|
95
|
+
parent repository.
|
|
96
|
+
|
|
97
|
+
Then: `init` → paste the approved config → `reconcile --set-baseline` → `setup` →
|
|
98
|
+
commit the snapshot and link it from the project's agent instructions.
|
|
99
|
+
|
|
100
|
+
## First command in a project: `init`
|
|
101
|
+
|
|
102
|
+
**Never run anything else against an uninitialised project.** `init` is where the
|
|
103
|
+
storage question gets asked and answered, once, and written down.
|
|
104
|
+
|
|
105
|
+
**Ask the operator these two things in chat — do not guess, do not pick a default:**
|
|
106
|
+
|
|
107
|
+
1. **Where should coordination state live?**
|
|
108
|
+
- a knowledge cloud (`outline`) — the shared record, awareness and board across
|
|
109
|
+
machines. **It does not decide leases**; nothing in it can (trap 1);
|
|
110
|
+
- or local files (`fs`) — no credentials, no shared awareness, every run `ungated`.
|
|
111
|
+
|
|
112
|
+
The lease is decided separately by `leaseBackend` — `git` for cross-machine exclusion,
|
|
113
|
+
`local` otherwise.
|
|
114
|
+
2. **If cloud: the instance URL.** The URL is configuration, not a secret, so you
|
|
115
|
+
may write it. The **token is not** — you never ask for it in chat, never read it
|
|
116
|
+
back, and never place it yourself.
|
|
117
|
+
|
|
118
|
+
Then run it with their answers:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend outline --url https://<their-instance>
|
|
122
|
+
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend fs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`init` writes `.claude/agent-sync.json` (shape, committed), writes
|
|
126
|
+
`.env.agent-sync` with the keys and an **empty** token line (identity, mode 600),
|
|
127
|
+
adds `.env.agent-sync` and `.agent-sync/` to `.gitignore`, and then prints exactly
|
|
128
|
+
what the operator must do themselves — create the token in their own instance and
|
|
129
|
+
paste it into that one line. It never overwrites an existing config or env file
|
|
130
|
+
without `--force`.
|
|
131
|
+
|
|
132
|
+
Relay those closing instructions to the operator verbatim. Getting the token into
|
|
133
|
+
the file is their step, and the design depends on it staying theirs.
|
|
134
|
+
|
|
135
|
+
## Then, before every session
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python3 "$SKILL_DIR/scripts/agent_sync.py" status
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Idempotent. Inspects, repairs what is missing, prints a status block, names exactly
|
|
142
|
+
ONE next action.
|
|
143
|
+
|
|
144
|
+
**Read the two awareness sections it prints — they are the point, not decoration.**
|
|
145
|
+
|
|
146
|
+
- **Other runs working this project right now.** Who holds what, this minute. Do not
|
|
147
|
+
take those on, and do not "just look at" the files they cover. A lease you cannot
|
|
148
|
+
see makes you blocked; a lease you can see makes you coordinated.
|
|
149
|
+
- **New since you last looked.** Cross-repo dependency moves that landed while you
|
|
150
|
+
were away. A dependency that moved may unblock what you planned — or invalidate it.
|
|
151
|
+
This list is watermarked per run, so it stays quiet until something actually
|
|
152
|
+
changes; when it speaks, it matters.
|
|
153
|
+
|
|
154
|
+
An agent that skips this block will re-derive work someone else is doing and act on a
|
|
155
|
+
dependency state that changed an hour ago.
|
|
156
|
+
|
|
157
|
+
What else `status` decides for you:
|
|
158
|
+
|
|
159
|
+
- No credentials in the environment → degraded mode, reported, and it continues.
|
|
160
|
+
Missing credentials are not an error, they are a smaller mode.
|
|
161
|
+
- `task-pipeline` absent → it prints the install line and stops. Do not improvise a
|
|
162
|
+
substitute flow; without those stages there is nothing to bind to.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx sshlg-skills install
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## The commands
|
|
169
|
+
|
|
170
|
+
| Command | Does |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `init` | **Run first.** Ask where state lives, write config + gitignored env file, print the operator's step |
|
|
173
|
+
| `status` | Inspect, repair, report, name one next action |
|
|
174
|
+
| `bootstrap` | Create the cloud container and print the id to paste into the env file |
|
|
175
|
+
| `acquire <KEY>` | Take the lease on a task id. Prints `won` or `lost <holder>` |
|
|
176
|
+
| `renew <KEY>` | Extend the lease. The `PostToolUse` hook does this for you |
|
|
177
|
+
| `release <KEY>` | Give the lease back. Always do this, including on failure |
|
|
178
|
+
| `reserve <REG>` | Reserve the next id in a register (`DEC`, `OQ`, `DEP`, …). Prints the id |
|
|
179
|
+
| `release-id <REG> <ID>` | Return an id you did not end up writing to git |
|
|
180
|
+
| `journal <text>` | Append one line to this run's journal |
|
|
181
|
+
| `record <text>` | Append what you **actually built** — `--decision DEC-…`, `--files a,b` |
|
|
182
|
+
| `reconcile` | Intent (git) vs as-built (cloud). `--set-baseline` once per project |
|
|
183
|
+
| `signal <DEP-ID> <state>` | Move a cross-repo dependency: `filed`/`accepted`/`delivered`/`closed`/`refused` |
|
|
184
|
+
| `guard <path>` | Answer whether this run may write that path. Exit 0 = yes, 2 = no |
|
|
185
|
+
| `board` | Regenerate the shared board and this repo's page. `--mirror` also renders the configured git docs into the plane |
|
|
186
|
+
| `whoami` | Print this run's id and its held leases |
|
|
187
|
+
| `setup` | Write the generated snapshot of how **this** project is wired, for agents to read |
|
|
188
|
+
| `adopt` | Inspect an existing project and **propose** a config — writes nothing |
|
|
189
|
+
| `scaffold` | Create the missing documentation architecture. Never overwrites |
|
|
190
|
+
| `check` | Validate the whole setup end to end. Non-zero when it is not healthy |
|
|
191
|
+
|
|
192
|
+
`$SKILL_DIR` is this skill's own directory. Every command reads
|
|
193
|
+
`.claude/agent-sync.json` from the project root and needs no arguments beyond those
|
|
194
|
+
listed.
|
|
195
|
+
|
|
196
|
+
## Claiming — the shape that matters
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
acquire → do the work → release
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Never skip `release`, including on failure: an abandoned lease blocks the task until its
|
|
203
|
+
TTL expires, and the next agent cannot tell "in progress" from "crashed an hour ago".
|
|
204
|
+
|
|
205
|
+
**The lease is not the claim.** The lease says who holds it *now* and expires; the durable
|
|
206
|
+
claim is the tag in git, written through by `acquire` and cleared by `release`. One fact,
|
|
207
|
+
one home — do not invent a third place that records ownership.
|
|
208
|
+
|
|
209
|
+
**Read `references/lease-protocol.md`** before changing acquisition, expiry, stealing or
|
|
210
|
+
id allocation.
|
|
211
|
+
|
|
212
|
+
## Guarded files
|
|
213
|
+
|
|
214
|
+
The config lists registry files several agents write. Before editing one:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
python3 "$SKILL_DIR/scripts/agent_sync.py" guard docs/DECISIONS.md
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Exit 2 means another run holds it. Do not edit anyway, and do not "just fix one line" —
|
|
221
|
+
a clobbered decision looks exactly like a decision.
|
|
222
|
+
|
|
223
|
+
Claude Code's `PreToolUse` hook runs this for you. Elsewhere nothing does.
|
|
224
|
+
|
|
225
|
+
## Reserving an id
|
|
226
|
+
|
|
227
|
+
Reading a "Next free ID" line is not reserving it — two agents read the same number and
|
|
228
|
+
both use it.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
python3 "$SKILL_DIR/scripts/agent_sync.py" reserve DEC # → DEC-0216
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Allocation is positional over the log, so every agent computes the same answer. Reserve
|
|
235
|
+
and not write it to git? `release-id` it — otherwise the number is a hole the board
|
|
236
|
+
reports as a leak, and nobody can tell a hole from work on a branch.
|
|
237
|
+
|
|
238
|
+
## Nothing in a log is ever edited or deleted
|
|
239
|
+
|
|
240
|
+
Logs are **replayed in order**, so an edit silently rewrites a conclusion other agents
|
|
241
|
+
already acted on. Correct by **appending**: release a lease, `release-id` an unused id,
|
|
242
|
+
supersede a wrong as-built entry with a later one. Generated pages are the only
|
|
243
|
+
exception, and one that lost its `agent-sync:generated` marker is **refused**, not
|
|
244
|
+
overwritten. Lifetimes: `references/two-sources.md`.
|
|
245
|
+
|
|
246
|
+
## Two documentation sources, and the duty to reconcile them
|
|
247
|
+
|
|
248
|
+
Git docs answer **how it should be** — written before the code, often without it.
|
|
249
|
+
The as-built record answers **how it actually is** — derived from what agents really
|
|
250
|
+
wrote. Neither is a copy of the other, and neither outranks the other, because they
|
|
251
|
+
answer different questions. **The gap between them is the finding**, not a defect.
|
|
252
|
+
|
|
253
|
+
The duty runs at both ends of every task:
|
|
254
|
+
|
|
255
|
+
- **Before starting** (docs-study stage) — `reconcile`, then read both sides for the
|
|
256
|
+
area you are about to touch. Resolve each divergence: the git doc is stale, or the
|
|
257
|
+
as-built record is wrong, or they genuinely disagree and that is a decision to make.
|
|
258
|
+
Building on an unresolved divergence means writing code against a system that does
|
|
259
|
+
not exist.
|
|
260
|
+
- **After finishing** (docs stage) — `record` what you actually built, update the git
|
|
261
|
+
documents that state intent, then `reconcile` again. A task that updated one side
|
|
262
|
+
has left the next agent a divergence to find the hard way.
|
|
263
|
+
|
|
264
|
+
`reconcile` is mechanical and says so: it compares ids, commits and presence, and refuses
|
|
265
|
+
to judge whether the built thing matches the document. That reading is yours.
|
|
266
|
+
|
|
267
|
+
Every project also carries a **generated snapshot** of its own wiring (`setup`) — commit
|
|
268
|
+
it and link it from the agent instructions, so agents read the pipeline instead of
|
|
269
|
+
inferring it.
|
|
270
|
+
|
|
271
|
+
**Read `references/two-sources.md`** before the first reconcile, and whenever deciding
|
|
272
|
+
which side a document belongs on.
|
|
273
|
+
|
|
274
|
+
## Binding to task-pipeline
|
|
275
|
+
|
|
276
|
+
This skill supplies stages; it does not define them. Stage names are
|
|
277
|
+
`task-pipeline`'s own.
|
|
278
|
+
|
|
279
|
+
| Stage | What to do here |
|
|
280
|
+
|---|---|
|
|
281
|
+
| 0 Intake grill | Add the cloud KB and the board to the harvest's source ledger; `acquire` **before** the brief is committed |
|
|
282
|
+
| 1 Docs study | `reconcile` — study git docs **and** the as-built record, resolve every divergence before writing code |
|
|
283
|
+
| 2 Brainstorm | `journal`; warn if a live run holds an overlapping key |
|
|
284
|
+
| 3 Spec | `reserve` every id before writing it to git |
|
|
285
|
+
| 4 Plan | Register file ownership for the plan's parallel groups |
|
|
286
|
+
| 5 Dev | Lease renews itself; own the submodule-commit → parent-gitlink bump |
|
|
287
|
+
| 6 Tests · 7 Lint · 8 Post-deploy | `journal` each gate result |
|
|
288
|
+
| 9 Docs + wiki | The main write point — `record` what was built, update the git docs, `signal` the dependency flips, `reconcile` again, then `board` |
|
|
289
|
+
| 10 Acceptance | `release` every lease, write the durable claim tag through to done |
|
|
290
|
+
|
|
291
|
+
**Read `references/pipeline-binding.md` when wiring `pipeline.json`** — it holds the
|
|
292
|
+
`skills[]` entries and the gate expressions.
|
|
293
|
+
|
|
294
|
+
## Configuration
|
|
295
|
+
|
|
296
|
+
Two files, and the split between them is the whole security model.
|
|
297
|
+
|
|
298
|
+
**`.claude/agent-sync.json`** — *shape*, committed: which backend, TTLs, which files
|
|
299
|
+
are guarded, which registers exist, which gates to run.
|
|
300
|
+
|
|
301
|
+
**`.env.agent-sync`** — *identity*, created by `init`, mode 600, gitignored:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
AGENT_SYNC_BACKEND=outline
|
|
305
|
+
AGENT_SYNC_OUTLINE_URL=https://<instance>
|
|
306
|
+
AGENT_SYNC_OUTLINE_TOKEN= # the operator fills this line, nobody else
|
|
307
|
+
AGENT_SYNC_OUTLINE_COLLECTION= # printed by `bootstrap`
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Load it before running agents:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
set -a && . ./.env.agent-sync && set +a
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Never write a host name or token into the config, a test, an example or a commit. Do not
|
|
317
|
+
handle a token value, echo it, or pass it in `argv`. If the operator offers one in chat,
|
|
318
|
+
tell them to put it in that file instead.
|
|
319
|
+
|
|
320
|
+
**A submodule's config declares only its own registers.** Cross-repository facts belong to
|
|
321
|
+
the parent; a service repo listing the parent's decision register is a config defect.
|
|
322
|
+
|
|
323
|
+
## Backends
|
|
324
|
+
|
|
325
|
+
| Backend | Read when |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `outline` | `references/backend-outline.md` — before any Outline call |
|
|
328
|
+
| `fs` | `references/backend-fs.md` — local files, no shared awareness |
|
|
329
|
+
|
|
330
|
+
**Read `references/adapter-contract.md` before adding a backend** — six primitives, the
|
|
331
|
+
capability flags, and a degradation path that must be honest.
|
|
332
|
+
|
|
333
|
+
## Generated objects
|
|
334
|
+
|
|
335
|
+
The board and the mirror are machine-written. Their first line is
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
<!-- agent-sync:generated source=<repo>@<sha> at=<iso8601> — edit in git, not here -->
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
A write to an object missing that marker is refused, not forced. If a human took over a
|
|
342
|
+
generated page, report it and stop.
|
|
343
|
+
|
|
344
|
+
The mirror is a **rendering** of git, stamped with the source commit. It has no
|
|
345
|
+
authority. When its stamp and `HEAD` disagree, the board gate fails — that is
|
|
346
|
+
drift, not a formatting problem.
|
|
347
|
+
|
|
348
|
+
## Non-negotiables
|
|
349
|
+
|
|
350
|
+
- Append, read back, then act. Never rewrite a coordination document.
|
|
351
|
+
- `release` what you `acquire`, on every path including failure.
|
|
352
|
+
- Credentials never reach `argv`, a log line, or the repository.
|
|
353
|
+
- Degrade out loud. `ungated` is an acceptable state; a false claim of enforcement is not.
|
|
354
|
+
- Everything the cloud holds about a durable fact is a link to git, never a substitute.
|
|
355
|
+
|
|
356
|
+
## References
|
|
357
|
+
|
|
358
|
+
Each file is loaded on its own trigger, not by default.
|
|
359
|
+
|
|
360
|
+
| File | Read it when |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `references/adapter-contract.md` | adding or auditing a knowledge backend |
|
|
363
|
+
| `references/lease-protocol.md` | changing acquisition, expiry, stealing or id allocation |
|
|
364
|
+
| `references/backend-outline.md` | making any Outline API call, or debugging one |
|
|
365
|
+
| `references/backend-fs.md` | running without a cloud backend, or explaining degraded mode |
|
|
366
|
+
| `references/pipeline-binding.md` | wiring `pipeline.json`, or adding a stage hook |
|
|
367
|
+
| `references/hooks.md` | installing, debugging or removing the Claude Code hooks |
|
|
368
|
+
| `references/two-sources.md` | before the first reconcile, or when deciding where a document belongs |
|
|
369
|
+
| `references/roadmap.md` | configuring `claimTags`, taking or closing a task, or re-planning a board |
|
|
370
|
+
|
|
371
|
+
If this copy arrived without `references/`, fetch them from
|
|
372
|
+
`https://raw.githubusercontent.com/appvillis-com/agent-sync/main/plugins/agent-sync/skills/agent-sync/references/<file>`.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Adapter contract
|
|
2
|
+
|
|
3
|
+
**Read this when** adding a knowledge backend, auditing one, or deciding whether a
|
|
4
|
+
candidate backend can be trusted with leases.
|
|
5
|
+
|
|
6
|
+
A backend is an adapter that implements six primitives and declares three
|
|
7
|
+
capabilities. Nothing else about it is the coordinator's business.
|
|
8
|
+
|
|
9
|
+
## Primitives
|
|
10
|
+
|
|
11
|
+
| Primitive | Signature | Semantics |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `tree.ensure` | `(path) -> id` | Idempotently create the container/document at `path`, return its id. MUST NOT overwrite existing content. |
|
|
14
|
+
| `log.append` | `(id, line) -> ok` | Append exactly one `\n`-terminated line to the end of the object's text, **server-side**, with no read-modify-write cycle. |
|
|
15
|
+
| `log.read` | `(id) -> text` | Return the object's full text. Line order MUST be identical for every reader at a given revision. |
|
|
16
|
+
| `doc.put` | `(id, text) -> ok` | Replace the object's text wholesale. Generators only. |
|
|
17
|
+
| `doc.get` | `(id) -> text` | Return the object's text. |
|
|
18
|
+
| `search` | `(query, limit) -> [{id,title,snippet}]` | Full-text search. Feeds the pipeline's stage-0 knowledge harvest. |
|
|
19
|
+
|
|
20
|
+
## Capabilities
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "atomicAppend": true, "totalOrderRead": true, "search": true,
|
|
24
|
+
"exclusiveLease": false }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **`atomicAppend`** — `log.append` reaches the server as an append. If the adapter
|
|
28
|
+
implements it as *read text, concatenate, write text back*, this is **false**, no
|
|
29
|
+
matter how fast that is.
|
|
30
|
+
- **`totalOrderRead`** — concurrent appends land in one order and every reader sees
|
|
31
|
+
the same order. A backend that merges concurrently, or that returns per-reader
|
|
32
|
+
views, is **false**.
|
|
33
|
+
- **`search`** — `search` is implemented rather than stubbed.
|
|
34
|
+
- **`exclusiveLease`** — whether *this backend* can decide a contended lease. Almost
|
|
35
|
+
always **false**: it requires compare-and-swap, and none of the document stores has
|
|
36
|
+
it. Declaring it true without one is the most damaging lie an adapter can tell, so
|
|
37
|
+
the default is false and the burden of proof is on the adapter.
|
|
38
|
+
|
|
39
|
+
## Degradation — non-negotiable
|
|
40
|
+
|
|
41
|
+
**No adapter is the lease authority.** Exclusion is an atomic local lock; the adapter
|
|
42
|
+
carries the record and the awareness. If `atomicAppend` or `totalOrderRead` is false the
|
|
43
|
+
coordinator additionally:
|
|
44
|
+
|
|
45
|
+
1. states it once, in plain words, at session start;
|
|
46
|
+
2. keeps the local lock as the only arbiter (`references/backend-fs.md`);
|
|
47
|
+
3. marks every run `ungated` on the board.
|
|
48
|
+
|
|
49
|
+
There is no third option. A lease that is not actually exclusive is worse than no
|
|
50
|
+
lease at all, because the other agent stops checking.
|
|
51
|
+
|
|
52
|
+
## Errors and retries
|
|
53
|
+
|
|
54
|
+
Every primitive returns a typed failure; none may raise past the caller.
|
|
55
|
+
|
|
56
|
+
| Condition | Handling |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Rate limited | Honour the backend's own retry hint; exponential backoff; at most 5 attempts, then fail loudly |
|
|
59
|
+
| Auth failure | Fail immediately. Never retry a credential — it is not going to become valid |
|
|
60
|
+
| Not found | For `tree.ensure`, create. For everything else, report the missing path; do not create silently |
|
|
61
|
+
| Transport error | Retry twice with backoff, then fail with the underlying message intact |
|
|
62
|
+
|
|
63
|
+
Never swallow an error into a success. A coordination layer that reports a write it
|
|
64
|
+
did not make is the failure this whole design exists to prevent.
|
|
65
|
+
|
|
66
|
+
## Credentials
|
|
67
|
+
|
|
68
|
+
- Read from the environment only. Never from the config file, never from `argv`.
|
|
69
|
+
- Never echo, never log, never include in a journal line or a board render.
|
|
70
|
+
- When absent: degraded mode with a clear message, not a crash and not a prompt.
|
|
71
|
+
|
|
72
|
+
## Adding a backend — checklist
|
|
73
|
+
|
|
74
|
+
- [ ] Six primitives implemented, each returning a typed failure
|
|
75
|
+
- [ ] Three capabilities declared **honestly**; `atomicAppend` false unless the append is server-side
|
|
76
|
+
- [ ] Credentials read from env only, and absent from every code path that builds a command line
|
|
77
|
+
- [ ] Rate-limit hint honoured
|
|
78
|
+
- [ ] `tree.ensure` proven idempotent by calling it twice against a live instance
|
|
79
|
+
- [ ] `log.append` proven atomic: two processes appending 100 lines each yield 200 lines, in one order both readers agree on
|
|
80
|
+
- [ ] Degradation path exercised: force `atomicAppend: false` and confirm the coordinator refuses lease authority and says so
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Filesystem backend — the degraded mode
|
|
2
|
+
|
|
3
|
+
**Read this when** running without a cloud backend, or explaining to an operator
|
|
4
|
+
what they do and do not get.
|
|
5
|
+
|
|
6
|
+
## What it is
|
|
7
|
+
|
|
8
|
+
Plain files under `.agent-sync/` in the repository:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
.agent-sync/
|
|
12
|
+
├── claims.log # the append log
|
|
13
|
+
├── reservations.log
|
|
14
|
+
├── signals.log
|
|
15
|
+
├── runs/<runId>.log
|
|
16
|
+
└── board.md # generated
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Capabilities — declared honestly
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{ "atomicAppend": false, "totalOrderRead": false, "search": false }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`atomicAppend` is false: agents on two clones append to two files, and a merge decides the
|
|
26
|
+
order after the fact — not at the moment the protocol needs one.
|
|
27
|
+
|
|
28
|
+
## What follows from that
|
|
29
|
+
|
|
30
|
+
**No adapter is ever the lease authority** — not this one, not the cloud. Exclusion comes
|
|
31
|
+
from `leaseBackend` (`local` = `O_EXCL`, `git` = a pushed ref). What this backend costs is
|
|
32
|
+
**awareness**: with it configured, the coordinator:
|
|
33
|
+
|
|
34
|
+
1. says so at session start, in one plain sentence;
|
|
35
|
+
2. keeps the lease exactly as configured — `leaseBackend` is independent of this choice;
|
|
36
|
+
3. marks every run `ungated` on the board, because nobody else can read the state.
|
|
37
|
+
|
|
38
|
+
## The lease is not this backend's job
|
|
39
|
+
|
|
40
|
+
Do not look for a lease mechanism here. `leaseBackend: "local"` decides with an atomic
|
|
41
|
+
file create; `leaseBackend: "git"` decides with a pushed ref whose non-fast-forward
|
|
42
|
+
rejection is a real compare-and-swap. Both work regardless of which knowledge backend is
|
|
43
|
+
configured — see `lease-protocol.md`.
|
|
44
|
+
|
|
45
|
+
## When this is the right choice
|
|
46
|
+
|
|
47
|
+
- A project with one agent at a time, which wants the journal and the board without
|
|
48
|
+
standing up a service.
|
|
49
|
+
- An air-gapped or offline repository.
|
|
50
|
+
- A first run, before the operator has chosen a knowledge backend.
|
|
51
|
+
|
|
52
|
+
## When it is the wrong choice
|
|
53
|
+
|
|
54
|
+
- Several agents at once: they hold leases correctly but cannot **see** each other, and
|
|
55
|
+
the whole point of the coordination plane is that they can.
|
|
56
|
+
- Anything where the operator has been told the project is coordinated. The lease still
|
|
57
|
+
works; the awareness does not, and the board must keep saying `ungated`.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Outline backend
|
|
2
|
+
|
|
3
|
+
**Read this when** making any Outline API call or debugging one.
|
|
4
|
+
|
|
5
|
+
[Outline](https://www.getoutline.com) is a collaborative knowledge base, available
|
|
6
|
+
hosted or self-hosted. Both work; the instance URL is configuration, never code.
|
|
7
|
+
|
|
8
|
+
## Shape of the API
|
|
9
|
+
|
|
10
|
+
- Base: `<AGENT_SYNC_OUTLINE_URL>/api` — the instance URL comes from the
|
|
11
|
+
environment. **Never hardcode a host, not even in a test or an example.**
|
|
12
|
+
- **Every endpoint is `POST`**, including reads.
|
|
13
|
+
- `Authorization: Bearer <token>` and `Content-Type: application/json`.
|
|
14
|
+
- Every response is enveloped: `{"ok": true, "status": 200, "data": {…}}` or
|
|
15
|
+
`{"ok": false, "error": "…", "message": "…", "status": 4xx}`. Check `ok`; an
|
|
16
|
+
HTTP 200 with `ok: false` is a failure.
|
|
17
|
+
|
|
18
|
+
## Capabilities
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{ "atomicAppend": true, "totalOrderRead": true, "search": true }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`atomicAppend` is true because `documents.update` accepts
|
|
25
|
+
`editMode: "append"`, which appends server-side without a read-modify-write cycle.
|
|
26
|
+
|
|
27
|
+
**There is no `lastRevision` parameter and no other optimistic-concurrency
|
|
28
|
+
control.** This is the single most important fact about this backend: a `replace`
|
|
29
|
+
update is last-write-wins and will silently discard a concurrent edit. Coordination
|
|
30
|
+
state is therefore never modelled as a document you rewrite — see
|
|
31
|
+
`lease-protocol.md`.
|
|
32
|
+
|
|
33
|
+
## Primitive mapping
|
|
34
|
+
|
|
35
|
+
| Primitive | Call | Body |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `tree.ensure` (container) | `POST /api/collections.create` | `{name}` — or `collections.list` first and reuse |
|
|
38
|
+
| `tree.ensure` (document) | `POST /api/documents.create` | `{collectionId, parentDocumentId?, title, text, publish: true}` |
|
|
39
|
+
| `log.append` | `POST /api/documents.update` | `{id, text: "<line>\n", editMode: "append"}` |
|
|
40
|
+
| `log.read` | `POST /api/documents.info` | `{id}` → `data.text` |
|
|
41
|
+
| `doc.put` | `POST /api/documents.update` | `{id, text, editMode: "replace"}` |
|
|
42
|
+
| `doc.get` | `POST /api/documents.info` | `{id}` → `data.text` |
|
|
43
|
+
| `search` | `POST /api/documents.search` | `{query, limit, collectionId?}` |
|
|
44
|
+
|
|
45
|
+
`publish: true` on create, or the document stays a draft and no other agent can
|
|
46
|
+
read it.
|
|
47
|
+
|
|
48
|
+
## Calling it without leaking the token
|
|
49
|
+
|
|
50
|
+
`curl -H "Authorization: Bearer $TOKEN"` puts the credential in `argv`, where every
|
|
51
|
+
other process on the machine can read it. Use a config file on stdin instead:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
payload=$(mktemp); chmod 600 "$payload"
|
|
55
|
+
printf '%s' "$body_json" > "$payload"
|
|
56
|
+
|
|
57
|
+
curl -sS --config - <<EOF
|
|
58
|
+
url = "$AGENT_SYNC_OUTLINE_URL/api/documents.update"
|
|
59
|
+
request = "POST"
|
|
60
|
+
header = "Authorization: Bearer $AGENT_SYNC_OUTLINE_TOKEN"
|
|
61
|
+
header = "Content-Type: application/json"
|
|
62
|
+
data-binary = "@$payload"
|
|
63
|
+
EOF
|
|
64
|
+
|
|
65
|
+
rm -f "$payload"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The heredoc reaches curl on stdin, so neither the token nor the payload appears in
|
|
69
|
+
the process table. Delete the payload file on every exit path.
|
|
70
|
+
|
|
71
|
+
**Prefer the bundled `scripts/agent_sync.py` over hand-rolling any request.** It
|
|
72
|
+
calls the API through `urllib` inside its own process — no subprocess, no `argv`,
|
|
73
|
+
nothing for another process to read. The `curl` recipe above is for the case where
|
|
74
|
+
you must issue a call by hand; it is the safe way to do that, not the better way.
|
|
75
|
+
|
|
76
|
+
## Rate limits
|
|
77
|
+
|
|
78
|
+
`429` comes back with `Retry-After` (seconds) plus `X-RateLimit-Limit`,
|
|
79
|
+
`X-RateLimit-Remaining` and `X-RateLimit-Reset`. Honour `Retry-After`, then back off
|
|
80
|
+
exponentially, at most 5 attempts. Do not spin.
|
|
81
|
+
|
|
82
|
+
## Getting a token
|
|
83
|
+
|
|
84
|
+
The operator creates an API key in their own instance's settings and puts it in
|
|
85
|
+
their environment. Do not ask for the value in chat, do not read it back, and do not
|
|
86
|
+
write it anywhere in the repository.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
AGENT_SYNC_OUTLINE_URL=https://<your-instance>
|
|
90
|
+
AGENT_SYNC_OUTLINE_TOKEN=<created by the operator>
|
|
91
|
+
AGENT_SYNC_OUTLINE_COLLECTION=<collection id, printed by `init`>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Verifying an instance
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
curl -sS -o /dev/null -w '%{http_code}\n' -X POST \
|
|
98
|
+
"$AGENT_SYNC_OUTLINE_URL/api/auth.info" -H 'Content-Type: application/json' -d '{}'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`401` is the healthy answer without a token: the host is up and demands auth.
|
|
102
|
+
A connection error means the URL is wrong or the instance is down — that is not a
|
|
103
|
+
credentials problem, and retrying with a different token will not help.
|