@ssheleg/agent-sync 1.10.1 → 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 +28 -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 +51 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
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
|
+
|
|
1
29
|
## v1.10.1
|
|
2
30
|
|
|
3
31
|
The claim tag could not be placed on nearly half a real board, and the lease was granted
|
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))
|
|
@@ -1720,7 +1743,23 @@ class Sync:
|
|
|
1720
1743
|
else:
|
|
1721
1744
|
if saved is None:
|
|
1722
1745
|
continue # nothing of ours to undo
|
|
1723
|
-
|
|
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")
|
|
1724
1763
|
state.get(key, {}).pop(str(rel), None)
|
|
1725
1764
|
if not state.get(key):
|
|
1726
1765
|
state.pop(key, None)
|
|
@@ -3182,10 +3221,17 @@ def check_setup(root: Path) -> tuple[list[str], list[str], list[str]]:
|
|
|
3182
3221
|
problems.append(f"register {reg}: file '{spec.get('file')}' does not exist")
|
|
3183
3222
|
continue
|
|
3184
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
|
|
3185
3231
|
try:
|
|
3186
|
-
m = re.search(
|
|
3232
|
+
m = re.search(pat, text)
|
|
3187
3233
|
except re.error as exc:
|
|
3188
|
-
problems.append(f"register {reg}:
|
|
3234
|
+
problems.append(f"register {reg}: id pattern is not valid regex ({exc})")
|
|
3189
3235
|
continue
|
|
3190
3236
|
if not m:
|
|
3191
3237
|
problems.append(f"register {reg}: nextFreeIdPattern matches nothing in "
|