@ssheleg/agent-sync 1.3.3 → 1.3.5

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,39 @@
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.5 — 2026-07-29
7
+
8
+ ### The two rules this plugin enforces are now stated, with the failures that taught them
9
+
10
+ Both are already code. Neither was written down, and a mechanism nobody can explain is a mechanism
11
+ the next person removes.
12
+
13
+ - **Identity comes before coordination.** Both ways of getting it wrong have happened here: one
14
+ session with two identities (acquired as one, denied by its own guard as the other) and two
15
+ sessions with one identity, which is worse because it is silent — `whoami` reported a lease that
16
+ belonged to somebody else and `release` would have taken it.
17
+ - **Work in a submodule is not finished until its parent points at it.** Neither repository looks
18
+ wrong alone; the disagreement lives between them, which is why it survives every check that runs
19
+ inside one. That is what `finish` is for.
20
+ - **The rule under both:** before trusting a tool's report about the world, make it report something
21
+ you can already verify.
22
+
23
+ ## 1.3.4 — 2026-07-29
24
+
25
+ ### Fixed
26
+ - **`release` reported success for a lease it did not release, and cleared the
27
+ board claim on the way.** The lease plane refused correctly — a lease held by
28
+ another run stayed held, and `_git_release` printed a note saying so — but the
29
+ command printed `released <key>` over the top of it and exited 0, and
30
+ `write_claim` had already blanked the claim cell before the refusal was
31
+ reached. The result was the board advertising a task as free while the lease
32
+ still held it: the exact disagreement a lease exists to prevent, manufactured
33
+ by the tool. Ownership is now checked **first**, in whichever plane arbitrates
34
+ it; nothing is written when the answer is no; the command exits non-zero and
35
+ says who holds it. Both lease modes were affected.
36
+ - A regression check covers it in `local` and `git` mode, and was probed against
37
+ the old code in both before being trusted.
38
+
6
39
  ## 1.3.3 — 2026-07-29
7
40
 
8
41
  ### Fixed
package/README.md CHANGED
@@ -137,6 +137,20 @@ claude plugin marketplace add appvillis-com/agent-sync && claude plugin install
137
137
 
138
138
  ## Update
139
139
 
140
+ **Update the whole family — one package, every agent.** A bundle with one member current and the
141
+ rest stale is a combination nobody tested:
142
+
143
+ ```bash
144
+ npx sshlg-skills update # installed but behind — updates everything
145
+ npx sshlg-skills install # nothing installed yet
146
+ npx --yes sshlg-skills@latest list # what the current release of each member is
147
+ ```
148
+
149
+ Restart your agent afterwards: skills and hooks load at session start, so the session that updates
150
+ is not the session that gets the new ones.
151
+
152
+ <details><summary>Updating this one member only</summary>
153
+
140
154
  **agent-sync itself** — update every channel you installed, then restart Claude Code:
141
155
 
142
156
  ```bash
@@ -167,6 +181,8 @@ npx sshlg-skills install
167
181
  Nothing else to update: the coordinator is stdlib-only python, and the npm package has
168
182
  zero runtime dependencies.
169
183
 
184
+ </details>
185
+
170
186
  ## Set up a project
171
187
 
172
188
  **Initialisation is the first command, and it asks a question rather than guessing.**
@@ -387,6 +403,7 @@ agent loads on their own trigger rather than by default:
387
403
  | [`pipeline-binding.md`](plugins/agent-sync/skills/agent-sync/references/pipeline-binding.md) | wiring `pipeline.json`, or adding a stage hook |
388
404
  | [`hooks.md`](plugins/agent-sync/skills/agent-sync/references/hooks.md) | installing, debugging or removing the Claude Code hooks |
389
405
  | [`two-sources.md`](plugins/agent-sync/skills/agent-sync/references/two-sources.md) | before the first reconcile, or when deciding where a document belongs |
406
+ | [`earned-rules.md`](plugins/agent-sync/skills/agent-sync/references/earned-rules.md) | why identity is resolved the way it is, and why `finish` exists — the two failures that produced both |
390
407
  | [`roadmap.md`](plugins/agent-sync/skills/agent-sync/references/roadmap.md) | configuring `claimTags`, taking or closing a task, or re-planning a board |
391
408
 
392
409
  See [CONTRIBUTING.md](CONTRIBUTING.md) and [CHANGELOG.md](CHANGELOG.md). Everyone
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.5",
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.5",
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.5"
8
8
  author: appvillis-com
9
9
  ---
10
10
 
@@ -371,12 +371,20 @@ The mirror is a **rendering** of git, stamped with the source commit. It has no
371
371
  authority. When its stamp and `HEAD` disagree, the board gate fails — that is
372
372
  drift, not a formatting problem.
373
373
 
374
+ ## Two rules, and the failures that taught them
375
+
376
+ Identity comes before coordination: a lease is only a lease if two agents get two identities. A
377
+ submodule commit is unfinished until its parent points at it. Both:
378
+ [`references/earned-rules.md`](references/earned-rules.md).
379
+
374
380
  ## Non-negotiables
375
381
 
376
382
  - Append, read back, then act. Never rewrite a coordination document.
377
383
  - `release` what you `acquire`, on every path including failure.
378
384
  - Credentials never reach `argv`, a log line, or the repository.
379
385
  - Degrade out loud. `ungated` is an acceptable state; a false claim of enforcement is not.
386
+ - Two agents in one checkout are two identities, or the lease is decoration.
387
+ - A submodule commit is unfinished until the parent points at it — `finish` before you call it done.
380
388
  - Everything the cloud holds about a durable fact is a link to git, never a substitute.
381
389
 
382
390
  ## References
@@ -0,0 +1,27 @@
1
+ # Two rules this plugin exists to enforce, and the failures that taught them
2
+
3
+ Both were found by running this plugin on a four-repository project with several agents working at
4
+ once. Each is now code, not advice — and each is stated here because a mechanism nobody can explain
5
+ is a mechanism the next person removes.
6
+
7
+ **Identity comes before coordination.** A lease is only a lease if two agents get two identities.
8
+ Both ends of getting this wrong have happened: deriving the id from `CLAUDE_SESSION_ID` alone gave
9
+ **one session two identities** — it acquired as one and was denied by its own guard as the other —
10
+ and keeping one id per checkout gave **two sessions one identity**, which is worse because it is
11
+ silent. Both acquired as one run, both were guarded as one run, `whoami` reported a lease that
12
+ belonged to somebody else, and `release` would have taken it. The resolution order is
13
+ `AGENT_SYNC_RUN_ID` · `CLAUDE_SESSION_ID` · the session that started this shell · shared, and where
14
+ none can be established the run says so instead of presenting a shared entry as separation.
15
+
16
+ **Work in a submodule is not finished until its parent says so.** A parent records each submodule as
17
+ a pointer to one commit, and moving the submodule does not move the pointer. The work is committed,
18
+ pushed, green in CI and marked done in its own roadmap — and a clone of the parent gets the commit
19
+ **before** it. Neither repository looks wrong on its own; the disagreement exists only between them,
20
+ which is why it survives every check that runs inside one. `finish` is that check, and it is the
21
+ reason this plugin's pipeline binding no longer describes gate expressions it never ran.
22
+
23
+ **The rule under both:** before trusting a tool's report about the world, make it report something
24
+ you can already verify. A test suite reporting green having skipped every assertion, a gate printing
25
+ `FAIL` and exiting `0`, containers reporting healthy while the tools talked to the host's services,
26
+ and a lease held by the wrong identity are the same failure — a tool describing a world it is not
27
+ looking at.
@@ -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.5"
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