@ssheleg/agent-sync 1.10.1 → 1.11.1

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,50 @@
1
+ ## v1.11.1 — the gate can see an invariant it breaks elsewhere
2
+
3
+ **This gate can now see an invariant it breaks one repository away.** The family umbrella
4
+ routes work by matching a prompt against a table in `lib/triggers.js`, and every trigger
5
+ there must be a word this skill's own `description` advertises. Nothing here knew that
6
+ table existed. On 2026-08-16 `sheleg-design` 1.37.0 shipped green having dropped a phrase
7
+ that was still a live trigger, the umbrella found out minutes after the tag, and it cost a
8
+ patch release — because the member releases FIRST and the umbrella re-pins after.
9
+
10
+ `test/validate.py` now asks the umbrella's own checker (`test/advertised_check.js`), which
11
+ reads the module the hook itself calls. **No copy of the table lives here**, so there is
12
+ nothing to drift. With no umbrella above this checkout — a standalone clone, and CI — it
13
+ discloses rather than passing, because a check that cannot look must never read as one
14
+ that looked.
15
+
16
+ Watched refusing a real drop before shipping: every one of the seven members carrying
17
+ routed triggers had one of its own advertised phrases removed and every one of them failed
18
+ its own gate.
19
+
20
+ ## v1.11.0 — releasing stops rewinding a cell, and a register with no pattern says so
21
+
22
+ Two more defects in the same afternoon that produced v1.10.1, both from using this tool
23
+ on a board that had just been worked through.
24
+
25
+ ### Fixed
26
+
27
+ - **Releasing no longer rewinds a cell that moved.** The restore was verbatim, and in this
28
+ family the claim cell IS the status cell — so `close then release` silently reopened a
29
+ row closed with evidence minutes earlier. Caught by `finish` reporting the board
30
+ uncommitted, not by anybody reading it. The protocol's intent is to remove *this run's*
31
+ marker, not to rewind the cell, so a changed cell keeps its change, loses only the
32
+ marker, and the note says so. An untouched cell is still restored exactly.
33
+ - **An id register with no pattern reports instead of crashing.** The script read
34
+ `nextFreeIdPattern`; every config this family ships writes `pattern`. Both are accepted
35
+ now — but the defect was the fallback: absence became `re.search("", text)`, which
36
+ matches the empty string at position 0, so `check` took the found branch and died with
37
+ `IndexError: no such group` rather than saying the register has no pattern. A component
38
+ that never received its input approved and then fell over.
39
+
40
+ ### Added
41
+
42
+ - **`test/claim_cell_test.py`** — 6 cases driving the shipped script as a process against
43
+ real project directories, because these defects are about what the command does to a
44
+ file on disk. **Four were watched failing** against the pre-fix script, including that
45
+ exact `IndexError`. One of the six is a regression test for v1.10.1's own fix: a board
46
+ where an id is cited by two other rows must still tag the row that owns it.
47
+
1
48
  ## v1.10.1
2
49
 
3
50
  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.1",
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.1",
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.1"
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.1"
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 "