@ssheleg/agent-sync 1.3.1 → 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 +62 -0
- package/README.md +7 -2
- package/package.json +1 -1
- package/plugins/agent-sync/.claude-plugin/plugin.json +2 -2
- package/plugins/agent-sync/skills/agent-sync/SKILL.md +1 -1
- package/plugins/agent-sync/skills/agent-sync/scripts/__pycache__/agent_sync.cpython-311.pyc +0 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/__pycache__/agent_sync.cpython-314.pyc +0 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/__pycache__/agent_sync.cpython-39.pyc +0 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/agent_sync.py +80 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
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
|
+
|
|
22
|
+
## 1.3.3 — 2026-07-29
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **The git lease backend could not take a lease on any machine without a git
|
|
26
|
+
identity.** Acquiring writes a lease object with `git commit-tree`, which
|
|
27
|
+
refuses when `user.email` is unset and cannot be auto-detected — CI runners,
|
|
28
|
+
containers, a freshly provisioned box. So the backend that advertises
|
|
29
|
+
*exclusive across machines* failed on precisely the machines least likely to
|
|
30
|
+
have a personal git config, with `could not create the lease object` and no
|
|
31
|
+
further detail. The lease object is plumbing, not authorship: it now carries a
|
|
32
|
+
fixed `agent-sync <agent-sync@localhost>` identity passed inline, so it never
|
|
33
|
+
depends on ambient config.
|
|
34
|
+
- The same failure now reports git's own last line instead of swallowing it. The
|
|
35
|
+
bug survived six red CI runs because the message named a possible cause and
|
|
36
|
+
showed no evidence.
|
|
37
|
+
|
|
38
|
+
## 1.3.2 — 2026-07-29
|
|
39
|
+
|
|
40
|
+
Open-source hygiene — the repo is public and ships in the `sshlg-skills` bundle,
|
|
41
|
+
so the files a first-time contributor looks for now exist.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- `CODE_OF_CONDUCT.md`, issue forms and a pull-request template. The forms ask
|
|
45
|
+
the question that actually matters for this project: **how many agents were
|
|
46
|
+
running, against what checkout** — a coordination bug reported without the
|
|
47
|
+
concurrency shape is not reproducible.
|
|
48
|
+
- The PR checklist requires a negative self-test with any new validator guard —
|
|
49
|
+
plant the defect, watch the check fail, then trust the green.
|
|
50
|
+
- README points at the code of conduct and at the family bundle.
|
|
51
|
+
|
|
6
52
|
## 1.3.1 — 2026-07-29
|
|
7
53
|
|
|
8
54
|
### The git lease was invisible to everything that reads a lease — fixed
|
|
@@ -24,6 +70,22 @@ already removed that note, which is why only one half of the loop was ever writt
|
|
|
24
70
|
now runs acquire → `whoami` → `guard` → release against **both** modes; against 1.3.0 it fails with
|
|
25
71
|
the two symptoms above, which is the point of adding it.
|
|
26
72
|
|
|
73
|
+
## 1.3.1 — 2026-07-29
|
|
74
|
+
|
|
75
|
+
### Ignoring the state directory does nothing once git is tracking it
|
|
76
|
+
|
|
77
|
+
Found in the project this plugin was built for: `.agent-sync/` was gitignored **and committed**,
|
|
78
|
+
because the files went in before the rule existed. Consequences, all of them silent:
|
|
79
|
+
|
|
80
|
+
- every tool call rewrites `last-renew`, so all three repositories were permanently dirty and no
|
|
81
|
+
run could ever report itself finished
|
|
82
|
+
- `run-id` is the checkout's **agent identity**. Committed, it reaches every clone — two machines
|
|
83
|
+
would then coordinate as one run, which is the failure 1.3.0 fixed at the other end
|
|
84
|
+
|
|
85
|
+
`init` now untracks the directory when it finds it tracked, and `check` reports it as a problem
|
|
86
|
+
rather than passing a project whose state is versioned. Probed: a repository with a committed
|
|
87
|
+
`.agent-sync/run-id` fails `check` with the exact removal command, and passes once it is untracked.
|
|
88
|
+
|
|
27
89
|
## 1.3.0 — 2026-07-29
|
|
28
90
|
|
|
29
91
|
### Two agents in one checkout were one identity — fixed
|
package/README.md
CHANGED
|
@@ -389,8 +389,13 @@ agent loads on their own trigger rather than by default:
|
|
|
389
389
|
| [`two-sources.md`](plugins/agent-sync/skills/agent-sync/references/two-sources.md) | before the first reconcile, or when deciding where a document belongs |
|
|
390
390
|
| [`roadmap.md`](plugins/agent-sync/skills/agent-sync/references/roadmap.md) | configuring `claimTags`, taking or closing a task, or re-planning a board |
|
|
391
391
|
|
|
392
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md).
|
|
393
|
-
|
|
392
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md). Everyone
|
|
393
|
+
taking part is expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
394
|
+
Security reports: [SECURITY.md](SECURITY.md).
|
|
395
|
+
|
|
396
|
+
`agent-sync` also ships in the [sshlg-skills](https://github.com/ssheleg/sshlg-skills)
|
|
397
|
+
bundle, which installs the whole family for Claude Code, Cursor, Codex and 70+
|
|
398
|
+
other agents with one command.
|
|
394
399
|
|
|
395
400
|
## License
|
|
396
401
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/agent-sync",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sync",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "Coordination layer for multi-agent repositories
|
|
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.
|
|
7
|
+
version: "1.3.4"
|
|
8
8
|
author: appvillis-com
|
|
9
9
|
---
|
|
10
10
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
35
|
+
VERSION = "1.3.4"
|
|
36
36
|
|
|
37
37
|
CONFIG_PATH = Path(".claude/agent-sync.json")
|
|
38
38
|
ENV_FILE = Path(".env.agent-sync")
|
|
@@ -813,10 +813,20 @@ class Sync:
|
|
|
813
813
|
payload = json.dumps({"run": self.rid, "ts": now_iso(), "ttl": self.ttl,
|
|
814
814
|
"repo": repo_name(), "host": os.uname().nodename})
|
|
815
815
|
empty_tree = git("hash-object", "-t", "tree", "/dev/null")
|
|
816
|
-
|
|
817
|
-
|
|
816
|
+
# A lease object is plumbing, not authorship, so it must not depend on the
|
|
817
|
+
# machine having a git identity. Without these `-c` flags `commit-tree`
|
|
818
|
+
# refuses wherever user.email is unset and cannot be auto-detected — CI
|
|
819
|
+
# runners, containers, a freshly provisioned box — and the lease backend
|
|
820
|
+
# silently becomes unusable on exactly the machines that need it most.
|
|
821
|
+
made = subprocess.run(
|
|
822
|
+
["git", "-c", "user.name=agent-sync", "-c", "user.email=agent-sync@localhost",
|
|
823
|
+
"commit-tree", empty_tree],
|
|
824
|
+
input=payload, capture_output=True, text=True)
|
|
825
|
+
commit = made.stdout.strip()
|
|
818
826
|
if not commit:
|
|
819
|
-
|
|
827
|
+
detail = (made.stderr or "").strip().splitlines()
|
|
828
|
+
why = detail[-1] if detail else "no output from git commit-tree"
|
|
829
|
+
raise Fail(f"could not create the lease object — is this a git repository? ({why})")
|
|
820
830
|
|
|
821
831
|
args = ["git", "push", remote, f"{commit}:{ref}"]
|
|
822
832
|
if held_sha: # stealing an expired lease, and only that
|
|
@@ -950,7 +960,36 @@ class Sync:
|
|
|
950
960
|
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
951
961
|
marker.write_text(now_iso())
|
|
952
962
|
|
|
953
|
-
def
|
|
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
|
+
|
|
954
993
|
for n in self.write_claim(key, None):
|
|
955
994
|
print(f" {n}")
|
|
956
995
|
if self.lease_mode == "git":
|
|
@@ -968,6 +1007,7 @@ class Sync:
|
|
|
968
1007
|
fmt_line("release", key, self.rid))
|
|
969
1008
|
except Fail as exc:
|
|
970
1009
|
print(f"note: released locally, not published ({exc})", file=sys.stderr)
|
|
1010
|
+
return True
|
|
971
1011
|
|
|
972
1012
|
def held(self) -> list[str]:
|
|
973
1013
|
d = self.root / STATE_DIR / "leases"
|
|
@@ -1778,6 +1818,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1778
1818
|
|
|
1779
1819
|
ensure_gitignored(root, str(ENV_FILE))
|
|
1780
1820
|
ensure_gitignored(root, f"{STATE_DIR}/")
|
|
1821
|
+
ensure_untracked(root, f"{STATE_DIR}/")
|
|
1781
1822
|
|
|
1782
1823
|
print()
|
|
1783
1824
|
if backend == "outline":
|
|
@@ -1833,6 +1874,25 @@ def ensure_gitignored(root: Path, entry: str) -> None:
|
|
|
1833
1874
|
print(f"✓ added {entry} to .gitignore")
|
|
1834
1875
|
|
|
1835
1876
|
|
|
1877
|
+
def ensure_untracked(root: Path, entry: str) -> None:
|
|
1878
|
+
"""Ignoring a path does nothing once git is already tracking it.
|
|
1879
|
+
|
|
1880
|
+
Found in a real project: the state directory had been committed before the ignore rule
|
|
1881
|
+
existed, so `git status` reported it modified after **every** tool call — the repository was
|
|
1882
|
+
permanently dirty and no run could report itself finished. `run-id` is the worse half: it is
|
|
1883
|
+
this checkout's agent identity, and committed it reaches every clone, so two machines
|
|
1884
|
+
coordinate as one run.
|
|
1885
|
+
"""
|
|
1886
|
+
tracked = git("ls-files", "--", entry.rstrip("/"), cwd=root)
|
|
1887
|
+
if not tracked:
|
|
1888
|
+
return
|
|
1889
|
+
if git("rm", "-r", "--cached", "-q", "--", entry.rstrip("/"), cwd=root) is None:
|
|
1890
|
+
return
|
|
1891
|
+
n = len(tracked.split("\n"))
|
|
1892
|
+
print(f"✓ untracked {n} committed file(s) under {entry} — commit that removal; "
|
|
1893
|
+
"ignoring a tracked path has no effect")
|
|
1894
|
+
|
|
1895
|
+
|
|
1836
1896
|
# --------------------------------------------------------------------------- status
|
|
1837
1897
|
|
|
1838
1898
|
def cmd_status(_args: argparse.Namespace) -> int:
|
|
@@ -1978,7 +2038,11 @@ def cmd_renew(args: argparse.Namespace) -> int:
|
|
|
1978
2038
|
|
|
1979
2039
|
|
|
1980
2040
|
def cmd_release(args: argparse.Namespace) -> int:
|
|
1981
|
-
|
|
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
|
|
1982
2046
|
print(f"released {args.key}")
|
|
1983
2047
|
return 0
|
|
1984
2048
|
|
|
@@ -2714,6 +2778,16 @@ def cmd_check(_args: argparse.Namespace) -> int:
|
|
|
2714
2778
|
except Fail:
|
|
2715
2779
|
pass
|
|
2716
2780
|
|
|
2781
|
+
tracked_state = git("ls-files", "--", STATE_DIR)
|
|
2782
|
+
if tracked_state:
|
|
2783
|
+
problems.append(
|
|
2784
|
+
f"{STATE_DIR}/ is tracked by git ({len(tracked_state.split(chr(10)))} file(s)) — it is "
|
|
2785
|
+
"generated state, the repository is dirty after every tool call, and a committed "
|
|
2786
|
+
"run-id hands this checkout's identity to every clone. "
|
|
2787
|
+
f"Run: git rm -r --cached {STATE_DIR} && commit")
|
|
2788
|
+
else:
|
|
2789
|
+
ok.append(f"{STATE_DIR}/ is not tracked")
|
|
2790
|
+
|
|
2717
2791
|
for line in ok:
|
|
2718
2792
|
print(f" ✓ {line}")
|
|
2719
2793
|
for line in warn:
|