@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 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.10.1",
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.",
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.10.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.10.1"
7
+ version: "1.11.0"
8
8
  author: ssheleg
9
9
  ---
10
10
 
@@ -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.10.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
- m = re.search(spec["nextFreeIdPattern"], path.read_text())
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
- cells[idx] = saved
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(spec.get("nextFreeIdPattern", ""), text)
3232
+ m = re.search(pat, text)
3187
3233
  except re.error as exc:
3188
- problems.append(f"register {reg}: nextFreeIdPattern is not valid regex ({exc})")
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 "