@ssheleg/agent-sync 1.16.0 → 1.17.0

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,31 @@
1
+ ## v1.17.0 — the identity that had never once been established
2
+
3
+ `_session_key()` falls back to one `shared` entry per checkout when it cannot tell which
4
+ session is asking, and everything downstream hedges about it: `classify_lock` answers
5
+ `ambiguous` rather than `reapable`, so an expired lease can never be cleared by the run that
6
+ took it. The fallback was documented as the rare case.
7
+
8
+ It was the only case. Measured 2026-08-25 in a checkout that had been running the tool all
9
+ day: `.agent-sync/sessions` did not exist, and `.agent-sync/run-id` held exactly one key —
10
+ `shared`. The stamping block in `session-start.sh` required `CLAUDE_SESSION_ID` in the hook's
11
+ **environment**, and Claude Code delivers the id to a hook on **stdin as JSON**, which is how
12
+ `guard.sh` next to it has always read its own payload. So the block had never run, in any
13
+ session, since it was written. A fallback that is always taken is not a fallback.
14
+
15
+ The hook reads its payload now, with the environment variable still winning where it exists.
16
+ `test/hooks_session_test.py` runs the real hook as a process — the only way this was ever
17
+ going to be caught, because every unit around it was correct — and covers the four ways it
18
+ must behave: an id from stdin is stamped, an id from the environment still wins, a payload
19
+ carrying no id stamps nothing rather than keying every session alike, and a payload that is
20
+ not JSON leaves the hook exit 0, because a SessionStart hook that throws takes the session
21
+ with it. A fifth case walks the whole chain: stamp, then the descendant's key, then the
22
+ run-id map, then `classify_lock` returning `reapable` where it used to return `ambiguous`.
23
+
24
+ Watched failing against the previous hook: 2 of the 5 cases red, and green after.
25
+
26
+ Also: the README no longer tells a reader to run `python3 test/validate.py` and `npm test`
27
+ from a package that ships no `test/` directory. It names where they run.
28
+
1
29
  ## Unreleased — both `AS-01` halves exercised outside their fixtures
2
30
 
3
31
  The two rows sat at priority `unverified`: shipped, and confirmed by nothing but their own
package/README.md CHANGED
@@ -444,6 +444,10 @@ and `.agent-sync/` if you want the project clean too.
444
444
 
445
445
  ## Develop and verify
446
446
 
447
+ <!-- commands-run-in: a clone -->
448
+ These run **in a clone of this repository**. The published npm package ships no
449
+ `test/` directory, so from an install they are names, not commands.
450
+
447
451
  ```bash
448
452
  python3 test/validate.py # manifests, version sync, no host/credential leaks
449
453
  python3 test/validate.py --self-test # the validator must still be able to fail
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-sync",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
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"
@@ -17,7 +17,7 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "scripts": {
20
- "test": "python3 test/validate.py && python3 test/validate.py --self-test && python3 test/claim_cell_test.py",
20
+ "test": "python3 test/validate.py && python3 test/validate.py --self-test && python3 test/claim_cell_test.py && python3 test/hooks_session_test.py",
21
21
  "prepublishOnly": "python3 test/validate.py"
22
22
  },
23
23
  "publishConfig": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-sync",
3
3
  "displayName": "Agent Sync",
4
- "version": "1.16.0",
4
+ "version": "1.17.0",
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",
@@ -6,14 +6,36 @@ S="$AGENT_SYNC_PY"
6
6
  agent_sync_configured || exit 0
7
7
 
8
8
  # Stamp who this session is, keyed by the process every command in it descends from.
9
- # A hook has CLAUDE_SESSION_ID in its environment and a plain shell command does not, so without
10
- # this a second session in the same checkout adopts the first one's identity: both acquire and
11
- # release as one run, and the lease stops separating the exact case it exists for. $PPID here is
12
- # the CLI process, which is the one ancestor every later command shares.
13
- if [ -n "${CLAUDE_SESSION_ID:-}" ]; then
9
+ # Without this a second session in the same checkout adopts the first one's identity: both
10
+ # acquire and release as one run, and the lease stops separating the exact case it exists for.
11
+ # $PPID here is the CLI process, which is the one ancestor every later command shares.
12
+ #
13
+ # The id arrives on STDIN as JSON, the way every other hook here reads its payload -- see
14
+ # guard.sh. This block used to require CLAUDE_SESSION_ID in the ENVIRONMENT and nothing else,
15
+ # so on this machine it never ran once: measured 2026-08-25, `.agent-sync/sessions` had never
16
+ # been created and the run-id map held a single `shared` key, which is the weak identity that
17
+ # makes an expired lease unattributable and `reap` refuse it forever. A fallback that is always
18
+ # taken is not a fallback.
19
+ if [ -t 0 ]; then
20
+ payload="" # no stdin (invoked by hand) -- do not block on `cat`
21
+ else
22
+ payload=$(cat 2>/dev/null || true)
23
+ fi
24
+ sid="${CLAUDE_SESSION_ID:-}"
25
+ if [ -z "$sid" ] && [ -n "$payload" ]; then
26
+ sid=$(printf '%s' "$payload" | python3 -c '
27
+ import json,sys
28
+ try:
29
+ d = json.load(sys.stdin)
30
+ except Exception:
31
+ sys.exit(0)
32
+ print(d.get("session_id") or "")
33
+ ' 2>/dev/null)
34
+ fi
35
+ if [ -n "$sid" ]; then
14
36
  d="$(git rev-parse --show-toplevel 2>/dev/null)/.agent-sync/sessions"
15
37
  if mkdir -p "$d" 2>/dev/null; then
16
- printf '%s' "$CLAUDE_SESSION_ID" > "$d/$PPID" 2>/dev/null || true
38
+ printf '%s' "$sid" > "$d/$PPID" 2>/dev/null || true
17
39
  # forget the stamps of processes that are gone, so the directory cannot grow without bound
18
40
  for f in "$d"/*; do
19
41
  b="$(basename "$f")"
@@ -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.16.0"
7
+ version: "1.17.0"
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.16.0"
36
+ VERSION = "1.17.0"
37
37
 
38
38
  CONFIG_PATH = Path(".claude/agent-sync.json")
39
39
  ENV_FILE = Path(".env.agent-sync")