@ssheleg/agent-sync 1.7.0 → 1.7.1

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,68 @@
1
+ ## v1.7.1
2
+
3
+ **A second audit, along a different axis: not whether the tool keeps its promises, but whether an
4
+ agent reading this skill can follow it and get a correct result.** The scenarios were executed, not
5
+ imagined — cold start, the whole adoption chain, the per-task cycle, two agents contending for one
6
+ task, every hook with a realistic payload, and the edges (no git repository, uninitialised project,
7
+ absolute paths, `git -C <dir> commit`). The coordination core came through clean. What did not was
8
+ everything the agent *reads*.
9
+
10
+ ### The documents the tool generates were two versions behind the doctrine it teaches
11
+
12
+ `setup` writes the snapshot every agent is told to read first — *"it states how documentation and
13
+ coordination work here"* — and `scaffold` seeds `AGENTS.md`. Neither mentioned a branch or `merge`
14
+ even once. Both prescribed a cycle ending in `release`, and the snapshot stated *"the claim tag in
15
+ git is written through"* as an unconditional fact, which has been false on any branch since 1.4.0 —
16
+ the branch being where the doctrine says the work belongs.
17
+
18
+ So an agent doing exactly what the skill instructs — trust the generated snapshot — got the
19
+ workflow from two releases ago. Regenerating did not help: the generator was what was stale. Both
20
+ now carry the branch rule, `merge --key`, and this project's integration branch by name.
21
+
22
+ `AGENTS.md` stopped restating the cycle altogether. It is seeded once and **never overwritten**, so
23
+ a copy of the protocol there is frozen on the day the project was created while the tool moves on —
24
+ and every project scaffolded before today would have kept the old one forever. It now points at the
25
+ snapshot, which is regenerable and which `check` fails on when it goes stale. One fact, one home.
26
+
27
+ ### `check` blessed a project an agent cannot work in
28
+
29
+ A configuration declaring `idRegisters` on a backend whose `reserve` always raises passed as
30
+ `setup healthy`. `check`'s own promise is that it refuses a rule pointing at what is not there, and
31
+ a register nobody can allocate from is exactly that — while the snapshot it generates instructs
32
+ every agent to run `agent_sync.py reserve DEC`, which cannot succeed there. It is now a problem,
33
+ named with both ways out.
34
+
35
+ ### The slash command offered a verb the CLI does not have
36
+
37
+ `argument-hint` advertised `claim <KEY>` and the README showed `/agent-sync claim ASC-072`. The
38
+ command is `acquire`; `claim` is an `invalid choice`. First thing an agent reads, first thing it
39
+ types.
40
+
41
+ ### `$SKILL_DIR` was used in every example and defined nowhere
42
+
43
+ Six invocations tell the agent to run `python3 "$SKILL_DIR/scripts/agent_sync.py"`, and the only
44
+ explanation was the prose *"this skill's own directory"*. Nothing gave a value. The Cursor rule
45
+ names a concrete path; the skill body now names both — `${CLAUDE_PLUGIN_ROOT}/skills/agent-sync`
46
+ and `~/.agents/skills/agent-sync`.
47
+
48
+ ### What the audit found working
49
+
50
+ Worth recording, because a report that only lists faults says nothing about the rest: two agents
51
+ contending for one task behave correctly end to end — the second loses, sees who holds it and in
52
+ which repository, is denied the guarded file, and cannot release a lease it does not hold. The
53
+ guard denies `Edit`, `Write`, `NotebookEdit` and absolute paths, and blocks `git commit`,
54
+ `git -C <dir> commit` and `cd <dir> && git commit` when a guarded file is staged, while letting
55
+ `git log --grep=commit` and malformed JSON through. `SessionStart` stamps the identity and prints
56
+ the awareness block; `SessionEnd` releases. The adoption chain works with and without a pre-existing
57
+ `docs/`. An uninitialised project and a non-git directory both answer with the next action and a
58
+ non-zero exit.
59
+
60
+ ### New checks
61
+
62
+ `check_every_advertised_verb_exists`, `check_generated_docs_carry_current_doctrine`,
63
+ `check_registers_need_a_backend_that_can_reserve`, `check_skill_gives_a_resolvable_script_path` —
64
+ with four more self-test fixtures. The validator now plants and catches 32 distinct defects.
65
+
1
66
  ## v1.7.0
2
67
 
3
68
  **Observability, honest degradation, and the removal of things that were never load-bearing.**
package/README.md CHANGED
@@ -225,8 +225,9 @@ action:
225
225
 
226
226
  ## Everyday use
227
227
 
228
- In an agent session you use the slash command (`/agent-sync claim ASC-072`); the same
229
- commands run directly against the coordinator script, which is what the hooks and CI do:
228
+ In an agent session you use the slash command (`/agent-sync acquire ASC-072` the verb is
229
+ `acquire`, the same one the CLI takes); the same commands run directly against the
230
+ coordinator script, which is what the hooks and CI do:
230
231
 
231
232
  ```bash
232
233
  python3 "$SKILL_DIR/scripts/agent_sync.py" <command>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-sync",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
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
3
  "displayName": "Agent Sync",
4
- "version": "1.7.0",
4
+ "version": "1.7.1",
5
5
  "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.",
6
6
  "author": {
7
7
  "name": "ssheleg",
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Coordinate concurrent agents — initialise the shared knowledge store, check status, claim a task, reserve an id, or regenerate the board.
3
- argument-hint: "[init|status|claim <KEY>|release <KEY>|reserve <REG>|board|finish]"
2
+ description: Coordinate concurrent agents — initialise the shared knowledge store, check status, take a task, reserve an id, or regenerate the board.
3
+ argument-hint: "[init|status|check|acquire <KEY>|release <KEY>|reserve <REG>|board|merge|finish]"
4
4
  ---
5
5
 
6
6
  Invoke the `agent-sync` skill.
@@ -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.7.0"
7
+ version: "1.7.1"
8
8
  author: ssheleg
9
9
  ---
10
10
 
@@ -113,10 +113,9 @@ storage question gets asked and answered, once, and written down.
113
113
  - or local files (`fs`) — no credentials, and no visibility to an agent on another
114
114
  machine: no shared awareness, no cross-repo signals, no shared board.
115
115
 
116
- The lease is decided separately by `leaseBackend` `git` for cross-machine exclusion,
117
- `local` otherwise — and **`gated` follows that choice, never the record plane**. `fs`
118
- with a local lock is still real exclusion between the agents on this machine; `outline`
119
- with a local lock is *not* exclusion across them. Report the one you actually have.
116
+ The lease is decided separately by `leaseBackend` (trap 2), and **`gated` follows that
117
+ choice, never the record plane**. Report the guarantee you actually have, not the
118
+ stronger one the record plane suggests.
120
119
  2. **If cloud: the instance URL.** The URL is configuration, not a secret, so you
121
120
  may write it. The **token is not** — you never ask for it in chat, never read it
122
121
  back, and never place it yourself.
@@ -144,9 +143,9 @@ the file is their step, and the design depends on it staying theirs.
144
143
  python3 "$SKILL_DIR/scripts/agent_sync.py" status
145
144
  ```
146
145
 
147
- Idempotent. Inspects, repairs what is missing, prints a status block, names exactly
148
- ONE next action — and carries `check`'s verdict, so the command every session runs and
149
- the command that validates the setup cannot give two answers about one project.
146
+ Idempotent. Inspects, repairs, reports, names exactly ONE next action and carries
147
+ `check`'s verdict, so the command every session runs and the one that validates the
148
+ setup cannot give two answers about one project.
150
149
 
151
150
  **Read the two awareness sections it prints — they are the point, not decoration.**
152
151
 
@@ -196,7 +195,9 @@ npx sshlg-skills install
196
195
  | `scaffold [--full]` | Create only what is missing, never a line over anything that exists. `--full` also seeds the question register, the index, the dependency board, the data model with its entity register, and the docs gate |
197
196
  | `finish [--gates]` | Is the **work** finished — every repository clean, pushed and pointed at, no lease left held. `check` answers whether the project is wired correctly; this answers whether you are done |
198
197
 
199
- `$SKILL_DIR` is this skill's own directory. Every command reads
198
+ `$SKILL_DIR` is this skill's own directory: `${CLAUDE_PLUGIN_ROOT}/skills/agent-sync`
199
+ under the Claude Code plugin, `~/.agents/skills/agent-sync` elsewhere. Resolve it once
200
+ per session and reuse it — do not guess a path. Every command reads
200
201
  `.claude/agent-sync.json` from the project root and needs no arguments beyond those
201
202
  listed.
202
203
 
@@ -321,16 +322,10 @@ Two files, and the split between them is the whole security model.
321
322
  **`.claude/agent-sync.json`** — *shape*, committed: which backend, TTLs, which files
322
323
  are guarded, which registers exist, which gates to run.
323
324
 
324
- **`.env.agent-sync`** — *identity*, created by `init`, mode 600, gitignored:
325
-
326
- ```
327
- AGENT_SYNC_BACKEND=outline
328
- AGENT_SYNC_OUTLINE_URL=https://<instance>
329
- AGENT_SYNC_OUTLINE_TOKEN= # the operator fills this line, nobody else
330
- AGENT_SYNC_OUTLINE_COLLECTION= # printed by `bootstrap`
331
- ```
332
-
333
- Load it before running agents:
325
+ **`.env.agent-sync`** — *identity*, written by `init` with the keys already in it, mode
326
+ 600, gitignored. The operator fills the token line and nobody else; `bootstrap` prints
327
+ the collection id to paste beside it. Found here, or in a superproject, or wherever
328
+ `AGENT_SYNC_ENV` points — `check` prints which. Load it before running agents:
334
329
 
335
330
  ```bash
336
331
  set -a && . ./.env.agent-sync && set +a
@@ -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.7.0"
36
+ VERSION = "1.7.1"
37
37
 
38
38
  CONFIG_PATH = Path(".claude/agent-sync.json")
39
39
  ENV_FILE = Path(".env.agent-sync")
@@ -2094,19 +2094,29 @@ class Sync:
2094
2094
  "## The cycle, per task",
2095
2095
  "",
2096
2096
  "```",
2097
- "statuswho else is working, and what changed while you were away",
2097
+ "merges → what landed while you were away",
2098
+ "status → who else is working, and what changed since you last looked",
2098
2099
  "reconcile → resolve every divergence BEFORE writing code",
2099
- "acquire ID take the lease; the claim tag in git is written through",
2100
+ "branch work happens on one; the integration branch is somebody",
2101
+ " else's stable base",
2102
+ "acquire ID → take the lease. On the integration branch the claim tag is",
2103
+ " written through to git; on any other branch the holder stays",
2104
+ " in the coordination plane, where `status` shows it to everyone",
2100
2105
  " … work …",
2101
2106
  "record → what you ACTUALLY built, with the decision id and files",
2102
2107
  " … update the git documents in the same change …",
2103
2108
  "reconcile → check both sides again",
2104
2109
  "board → regenerate the shared view",
2105
- "release ID on every path, including failure",
2110
+ "merge --key land the branch: conflicts checked first, the merge recorded,",
2111
+ " that lease released. Without a branch, `release ID` by hand",
2112
+ " — on every path, including failure",
2106
2113
  "```",
2107
2114
  "",
2115
+ f"This project's integration branch is `{self.integration_branch}`.",
2116
+ "",
2108
2117
  "Full doctrine ships with the skill: `references/two-sources.md`,",
2109
- "`references/lease-protocol.md`, `references/pipeline-binding.md`.",
2118
+ "`references/lease-protocol.md`, `references/branching.md`,",
2119
+ "`references/roadmap.md`, `references/pipeline-binding.md`.",
2110
2120
  ]
2111
2121
  return "\n".join(L) + "\n"
2112
2122
 
@@ -2889,31 +2899,24 @@ annotate the old entry's status line. The body of the old entry stays as history
2889
2899
 
2890
2900
  AGENTS_SEED = """# AGENTS.md — working protocol
2891
2901
 
2892
- **Read [`{snapshot}`]({snapshot}) first.** It is generated from the live configuration and
2893
- states how documentation and coordination work here: which registers exist, which files
2894
- need a lease, which gates run, what is written where, and what is never deleted.
2895
-
2896
- ## Before you write anything
2897
-
2898
- Several agents may work this repository at once.
2902
+ **Read [`{snapshot}`]({snapshot}) first, and follow the cycle it states.** That file is
2903
+ generated from the live configuration: which registers exist, which files need a lease,
2904
+ which gates run, what is written where, what is never deleted, and the order to do it in.
2899
2905
 
2900
- 1. `agent-sync status` who else is working, and what changed while you were away.
2901
- 2. `agent-sync reconcile` the git documents say how it *should* be, the as-built record
2902
- says how it *is*. Resolve every divergence **before** writing code.
2903
- 3. `agent-sync acquire <TASK-ID>` the guarded registers refuse an unleased write.
2904
- 4. Reserve any new id with `agent-sync reserve <REGISTER>` before writing it.
2906
+ This file deliberately does **not** restate that cycle. It is seeded once and never
2907
+ overwritten, so a copy of the protocol here would be frozen on the day the project was
2908
+ created while the tool moved on and the two would disagree in front of an agent with no
2909
+ way to tell which is current. One fact, one home; the home is the generated snapshot,
2910
+ because it can be regenerated and `agent-sync check` fails when it goes stale.
2905
2911
 
2906
- ## When you finish
2912
+ ## The three that are true in every project
2907
2913
 
2908
- 1. `agent-sync record` what you actually built, with the decision id and the files.
2909
- 2. Update the git documents in the **same** change.
2910
- 3. `agent-sync reconcile` again, then `agent-sync board`.
2911
- 4. `agent-sync release <TASK-ID>`on every path, including failure.
2912
-
2913
- ## The one rule that matters most
2914
-
2915
- **No decision lives only in chat.** Record it in the decision register, propagate it to
2916
- every document it affects, and commit referencing the id.
2914
+ 1. **Several agents may work this repository at once.** `agent-sync status` before you
2915
+ start: who holds what, and what changed since you last looked.
2916
+ 2. **Work on a branch, and land it with `agent-sync merge`.** The integration branch is
2917
+ somebody else's stable basenothing about work in flight is committed there.
2918
+ 3. **No decision lives only in chat.** Record it in the decision register, propagate it to
2919
+ every document it affects, and commit referencing the id.
2917
2920
  """
2918
2921
 
2919
2922
 
@@ -3119,8 +3122,21 @@ def check_setup(root: Path) -> tuple[list[str], list[str], list[str]]:
3119
3122
  for k in sorted(set(cfg) - CONFIG_KEYS):
3120
3123
  problems.append(f"config key '{k}' is not in the schema — it will be ignored")
3121
3124
 
3122
- # Registers must exist AND their allocation pattern must actually match.
3125
+ # Registers must exist, their allocation pattern must match — and the backend must be
3126
+ # able to hand an id out at all. A register declared on a plane whose `reserve` always
3127
+ # raises is a rule that protects nothing, which is the exact thing `check` promises to
3128
+ # refuse: the generated snapshot then instructs every agent to run `reserve <REG>`, and
3129
+ # the command cannot succeed in that project.
3123
3130
  regs = cfg.get("idRegisters") or {}
3131
+ if regs:
3132
+ backend = os.environ.get("AGENT_SYNC_BACKEND") or cfg.get("backend") or "fs"
3133
+ probe = make_adapter(cfg, root)
3134
+ if not probe.is_lease_authority:
3135
+ problems.append(
3136
+ f"{len(regs)} id register(s) declared, but backend '{probe.name}' cannot "
3137
+ f"reserve ids (atomicAppend is false){' — the configured backend is ' + backend + ' and it is not reachable, so runs degrade to fs' if backend != probe.name else ''}. "
3138
+ "`reserve` fails every time here; either configure a backend that can "
3139
+ "allocate, or remove the registers and allocate them in the parent repository")
3124
3140
  for reg, spec in sorted(regs.items()):
3125
3141
  f = root / spec.get("file", "")
3126
3142
  if not f.exists():