@ssheleg/agent-sync 1.3.1 → 1.3.3

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,36 @@
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.3 — 2026-07-29
7
+
8
+ ### Fixed
9
+ - **The git lease backend could not take a lease on any machine without a git
10
+ identity.** Acquiring writes a lease object with `git commit-tree`, which
11
+ refuses when `user.email` is unset and cannot be auto-detected — CI runners,
12
+ containers, a freshly provisioned box. So the backend that advertises
13
+ *exclusive across machines* failed on precisely the machines least likely to
14
+ have a personal git config, with `could not create the lease object` and no
15
+ further detail. The lease object is plumbing, not authorship: it now carries a
16
+ fixed `agent-sync <agent-sync@localhost>` identity passed inline, so it never
17
+ depends on ambient config.
18
+ - The same failure now reports git's own last line instead of swallowing it. The
19
+ bug survived six red CI runs because the message named a possible cause and
20
+ showed no evidence.
21
+
22
+ ## 1.3.2 — 2026-07-29
23
+
24
+ Open-source hygiene — the repo is public and ships in the `sshlg-skills` bundle,
25
+ so the files a first-time contributor looks for now exist.
26
+
27
+ ### Added
28
+ - `CODE_OF_CONDUCT.md`, issue forms and a pull-request template. The forms ask
29
+ the question that actually matters for this project: **how many agents were
30
+ running, against what checkout** — a coordination bug reported without the
31
+ concurrency shape is not reproducible.
32
+ - The PR checklist requires a negative self-test with any new validator guard —
33
+ plant the defect, watch the check fail, then trust the green.
34
+ - README points at the code of conduct and at the family bundle.
35
+
6
36
  ## 1.3.1 — 2026-07-29
7
37
 
8
38
  ### The git lease was invisible to everything that reads a lease — fixed
@@ -24,6 +54,22 @@ already removed that note, which is why only one half of the loop was ever writt
24
54
  now runs acquire → `whoami` → `guard` → release against **both** modes; against 1.3.0 it fails with
25
55
  the two symptoms above, which is the point of adding it.
26
56
 
57
+ ## 1.3.1 — 2026-07-29
58
+
59
+ ### Ignoring the state directory does nothing once git is tracking it
60
+
61
+ Found in the project this plugin was built for: `.agent-sync/` was gitignored **and committed**,
62
+ because the files went in before the rule existed. Consequences, all of them silent:
63
+
64
+ - every tool call rewrites `last-renew`, so all three repositories were permanently dirty and no
65
+ run could ever report itself finished
66
+ - `run-id` is the checkout's **agent identity**. Committed, it reaches every clone — two machines
67
+ would then coordinate as one run, which is the failure 1.3.0 fixed at the other end
68
+
69
+ `init` now untracks the directory when it finds it tracked, and `check` reports it as a problem
70
+ rather than passing a project whose state is versioned. Probed: a repository with a committed
71
+ `.agent-sync/run-id` fails `check` with the exact removal command, and passes once it is untracked.
72
+
27
73
  ## 1.3.0 — 2026-07-29
28
74
 
29
75
  ### 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). Security
393
- reports: [SECURITY.md](SECURITY.md).
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-sync",
3
- "version": "1.3.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.",
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.",
5
5
  "bin": {
6
6
  "agent-sync": "bin/agent-sync.js"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sync",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
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.",
5
5
  "author": {
6
6
  "name": "appvillis-com"
@@ -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.1"
7
+ version: "1.3.3"
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.1"
35
+ VERSION = "1.3.3"
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
- commit = subprocess.run(["git", "commit-tree", empty_tree], input=payload,
817
- capture_output=True, text=True).stdout.strip()
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
- raise Fail("could not create the lease object — is this a git repository?")
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
@@ -1778,6 +1788,7 @@ def cmd_init(args: argparse.Namespace) -> int:
1778
1788
 
1779
1789
  ensure_gitignored(root, str(ENV_FILE))
1780
1790
  ensure_gitignored(root, f"{STATE_DIR}/")
1791
+ ensure_untracked(root, f"{STATE_DIR}/")
1781
1792
 
1782
1793
  print()
1783
1794
  if backend == "outline":
@@ -1833,6 +1844,25 @@ def ensure_gitignored(root: Path, entry: str) -> None:
1833
1844
  print(f"✓ added {entry} to .gitignore")
1834
1845
 
1835
1846
 
1847
+ def ensure_untracked(root: Path, entry: str) -> None:
1848
+ """Ignoring a path does nothing once git is already tracking it.
1849
+
1850
+ Found in a real project: the state directory had been committed before the ignore rule
1851
+ existed, so `git status` reported it modified after **every** tool call — the repository was
1852
+ permanently dirty and no run could report itself finished. `run-id` is the worse half: it is
1853
+ this checkout's agent identity, and committed it reaches every clone, so two machines
1854
+ coordinate as one run.
1855
+ """
1856
+ tracked = git("ls-files", "--", entry.rstrip("/"), cwd=root)
1857
+ if not tracked:
1858
+ return
1859
+ if git("rm", "-r", "--cached", "-q", "--", entry.rstrip("/"), cwd=root) is None:
1860
+ return
1861
+ n = len(tracked.split("\n"))
1862
+ print(f"✓ untracked {n} committed file(s) under {entry} — commit that removal; "
1863
+ "ignoring a tracked path has no effect")
1864
+
1865
+
1836
1866
  # --------------------------------------------------------------------------- status
1837
1867
 
1838
1868
  def cmd_status(_args: argparse.Namespace) -> int:
@@ -2714,6 +2744,16 @@ def cmd_check(_args: argparse.Namespace) -> int:
2714
2744
  except Fail:
2715
2745
  pass
2716
2746
 
2747
+ tracked_state = git("ls-files", "--", STATE_DIR)
2748
+ if tracked_state:
2749
+ problems.append(
2750
+ f"{STATE_DIR}/ is tracked by git ({len(tracked_state.split(chr(10)))} file(s)) — it is "
2751
+ "generated state, the repository is dirty after every tool call, and a committed "
2752
+ "run-id hands this checkout's identity to every clone. "
2753
+ f"Run: git rm -r --cached {STATE_DIR} && commit")
2754
+ else:
2755
+ ok.append(f"{STATE_DIR}/ is not tracked")
2756
+
2717
2757
  for line in ok:
2718
2758
  print(f" ✓ {line}")
2719
2759
  for line in warn: