@ssheleg/agent-sync 1.10.0 → 1.11.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 +57 -0
- package/package.json +3 -3
- package/plugins/agent-sync/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-sync/skills/agent-sync/SKILL.md +1 -1
- package/plugins/agent-sync/skills/agent-sync/scripts/__pycache__/agent_sync.cpython-312.pyc +0 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/agent_sync.py +67 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
## v1.11.0 — releasing stops rewinding a cell, and a register with no pattern says so
|
|
2
|
+
|
|
3
|
+
Two more defects in the same afternoon that produced v1.10.1, both from using this tool
|
|
4
|
+
on a board that had just been worked through.
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
- **Releasing no longer rewinds a cell that moved.** The restore was verbatim, and in this
|
|
9
|
+
family the claim cell IS the status cell — so `close then release` silently reopened a
|
|
10
|
+
row closed with evidence minutes earlier. Caught by `finish` reporting the board
|
|
11
|
+
uncommitted, not by anybody reading it. The protocol's intent is to remove *this run's*
|
|
12
|
+
marker, not to rewind the cell, so a changed cell keeps its change, loses only the
|
|
13
|
+
marker, and the note says so. An untouched cell is still restored exactly.
|
|
14
|
+
- **An id register with no pattern reports instead of crashing.** The script read
|
|
15
|
+
`nextFreeIdPattern`; every config this family ships writes `pattern`. Both are accepted
|
|
16
|
+
now — but the defect was the fallback: absence became `re.search("", text)`, which
|
|
17
|
+
matches the empty string at position 0, so `check` took the found branch and died with
|
|
18
|
+
`IndexError: no such group` rather than saying the register has no pattern. A component
|
|
19
|
+
that never received its input approved and then fell over.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`test/claim_cell_test.py`** — 6 cases driving the shipped script as a process against
|
|
24
|
+
real project directories, because these defects are about what the command does to a
|
|
25
|
+
file on disk. **Four were watched failing** against the pre-fix script, including that
|
|
26
|
+
exact `IndexError`. One of the six is a regression test for v1.10.1's own fix: a board
|
|
27
|
+
where an id is cited by two other rows must still tag the row that owns it.
|
|
28
|
+
|
|
29
|
+
## v1.10.1
|
|
30
|
+
|
|
31
|
+
The claim tag could not be placed on nearly half a real board, and the lease was granted
|
|
32
|
+
anyway.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **A row that CITES an id no longer defeats that id's claim.** `claimTags` in `cell` mode
|
|
37
|
+
selected every row *containing* the id, so an ordinary board sentence — "closed by
|
|
38
|
+
B-12", "blocked until T-1 ships" — made the id ambiguous and the tag was refused.
|
|
39
|
+
Cross-referencing is what boards do: measured on `sshlg-skills`' own board on
|
|
40
|
+
2026-08-14, **14 of 41 rows cited others, leaving 19 ids that could never be tagged**.
|
|
41
|
+
|
|
42
|
+
The refusal itself was correct — guessing which row carries a claim is how a claim lands
|
|
43
|
+
on somebody else's work. What was wrong is that `acquire` still returned `won`: the
|
|
44
|
+
lease was granted while the registry silently carried no claim, which is precisely the
|
|
45
|
+
state a claim exists to make visible.
|
|
46
|
+
|
|
47
|
+
A markdown board declares which row is which in its **first cell**. `acquire` now
|
|
48
|
+
narrows to the row whose id cell equals the key, and refuses only when no row's id cell
|
|
49
|
+
matches. Tried *after* the existing marker narrowing, so `release` still trusts the
|
|
50
|
+
marker it actually wrote rather than re-deriving the row.
|
|
51
|
+
|
|
52
|
+
Covered by `check_claim_lands_on_the_row_whose_id_cell_matches()`, which builds a two-row
|
|
53
|
+
board where the second row cites the first, and asserts the tag lands on exactly one row
|
|
54
|
+
and that it is the right one — then that release still restores the file byte for byte.
|
|
55
|
+
Watched failing before the fix: *"acquire wrote no claim — a row citing T-1 defeated the
|
|
56
|
+
selector, and the lease was granted with the registry unmarked"*.
|
|
57
|
+
|
|
1
58
|
## v1.10.0
|
|
2
59
|
|
|
3
60
|
Following `task-pipeline` v1.53.0, which renamed the artifact root's default from
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/agent-sync",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Let concurrent coding agents share one project without colliding
|
|
3
|
+
"version": "1.11.0",
|
|
4
|
+
"description": "Let concurrent coding agents share one project without colliding \u2014 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"
|
|
7
7
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"LICENSE"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "python3 test/validate.py && python3 test/validate.py --self-test",
|
|
18
|
+
"test": "python3 test/validate.py && python3 test/validate.py --self-test && python3 test/claim_cell_test.py",
|
|
19
19
|
"prepublishOnly": "python3 test/validate.py"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sync",
|
|
3
3
|
"displayName": "Agent Sync",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.11.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",
|
|
@@ -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.11.0"
|
|
8
8
|
author: ssheleg
|
|
9
9
|
---
|
|
10
10
|
|
|
Binary file
|
|
@@ -33,7 +33,7 @@ from datetime import datetime, timezone
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import Any
|
|
35
35
|
|
|
36
|
-
VERSION = "1.
|
|
36
|
+
VERSION = "1.11.0"
|
|
37
37
|
|
|
38
38
|
CONFIG_PATH = Path(".claude/agent-sync.json")
|
|
39
39
|
ENV_FILE = Path(".env.agent-sync")
|
|
@@ -201,6 +201,24 @@ class Fail(Exception):
|
|
|
201
201
|
_GLOB_CACHE: dict[str, re.Pattern[str]] = {}
|
|
202
202
|
|
|
203
203
|
|
|
204
|
+
|
|
205
|
+
def id_pattern(spec: dict) -> str | None:
|
|
206
|
+
"""The regex that finds a register's next free id, under either accepted key.
|
|
207
|
+
|
|
208
|
+
The script read `nextFreeIdPattern`; every config this family ships writes `pattern`.
|
|
209
|
+
Both are accepted now, because a key mismatch is a documentation problem and should
|
|
210
|
+
not be a crash — but ABSENCE returns None rather than `""`, which is the whole defect:
|
|
211
|
+
`re.search("", text)` matches the empty string at position 0, so `check` took the found
|
|
212
|
+
branch and died with `IndexError: no such group` instead of saying the register has no
|
|
213
|
+
pattern. A component that never received its input approved and then fell over.
|
|
214
|
+
"""
|
|
215
|
+
for k in ("nextFreeIdPattern", "pattern"):
|
|
216
|
+
v = spec.get(k)
|
|
217
|
+
if v:
|
|
218
|
+
return v
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
204
222
|
def matches_glob(rel: str, pattern: str) -> bool:
|
|
205
223
|
"""Repo-root-anchored glob — ONE implementation, for the guard and for `check`.
|
|
206
224
|
|
|
@@ -1372,7 +1390,12 @@ class Sync:
|
|
|
1372
1390
|
path = self.root / spec["file"]
|
|
1373
1391
|
if not path.exists():
|
|
1374
1392
|
raise Fail(f"register file {spec['file']} does not exist")
|
|
1375
|
-
|
|
1393
|
+
pat = id_pattern(spec)
|
|
1394
|
+
if not pat:
|
|
1395
|
+
raise Fail(f"register has no id pattern — set `pattern` (or the legacy "
|
|
1396
|
+
f"`nextFreeIdPattern`) for {spec['file']}; without one there is "
|
|
1397
|
+
f"nothing to read a next free id out of")
|
|
1398
|
+
m = re.search(pat, path.read_text())
|
|
1376
1399
|
if not m:
|
|
1377
1400
|
raise Fail(f"could not read the next free id out of {spec['file']}")
|
|
1378
1401
|
return int(m.group(1))
|
|
@@ -1668,6 +1691,22 @@ class Sync:
|
|
|
1668
1691
|
if len(narrowed) == 1:
|
|
1669
1692
|
hits = narrowed
|
|
1670
1693
|
|
|
1694
|
+
# A board says which row is which in its FIRST cell. Every other match is a
|
|
1695
|
+
# row CITING this id — "closed by B-12", "blocked until T-1 ships" — and boards
|
|
1696
|
+
# cross-reference constantly: on the family's own board 14 rows cited others, so
|
|
1697
|
+
# 19 of 41 ids had two candidate rows and their claim was refused. The refusal
|
|
1698
|
+
# was right; granting the lease anyway was not, because the registry then carries
|
|
1699
|
+
# no claim while `acquire` reports `won`. Tried after the marker so a release
|
|
1700
|
+
# still trusts what it actually wrote.
|
|
1701
|
+
if len(hits) > 1:
|
|
1702
|
+
own = []
|
|
1703
|
+
for i in hits:
|
|
1704
|
+
cells = self._row_cells(lines[i])
|
|
1705
|
+
if cells and cells[0].strip() == key:
|
|
1706
|
+
own.append(i)
|
|
1707
|
+
if len(own) == 1:
|
|
1708
|
+
hits = own
|
|
1709
|
+
|
|
1671
1710
|
if len(hits) > 1:
|
|
1672
1711
|
notes.append(f"{rel}: `{key}` appears in {len(hits)} table rows — refusing "
|
|
1673
1712
|
"to guess which one is the claim. Narrow the pattern or edit by hand")
|
|
@@ -1704,7 +1743,23 @@ class Sync:
|
|
|
1704
1743
|
else:
|
|
1705
1744
|
if saved is None:
|
|
1706
1745
|
continue # nothing of ours to undo
|
|
1707
|
-
|
|
1746
|
+
# Restoring VERBATIM loses any edit made while the claim was held, and in
|
|
1747
|
+
# this family the claim cell IS the status cell — so `close then release`
|
|
1748
|
+
# silently reopened a row closed with evidence minutes earlier (B-35,
|
|
1749
|
+
# 2026-08-14, caught by `finish` reporting the board uncommitted, not by
|
|
1750
|
+
# anybody reading it). The protocol's intent is to remove this run's
|
|
1751
|
+
# marker, not to rewind the cell, so when the text moved on, strip the
|
|
1752
|
+
# marker and keep the movement.
|
|
1753
|
+
template = spec.get("held") or "{prev} (claimed: {holder})"
|
|
1754
|
+
marker = template.replace("{prev}", "").replace("{holder}", self.rid).strip()
|
|
1755
|
+
expected = template.replace("{prev}", saved.strip()).replace("{holder}", self.rid)
|
|
1756
|
+
if current.strip() == expected.strip() or not marker:
|
|
1757
|
+
cells[idx] = saved
|
|
1758
|
+
else:
|
|
1759
|
+
stripped = current.replace(marker, "").rstrip()
|
|
1760
|
+
cells[idx] = (stripped if stripped.strip() else saved)
|
|
1761
|
+
notes.append(f"{rel}: `{key}`'s cell changed while the claim was held — "
|
|
1762
|
+
f"kept the change and removed only the claim marker")
|
|
1708
1763
|
state.get(key, {}).pop(str(rel), None)
|
|
1709
1764
|
if not state.get(key):
|
|
1710
1765
|
state.pop(key, None)
|
|
@@ -3166,10 +3221,17 @@ def check_setup(root: Path) -> tuple[list[str], list[str], list[str]]:
|
|
|
3166
3221
|
problems.append(f"register {reg}: file '{spec.get('file')}' does not exist")
|
|
3167
3222
|
continue
|
|
3168
3223
|
text = f.read_text()
|
|
3224
|
+
pat = id_pattern(spec)
|
|
3225
|
+
if not pat:
|
|
3226
|
+
problems.append(f"register {reg}: no id pattern — set `pattern` (or the legacy "
|
|
3227
|
+
f"`nextFreeIdPattern`). An absent one used to become "
|
|
3228
|
+
f"`re.search(\"\", text)`, which matches at position 0, so this "
|
|
3229
|
+
f"check took the found branch and crashed instead of reporting")
|
|
3230
|
+
continue
|
|
3169
3231
|
try:
|
|
3170
|
-
m = re.search(
|
|
3232
|
+
m = re.search(pat, text)
|
|
3171
3233
|
except re.error as exc:
|
|
3172
|
-
problems.append(f"register {reg}:
|
|
3234
|
+
problems.append(f"register {reg}: id pattern is not valid regex ({exc})")
|
|
3173
3235
|
continue
|
|
3174
3236
|
if not m:
|
|
3175
3237
|
problems.append(f"register {reg}: nextFreeIdPattern matches nothing in "
|