@ssheleg/agent-sync 1.13.0 → 1.15.0
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 +89 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/plugins/agent-sync/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-sync/commands/agent-sync.md +5 -2
- package/plugins/agent-sync/skills/agent-sync/SKILL.md +93 -136
- package/plugins/agent-sync/skills/agent-sync/references/lease-protocol.md +66 -0
- package/plugins/agent-sync/skills/agent-sync/references/two-sources.md +10 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/agent_sync.py +585 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
+
## v1.15.0 — a claim tag outlived its lease, and no command reached it
|
|
2
|
+
|
|
3
|
+
**GitHub issue #5, filed 2026-08-17, reproduced verbatim at v1.14.0.** A board row shipped
|
|
4
|
+
in a published release reading `(claimed: r-6e62c4dab)` while the lease plane said
|
|
5
|
+
`leases held: none`. `release <key>` printed `released`, exited **0** and changed nothing;
|
|
6
|
+
`residue` said *nothing on disk*; `reconcile` found *no mechanical divergence*. Three
|
|
7
|
+
commands, none of which could reach the tag, because `write_claim(key, None)` restores the
|
|
8
|
+
cell for the key it holds and it held none.
|
|
9
|
+
|
|
10
|
+
This is #4 from the other side. #4 was a lease that outlived its run; this is a tag that
|
|
11
|
+
outlived its lease — and the second is worse, because an invisible lease invites a check
|
|
12
|
+
while a confident wrong answer does not. Closed the same way: **one notion of held,
|
|
13
|
+
consulted by both planes.** `_lease_holder` stays the single reader, memoised per command
|
|
14
|
+
so a report over a tagged board costs one `ls-remote` per *tagged row* in git mode rather
|
|
15
|
+
than one per board row. `orphan_claims()` classifies a tag `orphan` — the TTL has ended its
|
|
16
|
+
lease — or `disputed`, live under another run, which is reported and never touched.
|
|
17
|
+
`release` clears an orphan and names the run it belonged to. `status`, `residue` and
|
|
18
|
+
`reconcile` each report one, so the disagreement is visible to a gate rather than to a
|
|
19
|
+
diff.
|
|
20
|
+
|
|
21
|
+
Four more, each with its plant watched refusing:
|
|
22
|
+
|
|
23
|
+
- **`residue` read as a complete answer in git mode and could not be.** It walks
|
|
24
|
+
`.agent-sync/leases/*.lock`, which only the local plane writes, so a ref won on another
|
|
25
|
+
machine was invisible. It now prints `⚠ INCOMPLETE IN THIS MODE` with the `ls-remote`
|
|
26
|
+
command it does not run. The sweep itself is still open, and the row says so — a check
|
|
27
|
+
that cannot look must not read as one that looked.
|
|
28
|
+
- **A `local` lock recorded no host**, so residue could not tell a lock written here from
|
|
29
|
+
one written on another machine; 25 of 25 locks on this machine carried none. Both modes
|
|
30
|
+
write it now, with a fixture separating two machines.
|
|
31
|
+
- **The ledger described an artifact nobody shipped**: its newest section was headed
|
|
32
|
+
*(in tree, unreleased)* and quoted `PASS: agent-sync v1.13.0` while v1.14.0 was tagged,
|
|
33
|
+
in `package.json` and on npm. Guarded three ways.
|
|
34
|
+
- **Two divisors for one token budget.** The family's auditor measured the pack's body at
|
|
35
|
+
~5084 tokens against a 5000 limit; this repo's own gate divided by 4 and passed at 4957.
|
|
36
|
+
One divisor now (3.9, the auditor's), and the body is under it at ~4683 after a split.
|
|
37
|
+
|
|
38
|
+
Also corrected: the board's *"14 of the 17 live in repositories this row must not touch"*
|
|
39
|
+
recomputes to **16 outside this checkout, 13 outside it and the umbrella**, with the `find`
|
|
40
|
+
that produces each.
|
|
41
|
+
|
|
42
|
+
**Found while doing it:** a plant had stopped planting. `the script path is prose only`
|
|
43
|
+
substituted a paragraph that had since been reflowed, so it changed nothing — and a no-op
|
|
44
|
+
plant still reported `detected` for a check that never ran. Re-anchored on the values the
|
|
45
|
+
check actually reads.
|
|
46
|
+
|
|
47
|
+
Self-test fixtures 43 → **51**; the claim-cell suite 9 → **16 cases**.
|
|
48
|
+
|
|
49
|
+
## v1.14.0 — expiry ended a lease and left the file, and every reader folded that away
|
|
50
|
+
|
|
51
|
+
**`status` reported `leases held: none` over three expired locks in the directory it had
|
|
52
|
+
just read.** Not a bug in one function: *every* reader of lease state folded the TTL into
|
|
53
|
+
the read. `held()` keeps only what is this run's **and** alive; `_lease_holder()` returns
|
|
54
|
+
`None` for an expired lock; `all_holdings()` drops it. All three are correct for
|
|
55
|
+
exclusion — an expired lease is not held — and all three therefore give one answer for
|
|
56
|
+
*expired* and for *absent*. There was no fourth reader, so nothing could tell the two
|
|
57
|
+
apart, and `finish` printed `✓ no lease left held` beside a two-day-old corpse.
|
|
58
|
+
|
|
59
|
+
Measured across the family when the conformance audit went looking: **17 expired lock
|
|
60
|
+
files across 9 checkouts, the oldest 3 days 11 hours.**
|
|
61
|
+
|
|
62
|
+
### The mechanism
|
|
63
|
+
|
|
64
|
+
`classify_lock()` — pure, all arguments in — reads every `.agent-sync/leases/*.lock` as
|
|
65
|
+
`live` · `reapable` · `foreign` · `ambiguous`. `status` and `finish` report residue
|
|
66
|
+
instead of silence. Two new verbs: `residue` (report only, safe in any checkout) and
|
|
67
|
+
`reap`, which clears **only** provably-own spent state and refuses foreign or ambiguous
|
|
68
|
+
state out loud, with exit 1, even when it is named on the command line.
|
|
69
|
+
|
|
70
|
+
`reapable` requires four things together, and anything short of all four is reported and
|
|
71
|
+
never deleted: the lease is spent with a parseable clock; it records a run; that run is
|
|
72
|
+
this one; and the run id means something. That last clause carries the rule — a shell
|
|
73
|
+
with no session id is served one **shared** identity, so a matching run id under it
|
|
74
|
+
proves nothing and does not license a delete. In doubt: `ambiguous`.
|
|
75
|
+
|
|
76
|
+
**Teardown is verified by re-reading the directory**, not by trusting the delete's return
|
|
77
|
+
value. Driven by hand: with the lease directory made read-only, `reap` exits 1 with
|
|
78
|
+
*"MINE is STILL PRESENT after the delete … the teardown was not verified, whatever the
|
|
79
|
+
call returned"* — and the lock is still there, which is the point.
|
|
80
|
+
|
|
81
|
+
### Standing instruction 9
|
|
82
|
+
|
|
83
|
+
*A predicate cannot report the condition it folds into its answer.* That is the class,
|
|
84
|
+
and it is now in `docs/evidence/retro.md` rather than in this entry alone.
|
|
85
|
+
|
|
86
|
+
Self-test 38 → 43 fixtures, every one detected. Latency measured, not assumed: `status`
|
|
87
|
+
0.39 s against 0.31 s before; `guard` unchanged at 0.15 s — residue is not on the guard
|
|
88
|
+
path.
|
|
89
|
+
|
|
1
90
|
## v1.13.0 — the file carried two notions of *held* and they disagreed where it mattered
|
|
2
91
|
|
|
3
92
|
**A lease from a run that died could not be cleared by any command.** Measured in the field:
|
package/README.md
CHANGED
|
@@ -248,6 +248,8 @@ python3 "$SKILL_DIR/scripts/agent_sync.py" <command>
|
|
|
248
248
|
| `guard <path>` | May this run write that path? Exit 0 = yes, 2 = no |
|
|
249
249
|
| `board` | Regenerate the read-only board and the mirror from git |
|
|
250
250
|
| `whoami` | Print this run's id and its held leases |
|
|
251
|
+
| `residue` | Expired locks still on disk, classified — this run's spent ones, and the foreign or ambiguously owned ones it reports and leaves alone |
|
|
252
|
+
| `reap [KEY…]` | Clear only the locks this run can prove it owns and has spent, then re-read the directory to confirm the teardown |
|
|
251
253
|
| `merge` | Land this branch on the integration branch: conflicts checked first, merge log written, lease released |
|
|
252
254
|
| `merges` | What landed while you were on your branch |
|
|
253
255
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/agent-sync",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Let concurrent coding agents share one project without colliding — leases with TTL, race-free id reservation, a run journal and a generated board, over a pluggable knowledge cloud.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-sync": "bin/agent-sync.js"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sync",
|
|
3
3
|
"displayName": "Agent Sync",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.15.0",
|
|
5
5
|
"description": "Coordination layer for multi-agent repositories — leases with TTL, race-free ID reservation, a run journal, a cross-repo signal feed and a generated board, over a pluggable knowledge cloud.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ssheleg",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Coordinate concurrent agents — initialise the shared knowledge store, check status, take a task, reserve an id, or regenerate the board.
|
|
3
|
-
argument-hint: "[init|status|check|acquire <KEY>|release <KEY>|reserve <REG>|board|merge|finish]"
|
|
3
|
+
argument-hint: "[init|status|check|acquire <KEY>|release <KEY>|reserve <REG>|residue|board|merge|finish]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Invoke the `agent-sync` skill.
|
|
@@ -15,6 +15,9 @@ instance URL) or local files. Never guess that answer.
|
|
|
15
15
|
If the project is already initialised, report status and name exactly one next
|
|
16
16
|
action.
|
|
17
17
|
|
|
18
|
+
With `residue`, report the expired locks still on disk — what this run can prove it owns and
|
|
19
|
+
has spent, and the foreign or ambiguously owned ones it reports and leaves alone.
|
|
20
|
+
|
|
18
21
|
With `finish`, run the end-of-work check instead: every repository clean, pushed and pointed at,
|
|
19
|
-
|
|
22
|
+
no lease left held, and what the run leaves behind named rather than assumed. In a project of git submodules that is the one failure nobody sees — the
|
|
20
23
|
submodule is pushed and the parent still points at the commit before the work.
|
|
@@ -4,7 +4,7 @@ description: "Use when several coding agents work one repository at the same tim
|
|
|
4
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
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.15.0"
|
|
8
8
|
author: ssheleg
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -24,7 +24,6 @@ keep that true while several agents write at once.
|
|
|
24
24
|
**1. The knowledge base never decides a lease.** It cannot: twelve concurrent appends to
|
|
25
25
|
one Outline document returned twelve successes and left **three** lines. Exclusion comes
|
|
26
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
27
|
|
|
29
28
|
**2. Know which lease you have, and say so.** `leaseBackend: "local"` is an atomic file
|
|
30
29
|
create — exclusive between processes on one filesystem, **advisory across machines**.
|
|
@@ -38,14 +37,12 @@ round-trip. **Read `references/roadmap.md`** before configuring `claimTags` or c
|
|
|
38
37
|
task; closing is a statement about the work and stays yours.
|
|
39
38
|
|
|
40
39
|
**Work on a branch; the integration branch is somebody else's stable base.** `acquire`
|
|
41
|
-
writes the claim through **only** there
|
|
42
|
-
coordination plane, where `status` shows it to every agent
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
lease released. `merges` tells the next agent what landed while it was away. **Read
|
|
48
|
-
`references/branching.md`** before merging.
|
|
40
|
+
writes the claim through **only** there — on any other branch the holder stays in the
|
|
41
|
+
coordination plane, where `status` shows it to every agent. Committed to a branch, a claim
|
|
42
|
+
is invisible until the merge and turns the shared roadmap into a file two branches both
|
|
43
|
+
edit. Land work with `merge`: conflicts computed **before** anything is touched, the merge
|
|
44
|
+
recorded in `docs/MERGES.md`, the `--key` lease released; `merges` says what landed while
|
|
45
|
+
you were away. **Read `references/branching.md`** before merging.
|
|
49
46
|
|
|
50
47
|
**3. Hooks exist only in Claude Code.** Elsewhere nothing blocks a guarded edit: run
|
|
51
48
|
`guard` yourself and record the run as `ungated`. Do not describe a project as protected
|
|
@@ -55,8 +52,7 @@ when it is not.
|
|
|
55
52
|
what you wrote — Outline turns a `- ` bullet into `* `. Emit `- `, accept `-`/`*`/`+`,
|
|
56
53
|
count anything entry-shaped that fails, and **fail loudly** past 2% unparseable. Reporting
|
|
57
54
|
`lost` when the truth is *unreadable* names a holder who does not exist. Watch for a
|
|
58
|
-
silent pre-filter: a `continue` before the regex hides bad lines from the counter
|
|
59
|
-
expose them.
|
|
55
|
+
silent pre-filter: a `continue` before the regex hides bad lines from the counter.
|
|
60
56
|
|
|
61
57
|
## Bringing this into ANY project — the whole chain
|
|
62
58
|
|
|
@@ -70,19 +66,15 @@ setup → generate the snapshot that describes this project's wiring
|
|
|
70
66
|
check → validate the whole thing; non-zero if it is not healthy
|
|
71
67
|
```
|
|
72
68
|
|
|
73
|
-
**`check` is what makes the skill self-sufficient.** It refuses to call a setup healthy on
|
|
74
|
-
|
|
69
|
+
**`check` is what makes the skill self-sufficient.** It refuses to call a setup healthy on a
|
|
70
|
+
rule that protects nothing (a register, guard glob, claim pattern, gate or mirror source
|
|
75
71
|
pointing at what is not there), on missing credentials, on an env file **tracked by git** —
|
|
76
|
-
the one unrecoverable mistake here — on a stale
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
the one unrecoverable mistake here — on a stale snapshot, or on a register with no baseline.
|
|
73
|
+
It names each one; every one failed for real during this tool's own adoption. Run it after
|
|
74
|
+
adopting, after changing the config, and in CI.
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
**`scaffold` never overwrites.** It seeds a decision register with an allocation line and
|
|
83
|
-
an `AGENTS.md` that points at the snapshot, and leaves every existing file untouched — a
|
|
84
|
-
tool that rewrites a project's own conventions on adoption is worse than one that does
|
|
85
|
-
nothing.
|
|
76
|
+
**`scaffold` never overwrites.** It seeds a decision register and an `AGENTS.md` pointing at
|
|
77
|
+
the snapshot, and leaves every existing file untouched.
|
|
86
78
|
|
|
87
79
|
## Existing project: start with `adopt`
|
|
88
80
|
|
|
@@ -95,15 +87,14 @@ python3 "$SKILL_DIR/scripts/agent_sync.py" adopt
|
|
|
95
87
|
```
|
|
96
88
|
|
|
97
89
|
Confirm the registers and guarded files with the operator first: a register pointed at the
|
|
98
|
-
wrong file makes every later check confidently wrong, and a guarded list that misses a
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`init`.
|
|
90
|
+
wrong file makes every later check confidently wrong, and a guarded list that misses a shared
|
|
91
|
+
file leaves the one place collisions happen unprotected. In a submodule it declares no
|
|
92
|
+
registers — decisions belong to the parent. Then take the chain above from `init`.
|
|
102
93
|
|
|
103
94
|
## First command in a project: `init`
|
|
104
95
|
|
|
105
|
-
**Never run anything else against an uninitialised project.** `init` is where the
|
|
106
|
-
|
|
96
|
+
**Never run anything else against an uninitialised project.** `init` is where the storage
|
|
97
|
+
question gets asked and answered, once, and written down.
|
|
107
98
|
|
|
108
99
|
**Ask the operator these two things in chat — do not guess, do not pick a default:**
|
|
109
100
|
|
|
@@ -127,15 +118,11 @@ python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend outline --url https://
|
|
|
127
118
|
python3 "$SKILL_DIR/scripts/agent_sync.py" init --backend fs
|
|
128
119
|
```
|
|
129
120
|
|
|
130
|
-
`init` writes `.claude/agent-sync.json` (shape, committed)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
without `--force`.
|
|
136
|
-
|
|
137
|
-
Relay those closing instructions to the operator verbatim. Getting the token into
|
|
138
|
-
the file is their step, and the design depends on it staying theirs.
|
|
121
|
+
`init` writes `.claude/agent-sync.json` (shape, committed) and `.env.agent-sync` with an
|
|
122
|
+
**empty** token line (identity, mode 600), gitignores both, and prints the one step that is
|
|
123
|
+
the operator's — creating the token and pasting it into that line. Never overwrites either
|
|
124
|
+
file without `--force`. **Relay those closing instructions verbatim**; the design depends on
|
|
125
|
+
the token staying theirs.
|
|
139
126
|
|
|
140
127
|
## Then, before every session
|
|
141
128
|
|
|
@@ -144,25 +131,23 @@ python3 "$SKILL_DIR/scripts/agent_sync.py" status
|
|
|
144
131
|
```
|
|
145
132
|
|
|
146
133
|
Idempotent. Inspects, repairs, reports, names exactly ONE next action — and carries
|
|
147
|
-
`check`'s verdict, so the command every session runs and the one that validates the
|
|
148
|
-
|
|
134
|
+
`check`'s verdict, so the command every session runs and the one that validates the setup
|
|
135
|
+
cannot give two answers about one project.
|
|
149
136
|
|
|
150
137
|
**Read the two awareness sections it prints — they are the point, not decoration.**
|
|
151
138
|
|
|
152
|
-
- **Other runs working this project right now.** Who holds what, this minute. Do not
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
This list is watermarked per run, so it stays quiet until something actually
|
|
158
|
-
changes; when it speaks, it matters.
|
|
139
|
+
- **Other runs working this project right now.** Who holds what, this minute. Do not take
|
|
140
|
+
those on, and do not "just look at" the files they cover.
|
|
141
|
+
- **New since you last looked.** Cross-repo dependency moves that landed while you were
|
|
142
|
+
away — watermarked per run, so it stays quiet until something changes. A dependency that
|
|
143
|
+
moved may unblock what you planned, or invalidate it.
|
|
159
144
|
|
|
160
145
|
An agent that skips this block will re-derive work someone else is doing and act on a
|
|
161
146
|
dependency state that changed an hour ago.
|
|
162
147
|
|
|
163
|
-
What else `status` decides: no credentials → degraded mode, reported, and it continues
|
|
164
|
-
|
|
165
|
-
|
|
148
|
+
What else `status` decides: no credentials → degraded mode, reported, and it continues;
|
|
149
|
+
`task-pipeline` absent → it prints the install line and stops. Do not improvise a substitute
|
|
150
|
+
flow — without those stages there is nothing to bind to.
|
|
166
151
|
|
|
167
152
|
```bash
|
|
168
153
|
npx sshlg-skills install
|
|
@@ -187,35 +172,32 @@ npx sshlg-skills install
|
|
|
187
172
|
| `guard <path>` | Answer whether this run may write that path. Exit 0 = yes, 2 = no |
|
|
188
173
|
| `board` | Regenerate the shared board and this repo's page. `--mirror` also renders the configured git docs into the plane |
|
|
189
174
|
| `whoami` | Print this run's id and its held leases |
|
|
175
|
+
| `residue` | What a run left behind in BOTH planes: expired locks on disk, and claim tags with no live lease behind them |
|
|
176
|
+
| `reap [KEY…]` | Clear only what this run **proves** it owns and has spent, then re-read the directory to confirm it went |
|
|
190
177
|
| `setup` | Write the generated snapshot of how **this** project is wired, for agents to read |
|
|
191
178
|
| `adopt` | Inspect an existing project and **propose** a config — writes nothing |
|
|
192
|
-
| `merge` | Land this branch:
|
|
179
|
+
| `merge` | Land this branch: target fast-forwarded, conflicts checked **before** anything is touched, merge log written, the `--key` lease released. `--summary`, `--dry-run`, `--push` |
|
|
193
180
|
| `merges` | What landed while you were on your branch. `--all` includes the compacted tail |
|
|
194
181
|
| `check` | Validate the whole setup end to end. Non-zero when it is not healthy |
|
|
195
|
-
| `scaffold [--full]` | Create only what is missing, never a line over
|
|
196
|
-
| `finish [--gates]` | Is the **work** finished — every repository clean, pushed and pointed at,
|
|
182
|
+
| `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 |
|
|
183
|
+
| `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 |
|
|
197
184
|
|
|
198
|
-
`$SKILL_DIR` is this skill's own directory: `${CLAUDE_PLUGIN_ROOT}/skills/agent-sync`
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
listed.
|
|
185
|
+
`$SKILL_DIR` is this skill's own directory: `${CLAUDE_PLUGIN_ROOT}/skills/agent-sync` under
|
|
186
|
+
the Claude Code plugin, `~/.agents/skills/agent-sync` elsewhere. Resolve it once per session
|
|
187
|
+
and reuse it — do not guess. Every command reads `.claude/agent-sync.json` from the project
|
|
188
|
+
root and needs no arguments beyond those listed.
|
|
203
189
|
|
|
204
190
|
## One identity per session, and how it is decided
|
|
205
191
|
|
|
206
|
-
A lease is only a lease if two agents get two identities
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
guarded, and `release` takes a lease the caller never had.
|
|
192
|
+
A lease is only a lease if two agents get two identities, and both ends have bitten:
|
|
193
|
+
`CLAUDE_SESSION_ID` alone gave **one session two identities** — it acquired as one and was denied by
|
|
194
|
+
its own guard as the other — and one id per checkout gave **two sessions one identity**, silently,
|
|
195
|
+
so `release` took a lease the caller never had.
|
|
211
196
|
|
|
212
197
|
The order is: `AGENT_SYNC_RUN_ID` · `CLAUDE_SESSION_ID` · **the session that started this shell** ·
|
|
213
|
-
shared.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`SessionStart` stamps `.agent-sync/sessions/<CLI pid>` with the session it knows, and a later
|
|
217
|
-
command finds itself by walking its own process ancestry to a stamped pid. Why that and not
|
|
218
|
-
command-line parsing: `references/earned-rules.md`.
|
|
198
|
+
shared. The third exists because a plain shell command has no session id and a hook does, so
|
|
199
|
+
`SessionStart` stamps `.agent-sync/sessions/<CLI pid>` and a later command walks its own process
|
|
200
|
+
ancestry to it. Why that and not command-line parsing: `references/earned-rules.md`.
|
|
219
201
|
|
|
220
202
|
When none of the four can be established the run says so — *"this identity is shared with any other
|
|
221
203
|
session in this checkout"* — rather than presenting a shared entry as separation.
|
|
@@ -233,6 +215,11 @@ TTL expires, and the next agent cannot tell "in progress" from "crashed an hour
|
|
|
233
215
|
claim is the tag in git, written through by `acquire` and cleared by `release`. One fact,
|
|
234
216
|
one home — do not invent a third place that records ownership.
|
|
235
217
|
|
|
218
|
+
**One notion of held, consulted by both planes.** The TTL ends the lease, so a tag naming a
|
|
219
|
+
run whose lease has ended is residue: reported by `status`, `residue` and `reconcile`, and
|
|
220
|
+
cleared by `release <KEY>`, which says whose it was. A tag whose lease is still live is
|
|
221
|
+
never touched by another run.
|
|
222
|
+
|
|
236
223
|
**Read `references/lease-protocol.md`** before changing acquisition, expiry, stealing or
|
|
237
224
|
id allocation.
|
|
238
225
|
|
|
@@ -244,65 +231,59 @@ The config lists registry files several agents write. Before editing one:
|
|
|
244
231
|
python3 "$SKILL_DIR/scripts/agent_sync.py" guard docs/DECISIONS.md
|
|
245
232
|
```
|
|
246
233
|
|
|
247
|
-
**Exit 2 is about *this run*: it holds no lease** — not that somebody else holds that
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
234
|
+
**Exit 2 is about *this run*: it holds no lease** — not that somebody else holds that file.
|
|
235
|
+
One lease covers every guarded file; hold one or write none. A denial names the other run
|
|
236
|
+
**and its key**, because "r-x holds a lease" beside a path gets repeated as "r-x holds this
|
|
237
|
+
file". Do not edit anyway, and do not "just fix one line" — a clobbered decision looks exactly
|
|
238
|
+
like a decision.
|
|
252
239
|
|
|
253
240
|
Claude Code's `PreToolUse` hook runs this for you. Elsewhere nothing does.
|
|
254
241
|
|
|
255
242
|
## Reserving an id
|
|
256
243
|
|
|
257
|
-
Reading a "Next free ID" line is not reserving it — two agents read the same number and
|
|
258
|
-
|
|
244
|
+
Reading a "Next free ID" line is not reserving it — two agents read the same number and both
|
|
245
|
+
use it.
|
|
259
246
|
|
|
260
247
|
```bash
|
|
261
248
|
python3 "$SKILL_DIR/scripts/agent_sync.py" reserve DEC # → DEC-0216
|
|
262
249
|
```
|
|
263
250
|
|
|
264
|
-
Allocation is positional over the **merged** log — every shard, never just this run's —
|
|
265
|
-
|
|
266
|
-
|
|
251
|
+
Allocation is positional over the **merged** log — every shard, never just this run's — so
|
|
252
|
+
every agent computes the same answer. Reserved and not written to git? `release-id` it, or the
|
|
253
|
+
number is a hole the board reports as a leak.
|
|
267
254
|
|
|
268
255
|
## Nothing in a log is ever edited or deleted
|
|
269
256
|
|
|
270
|
-
Logs are **replayed in order**, so an edit silently rewrites a conclusion other agents
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
overwritten. Lifetimes: `references/two-sources.md`.
|
|
257
|
+
Logs are **replayed in order**, so an edit silently rewrites a conclusion other agents already
|
|
258
|
+
acted on. Correct by **appending**: release a lease, `release-id` an unused id, supersede a
|
|
259
|
+
wrong as-built entry with a later one. Generated pages are the only exception. Lifetimes and
|
|
260
|
+
the generated-object contract: `references/two-sources.md`.
|
|
275
261
|
|
|
276
262
|
## Two documentation sources, and the duty to reconcile them
|
|
277
263
|
|
|
278
|
-
Git docs answer **how it should be**; the as-built record answers **how it actually
|
|
279
|
-
|
|
280
|
-
**the gap between them is the finding**, not a defect.
|
|
264
|
+
Git docs answer **how it should be**; the as-built record answers **how it actually is**.
|
|
265
|
+
Neither outranks the other, and **the gap between them is the finding**, not a defect.
|
|
281
266
|
|
|
282
267
|
The duty runs at both ends of a task: `reconcile` and resolve every divergence before
|
|
283
|
-
starting, then `record` and `reconcile` again after finishing. Building on an
|
|
284
|
-
|
|
268
|
+
starting, then `record` and `reconcile` again after finishing. Building on an unresolved
|
|
269
|
+
divergence is writing code against a system that does not exist.
|
|
285
270
|
|
|
286
|
-
**The trap: `reconcile` is mechanical and refuses to judge** whether the built thing
|
|
287
|
-
|
|
288
|
-
|
|
271
|
+
**The trap: `reconcile` is mechanical and refuses to judge** whether the built thing matches
|
|
272
|
+
the document — it compares ids, commits, presence and claim tags. That reading is yours, and
|
|
273
|
+
treating its green as agreement is how a divergence survives both ends.
|
|
289
274
|
|
|
290
|
-
Every project also carries a **generated snapshot** of its own wiring (`setup`) — commit
|
|
291
|
-
|
|
292
|
-
inferring it.
|
|
275
|
+
Every project also carries a **generated snapshot** of its own wiring (`setup`) — commit it
|
|
276
|
+
and link it from the agent instructions, so agents read the pipeline instead of inferring it.
|
|
293
277
|
|
|
294
|
-
**Read `references/two-sources.md`** before the first reconcile, and whenever deciding
|
|
295
|
-
|
|
296
|
-
decides, why the check is a ratchet, where a document belongs, and why nothing is
|
|
297
|
-
deleted.
|
|
278
|
+
**Read `references/two-sources.md`** before the first reconcile, and whenever deciding which
|
|
279
|
+
side a document belongs on.
|
|
298
280
|
|
|
299
281
|
## Binding to task-pipeline
|
|
300
282
|
|
|
301
|
-
This skill supplies stages; the names are `task-pipeline`'s own.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
reaches git; **9** the main write point; **10** `merge` or `release` every lease.
|
|
283
|
+
This skill supplies stages; the names are `task-pipeline`'s own. Five of the eleven stages
|
|
284
|
+
carry an ordering rule: **0** `acquire` before the brief is committed; **1** `reconcile` before
|
|
285
|
+
writing code; **3** `reserve` every id before it reaches git; **9** the main write point;
|
|
286
|
+
**10** `merge` or `release` every lease.
|
|
306
287
|
|
|
307
288
|
**Read `references/pipeline-binding.md`** when wiring `pipeline.json` — it holds the
|
|
308
289
|
per-stage reasoning, the `skills[]` entries, what must be guarded, and the gate
|
|
@@ -324,42 +305,19 @@ the collection id to paste beside it. Found here, or in a superproject, or where
|
|
|
324
305
|
set -a && . ./.env.agent-sync && set +a
|
|
325
306
|
```
|
|
326
307
|
|
|
327
|
-
Never write a host name or token into the config, a test, an example or a commit
|
|
328
|
-
handle
|
|
329
|
-
tell them to put it in that file instead.
|
|
308
|
+
Never write a host name or token into the config, a test, an example or a commit; never
|
|
309
|
+
handle, echo or `argv`-pass a token value. Offered one in chat, point at that file instead.
|
|
330
310
|
|
|
331
311
|
**A submodule's config declares only its own registers.** Cross-repository facts belong to
|
|
332
312
|
the parent; a service repo listing the parent's decision register is a config defect.
|
|
333
313
|
|
|
334
|
-
## Backends
|
|
335
|
-
|
|
336
|
-
| Backend | Read when |
|
|
337
|
-
|---|---|
|
|
338
|
-
| `outline` | `references/backend-outline.md` — before any Outline call |
|
|
339
|
-
| `fs` | `references/backend-fs.md` — local files, no shared awareness |
|
|
340
|
-
|
|
341
|
-
**Read `references/adapter-contract.md` before adding a backend** — six primitives, the
|
|
342
|
-
capability flags, and a degradation path that must be honest.
|
|
343
|
-
|
|
344
|
-
## Generated objects
|
|
345
|
-
|
|
346
|
-
The board and the mirror are machine-written. Their first line is
|
|
347
|
-
|
|
348
|
-
```
|
|
349
|
-
<!-- agent-sync:generated source=<repo>@<sha> at=<iso8601> — edit in git, not here -->
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
A write to an object missing that marker is refused, not forced. If a human took over a
|
|
353
|
-
generated page, report it and stop.
|
|
354
|
-
|
|
355
|
-
The mirror is a **rendering** of git, stamped with the source commit. It has no
|
|
356
|
-
authority. When its stamp and `HEAD` disagree, the board gate fails — that is
|
|
357
|
-
drift, not a formatting problem.
|
|
358
|
-
|
|
359
314
|
## Non-negotiables
|
|
360
315
|
|
|
361
316
|
- Append, read back, then act. Never rewrite a coordination document.
|
|
317
|
+
- A generated object without its `agent-sync:generated` marker is **refused**, not
|
|
318
|
+
overwritten — a human took it over. Contract: `references/two-sources.md`.
|
|
362
319
|
- `release` what you `acquire`, on every path including failure.
|
|
320
|
+
- A run reports what it leaves behind: expiry ends a lease, not the file — `residue`.
|
|
363
321
|
- Credentials never reach `argv`, a log line, or the repository.
|
|
364
322
|
- Degrade out loud. `ungated` is an acceptable state; a false claim of enforcement is not.
|
|
365
323
|
- Two agents in one checkout are two identities, or the lease is decoration.
|
|
@@ -372,16 +330,15 @@ Each file is loaded on its own trigger, not by default.
|
|
|
372
330
|
|
|
373
331
|
| File | Read it when |
|
|
374
332
|
|---|---|
|
|
375
|
-
| `references/adapter-contract.md` | adding or auditing a knowledge backend |
|
|
333
|
+
| `references/adapter-contract.md` | adding or auditing a knowledge backend — six primitives, the capability flags, an honest degradation path |
|
|
376
334
|
| `references/lease-protocol.md` | changing acquisition, expiry, stealing or id allocation |
|
|
377
335
|
| `references/backend-outline.md` | making any Outline API call, or debugging one |
|
|
378
336
|
| `references/backend-fs.md` | running without a cloud backend, or explaining degraded mode |
|
|
379
337
|
| `references/pipeline-binding.md` | wiring `pipeline.json`, or adding a stage hook |
|
|
380
338
|
| `references/hooks.md` | installing, debugging or removing the Claude Code hooks |
|
|
381
|
-
| `references/two-sources.md` | before the first reconcile,
|
|
339
|
+
| `references/two-sources.md` | before the first reconcile, deciding where a document belongs, or writing a generated object |
|
|
382
340
|
| `references/roadmap.md` | configuring `claimTags`, taking or closing a task, or re-planning a board |
|
|
383
341
|
| `references/branching.md` | starting work that will produce commits, merging a branch, or asking what landed while you were away |
|
|
384
342
|
| `references/earned-rules.md` | asking why identity resolves the way it does, or why `finish` exists |
|
|
385
343
|
|
|
386
|
-
|
|
387
|
-
`https://raw.githubusercontent.com/ssheleg/agent-sync/main/plugins/agent-sync/skills/agent-sync/references/<file>`.
|
|
344
|
+
Missing `references/`? Fetch from `https://raw.githubusercontent.com/ssheleg/agent-sync/main/plugins/agent-sync/skills/agent-sync/references/<file>`.
|
|
@@ -9,6 +9,7 @@ when two agents disagree about who holds something.
|
|
|
9
9
|
- [Acquiring — the third design, and the first that is true](#acquiring--the-third-design-and-the-first-that-is-true)
|
|
10
10
|
- [Expiry and stealing](#expiry-and-stealing)
|
|
11
11
|
- [Releasing](#releasing)
|
|
12
|
+
- [Residue — what expiry leaves behind](#residue--what-expiry-leaves-behind)
|
|
12
13
|
- [Id reservation](#id-reservation)
|
|
13
14
|
- [The lease is not the claim](#the-lease-is-not-the-claim)
|
|
14
15
|
|
|
@@ -143,6 +144,71 @@ and when.
|
|
|
143
144
|
from active work until its TTL runs out, and during that window the task looks
|
|
144
145
|
taken. Report the failure and release; do not hold the lease "in case".
|
|
145
146
|
|
|
147
|
+
## Residue — what expiry leaves behind
|
|
148
|
+
|
|
149
|
+
**Expiry ends a lease. It does not remove a file.** Every reader of lease state in this
|
|
150
|
+
tool folds the TTL into the read — `held()`, `_lease_holder()` and `all_holdings()` each
|
|
151
|
+
answer *none* for an expired lock and *none* for a lock that is not there. That is exactly
|
|
152
|
+
right for exclusion, and it means an expired lock is not un-owned but **invisible**.
|
|
153
|
+
|
|
154
|
+
Measured across the nine repositories of one skill family on 2026-08-19: **seventeen lock
|
|
155
|
+
files, all seventeen expired**, the oldest by three days. `status` printed `leases held:
|
|
156
|
+
none` / `other runs: none holding anything` in a checkout holding three of them, and
|
|
157
|
+
`finish` printed `✓ no lease left held` beside a two-day-expired one. Nothing was wrong
|
|
158
|
+
with any of those answers. Nothing enumerated.
|
|
159
|
+
|
|
160
|
+
So `status` and `finish` report residue, `residue` prints it in full, and `reap` is the
|
|
161
|
+
only thing that removes any of it. The split between the two kinds is the mechanism:
|
|
162
|
+
|
|
163
|
+
| Verdict | What it means | What happens to it |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `live` | still inside its TTL | held, not residue — untouched |
|
|
166
|
+
| `reapable` | this run PROVABLY owns it and has spent it | `reap` clears it |
|
|
167
|
+
| `foreign` | it demonstrably belongs to another run or another machine | reported, left alone |
|
|
168
|
+
| `ambiguous` | ownership or expiry cannot be established at all | reported, left alone |
|
|
169
|
+
|
|
170
|
+
**What makes ownership provable** — all four, or the verdict is not `reapable`:
|
|
171
|
+
|
|
172
|
+
1. the lock is past its TTL (a live lease is held, not residue);
|
|
173
|
+
2. it records a `run`, and that run is this one;
|
|
174
|
+
3. **this run's identity is not the shared fallback.** `run_id()` keys its marker by
|
|
175
|
+
session; a shell with no session id is served one shared entry, and that identity is
|
|
176
|
+
shared with any other session in the same checkout. Under it a matching run id proves
|
|
177
|
+
nothing — so it does not license a delete, and every such lock is reported as
|
|
178
|
+
`ambiguous` instead;
|
|
179
|
+
4. the lock's `repo` is this checkout and its `host` — written by the git mode, absent in
|
|
180
|
+
`local`, where the lease is machine-local by construction — is this machine.
|
|
181
|
+
|
|
182
|
+
In doubt the answer is `ambiguous`, never `reapable`. A classifier that resolves doubt by
|
|
183
|
+
deleting is worse than none, because it deletes under a claim of authority.
|
|
184
|
+
|
|
185
|
+
**Teardown is verified by re-reading the state, never by the delete's return value.**
|
|
186
|
+
`unlink` returns nothing and raises nothing on a filesystem where the entry survives the
|
|
187
|
+
call — a read-only mount, an NFS write that never lands, another process recreating the
|
|
188
|
+
name. `reap` therefore reads the lease directory again and compares `(run, ts)`: a lock
|
|
189
|
+
still there under the same identity is a **failure**, however cleanly the delete went, and
|
|
190
|
+
nothing is reported as cleared. A key that came back as another run's live lease *was*
|
|
191
|
+
torn down, and calling that a failure would teach an operator to ignore the one message
|
|
192
|
+
that matters.
|
|
193
|
+
|
|
194
|
+
`reap` never touches `foreign` or `ambiguous` state, including when it is named on the
|
|
195
|
+
command line: naming one is refused out loud with a non-zero exit, because an operator
|
|
196
|
+
reads a silent zero as done.
|
|
197
|
+
|
|
198
|
+
### Why `release <KEY>` may reap a foreign corpse and `reap` may not
|
|
199
|
+
|
|
200
|
+
They look contradictory and they are not. `release` clears an expired lock in another
|
|
201
|
+
run's name — that is the remedy for the #4 incident, a lease measured at **604×** its
|
|
202
|
+
2700-second TTL that no command could clear, and it is tested
|
|
203
|
+
(`test/claim_cell_test.py`). What makes it safe is the thing `reap` does not have: **a
|
|
204
|
+
person named that one key.** The reap is announced in the output, because the operator
|
|
205
|
+
asked to release *their* lease and is getting somebody else's corpse cleared as well.
|
|
206
|
+
|
|
207
|
+
`reap` is a sweep nobody named key by key. A sweep that deletes what it cannot prove it
|
|
208
|
+
owns is how one run destroys another's only record of work in progress — an agent that
|
|
209
|
+
stopped renewing may still be running. So the sweep refuses, and the two verbs keep two
|
|
210
|
+
contracts on purpose. Do not "align" them by widening `reap`.
|
|
211
|
+
|
|
146
212
|
## Id reservation
|
|
147
213
|
|
|
148
214
|
Reading a "next free id" line from a file is not reserving it. Allocation is
|
|
@@ -130,6 +130,16 @@ Generated pages are the exception, and a narrow one: they are rewritten wholesal
|
|
|
130
130
|
page whose first line has lost its `agent-sync:generated` marker is **refused** rather
|
|
131
131
|
than overwritten, because a human took it over.
|
|
132
132
|
|
|
133
|
+
**The marker itself.** The board and the mirror are machine-written, and their first line is
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
<!-- agent-sync:generated source=<repo>@<sha> at=<iso8601> — edit in git, not here -->
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A write to an object missing it is refused, not forced; report the takeover and stop. The
|
|
140
|
+
mirror is a **rendering** of git stamped with the source commit, and has no authority — when
|
|
141
|
+
its stamp and `HEAD` disagree the board gate fails, and that is drift, not formatting.
|
|
142
|
+
|
|
133
143
|
**Growth.** These logs are small — one line per event — so rotation is not urgent. When a
|
|
134
144
|
log does need trimming, archive the whole document and start a fresh one with a `base`
|
|
135
145
|
line carrying the current allocation state. Never delete lines from a live log to shrink
|