@ssheleg/agent-sync 1.3.3 → 1.3.4

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
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 1.3.4 — 2026-07-29
7
+
8
+ ### Fixed
9
+ - **`release` reported success for a lease it did not release, and cleared the
10
+ board claim on the way.** The lease plane refused correctly — a lease held by
11
+ another run stayed held, and `_git_release` printed a note saying so — but the
12
+ command printed `released <key>` over the top of it and exited 0, and
13
+ `write_claim` had already blanked the claim cell before the refusal was
14
+ reached. The result was the board advertising a task as free while the lease
15
+ still held it: the exact disagreement a lease exists to prevent, manufactured
16
+ by the tool. Ownership is now checked **first**, in whichever plane arbitrates
17
+ it; nothing is written when the answer is no; the command exits non-zero and
18
+ says who holds it. Both lease modes were affected.
19
+ - A regression check covers it in `local` and `git` mode, and was probed against
20
+ the old code in both before being trusted.
21
+
6
22
  ## 1.3.3 — 2026-07-29
7
23
 
8
24
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-sync",
3
- "version": "1.3.3",
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.3.4",
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
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-sync",
3
- "version": "1.3.3",
4
- "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.",
3
+ "version": "1.3.4",
4
+ "description": "Coordination layer for multi-agent repositories \u2014 leases with TTL, race-free ID reservation, a run journal, a cross-repo signal feed and a generated board, over a pluggable knowledge cloud.",
5
5
  "author": {
6
6
  "name": "appvillis-com"
7
7
  },
@@ -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.3.3"
7
+ version: "1.3.4"
8
8
  author: appvillis-com
9
9
  ---
10
10
 
@@ -32,7 +32,7 @@ from datetime import datetime, timezone
32
32
  from pathlib import Path
33
33
  from typing import Any
34
34
 
35
- VERSION = "1.3.3"
35
+ VERSION = "1.3.4"
36
36
 
37
37
  CONFIG_PATH = Path(".claude/agent-sync.json")
38
38
  ENV_FILE = Path(".env.agent-sync")
@@ -960,7 +960,36 @@ class Sync:
960
960
  marker.parent.mkdir(parents=True, exist_ok=True)
961
961
  marker.write_text(now_iso())
962
962
 
963
- def release(self, key: str) -> None:
963
+ def _lease_holder(self, key: str) -> str | None:
964
+ """Who holds this lease right now, in whichever plane arbitrates it."""
965
+ if self.lease_mode == "git":
966
+ sha, held = self._git_read_lease(key)
967
+ return held.get("run") if sha else None
968
+ lock = self._local_lock(key)
969
+ if not lock.exists():
970
+ return None
971
+ try:
972
+ return json.loads(lock.read_text()).get("run")
973
+ except (json.JSONDecodeError, OSError):
974
+ return None
975
+
976
+ def release(self, key: str) -> bool:
977
+ """Release only what this run holds, and say so plainly when it does not.
978
+
979
+ This used to clear the board claim and report success unconditionally. The lease
980
+ plane refused correctly — `_git_release` prints a note and returns — but the
981
+ caller printed "released" over the top of it and exited 0, and `write_claim` had
982
+ already blanked the claim cell on the way in. The board then said the task was
983
+ free while the lease said it was taken: the exact disagreement a lease exists to
984
+ prevent, manufactured by the tool. Ownership is therefore checked FIRST, and
985
+ nothing is written when the answer is no.
986
+ """
987
+ holder = self._lease_holder(key)
988
+ if holder is not None and holder != self.rid:
989
+ print(f"note: {key} is held by {holder}, not this run — nothing released",
990
+ file=sys.stderr)
991
+ return False
992
+
964
993
  for n in self.write_claim(key, None):
965
994
  print(f" {n}")
966
995
  if self.lease_mode == "git":
@@ -978,6 +1007,7 @@ class Sync:
978
1007
  fmt_line("release", key, self.rid))
979
1008
  except Fail as exc:
980
1009
  print(f"note: released locally, not published ({exc})", file=sys.stderr)
1010
+ return True
981
1011
 
982
1012
  def held(self) -> list[str]:
983
1013
  d = self.root / STATE_DIR / "leases"
@@ -2008,7 +2038,11 @@ def cmd_renew(args: argparse.Namespace) -> int:
2008
2038
 
2009
2039
 
2010
2040
  def cmd_release(args: argparse.Namespace) -> int:
2011
- Sync().release(args.key)
2041
+ # Exit non-zero when nothing was released. A caller that scripts `release` in a
2042
+ # cleanup path has no other way to learn the lease is still out there.
2043
+ if not Sync().release(args.key):
2044
+ print(f"NOT released: {args.key} is held by another run", file=sys.stderr)
2045
+ return 1
2012
2046
  print(f"released {args.key}")
2013
2047
  return 0
2014
2048