@ssheleg/agent-sync 1.18.3 → 1.18.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 +51 -0
- package/README.md +1 -0
- package/bin/agent-sync.js +109 -13
- package/package.json +3 -3
- package/plugins/agent-sync/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-sync/hooks/guard.sh +6 -2
- package/plugins/agent-sync/skills/agent-sync/SKILL.md +1 -1
- package/plugins/agent-sync/skills/agent-sync/scripts/agent_sync.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
## v1.18.5 — a pipe reaches the guard, and the installers stop deleting blind
|
|
2
|
+
|
|
3
|
+
Two enforcement holes, each of the same shape: a mechanism whose own text claimed more
|
|
4
|
+
than its code did.
|
|
5
|
+
|
|
6
|
+
- **ASY-05 — a piped commit bypassed the lease guard entirely.** `guard.sh`'s tokenizer
|
|
7
|
+
said "each `&&`/`;`/`|` segment is its own command" and consumed only `&&`, `;` and
|
|
8
|
+
`||` — so `echo msg | git commit -F -`, the ordinary way to commit a generated
|
|
9
|
+
message, was one segment whose first token is `echo`, and the whole pipeline skipped
|
|
10
|
+
the guard. The single pipe and `|&` are now consumed (ordered after `||`, or each
|
|
11
|
+
would shatter into stray halves), three new `GUARD_SHAPES` cover the bypass and the
|
|
12
|
+
pipe that must NOT block (`git log | grep commit`), and the self-test plants the
|
|
13
|
+
shipped tokenizer back (`a piped commit slips past the guard`). Watched failing
|
|
14
|
+
against the pre-fix guard: both piped shapes reached a guarded file with exit 0.
|
|
15
|
+
- **The installers consulted nothing before deleting the Claude Code channel.** This
|
|
16
|
+
member's installers never write `~/.claude/skills/agent-sync` themselves — the skills
|
|
17
|
+
CLI they drive recreates it — and both deleted that copy unconditionally afterwards.
|
|
18
|
+
The family canon this implements (make-skill v0.25.0, distribution.md §3) names the
|
|
19
|
+
fail-open class, and here it ran in mirror image: on a home where the plugin is NOT
|
|
20
|
+
installed — no claude CLI, or the plugin install failed — the prune destroyed the only
|
|
21
|
+
Claude Code channel the very same run had just installed, and exited 0. The fate of
|
|
22
|
+
the copy is now a decision read from the target home's
|
|
23
|
+
`~/.claude/plugins/installed_plugins.json` (the record of what is installed; plugin
|
|
24
|
+
and marketplace names differ, so the spec is taken from the JSON), with the
|
|
25
|
+
`marketplaces/<name>` dir kept only as the fallback signal: plugin present → the
|
|
26
|
+
shadow is pruned and the message names the real spec, the plugin-channel remedy and
|
|
27
|
+
the family launcher; no plugin → the copy is kept, because it IS the Claude Code
|
|
28
|
+
channel; `--force` → kept beside the plugin, as the recorded choice to run two
|
|
29
|
+
channels where the stale one wins. Absent or corrupt JSON reads as "no plugin" — fail
|
|
30
|
+
open, never crash. Only the Claude Code channel is gated; other agents' installs are
|
|
31
|
+
untouched.
|
|
32
|
+
- **The install now says how the next version arrives** — `npx @ssheleg/agent-sync@latest
|
|
33
|
+
update`, or the family launcher — because an installer that never mentions updates has
|
|
34
|
+
still chosen an update model: never.
|
|
35
|
+
- `test/installer_test.js`: 11 cases against throwaway HOMEs with the delegated CLIs
|
|
36
|
+
stubbed through PATH — plugin-present prune with the spec from the JSON, a
|
|
37
|
+
differently-named marketplace, `--force`, corrupt JSON, a prefix-collider
|
|
38
|
+
(`agent-sync-extra@x`), the marketplaces-dir fallback, the update path, and the
|
|
39
|
+
install.sh mirrors. Ten of eleven watched failing against the pre-fix installers.
|
|
40
|
+
Wired into `npm test` and CI; `hooks_session_test.py` — already in `npm test` —
|
|
41
|
+
joins CI in the same step block.
|
|
42
|
+
|
|
43
|
+
## v1.18.4 — the channel that sends the installs, on npm too
|
|
44
|
+
|
|
45
|
+
- The `skills.sh` badge and the canonical `homepage` reached GitHub in the previous cycle and stopped
|
|
46
|
+
there: npm serves the README and the metadata from the last **publish**, so the package
|
|
47
|
+
page still showed a badge-less README and a homepage pointing at GitHub.
|
|
48
|
+
This release carries both across.
|
|
49
|
+
- No behaviour changes. Cut because a change that lands on `main` and never publishes is a
|
|
50
|
+
change the package's own readers cannot see.
|
|
51
|
+
|
|
1
52
|
## v1.18.3 — the shared seam is explicit
|
|
2
53
|
|
|
3
54
|
Both shared validators now state `diverges: none`, completing the umbrella
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@ssheleg/agent-sync)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://skills.sshlg.me/skills/agent-sync/)
|
|
7
|
+
[](https://skills.sh/ssheleg/agent-sync)
|
|
7
8
|
|
|
8
9
|
**Coordinate concurrent coding agents with expiring claims, race-free ids and a durable run journal.**
|
|
9
10
|
|
package/bin/agent-sync.js
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* One channel per agent: Claude Code gets the plugin, every other agent gets the
|
|
8
8
|
* skill through the vercel skills CLI, and the plain ~/.claude/skills/agent-sync
|
|
9
|
-
* copy that the skills CLI recreates on its own is
|
|
10
|
-
*
|
|
9
|
+
* copy that the skills CLI recreates on its own is settled afterwards against
|
|
10
|
+
* the home's installed_plugins.json — pruned when the plugin owns Claude Code
|
|
11
|
+
* (the duplicate would shadow it and silently serve a stale skill), kept when no
|
|
12
|
+
* plugin does (then it IS the Claude Code channel), kept on --force as the
|
|
13
|
+
* recorded choice to run two channels. Canon: make-skill distribution.md §3.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
const { spawnSync } = require('child_process');
|
|
@@ -19,6 +22,41 @@ const REPO = 'ssheleg/agent-sync';
|
|
|
19
22
|
const NAME = 'agent-sync';
|
|
20
23
|
const SHADOW = path.join(os.homedir(), '.claude', 'skills', NAME);
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The plugin spec (`<name>@<marketplace>`) installed for `name` in this home,
|
|
27
|
+
* or null.
|
|
28
|
+
*
|
|
29
|
+
* `installed_plugins.json` is the record of what is actually installed. The
|
|
30
|
+
* `plugins/marketplaces/<name>` directory — the only signal the family's
|
|
31
|
+
* installers read until the 2026-08-29 canon (make-skill v0.25.0,
|
|
32
|
+
* distribution.md §3) — under-reports: a marketplace added from a local
|
|
33
|
+
* `directory` source has no dir there at all, and plugin names differ from
|
|
34
|
+
* marketplace names, so a check keyed on it stays green while the shadow
|
|
35
|
+
* lands. Absence and corruption both read as "no plugin": the fresh HOME is
|
|
36
|
+
* the common case, and an installer that crashes on a parse error refuses the
|
|
37
|
+
* machines that need it most.
|
|
38
|
+
*/
|
|
39
|
+
function installedPluginSpec(home, name) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(
|
|
42
|
+
path.join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
const plugins =
|
|
45
|
+
parsed && typeof parsed === 'object' &&
|
|
46
|
+
parsed.plugins && typeof parsed.plugins === 'object'
|
|
47
|
+
? parsed.plugins
|
|
48
|
+
: parsed;
|
|
49
|
+
if (!plugins || typeof plugins !== 'object') return null;
|
|
50
|
+
for (const spec of Object.keys(plugins)) {
|
|
51
|
+
if (spec === name) return `${name}@${name}`;
|
|
52
|
+
if (spec.startsWith(name + '@')) return spec;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// missing or corrupt = no plugin — fail open on absence, never crash
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
22
60
|
const C = {
|
|
23
61
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
24
62
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
@@ -46,7 +84,12 @@ ${C.bold('agent-sync')} — coordination for concurrent agents
|
|
|
46
84
|
npx @ssheleg/${NAME} install install for Claude Code and other agents
|
|
47
85
|
npx @ssheleg/${NAME} install --claude-only Claude Code plugin only
|
|
48
86
|
npx @ssheleg/${NAME} install --agent a,b pick agents for the skills CLI
|
|
49
|
-
npx @ssheleg/${NAME} update update every channel, then
|
|
87
|
+
npx @ssheleg/${NAME} update update every channel, then settle the
|
|
88
|
+
~/.claude/skills copy: pruned when it
|
|
89
|
+
would shadow the installed plugin, kept
|
|
90
|
+
when no plugin owns Claude Code
|
|
91
|
+
npx @ssheleg/${NAME} install|update --force keep the plain copy even beside the
|
|
92
|
+
plugin — two channels, the stale one wins
|
|
50
93
|
npx @ssheleg/${NAME} --help
|
|
51
94
|
|
|
52
95
|
After installing, initialise the project — this is the step that asks where
|
|
@@ -64,6 +107,7 @@ asks for a token and never stores one.
|
|
|
64
107
|
function install(argv) {
|
|
65
108
|
const claudeOnly = argv.includes('--claude-only');
|
|
66
109
|
const noClaude = argv.includes('--no-claude');
|
|
110
|
+
const force = argv.includes('--force');
|
|
67
111
|
const agentIdx = argv.indexOf('--agent');
|
|
68
112
|
const agents = agentIdx !== -1 && argv[agentIdx + 1] ? argv[agentIdx + 1].split(',') : null;
|
|
69
113
|
|
|
@@ -88,7 +132,7 @@ function install(argv) {
|
|
|
88
132
|
ok = run('npx', args) && ok;
|
|
89
133
|
}
|
|
90
134
|
|
|
91
|
-
|
|
135
|
+
settleClaudeChannel(force);
|
|
92
136
|
|
|
93
137
|
console.log(
|
|
94
138
|
ok
|
|
@@ -98,8 +142,14 @@ function install(argv) {
|
|
|
98
142
|
// Before the "Next:" block, so the last thing on screen stays the instruction
|
|
99
143
|
// rather than the tail of a delegated command.
|
|
100
144
|
offerRouters();
|
|
145
|
+
// How the next version arrives — an installer that never says has still
|
|
146
|
+
// chosen an update model: never.
|
|
101
147
|
console.log(`
|
|
102
|
-
${C.bold('
|
|
148
|
+
${C.bold('Updates:')} npx @ssheleg/${NAME}@latest update — every channel, and it settles
|
|
149
|
+
the plain copy that would shadow the plugin. Whole family:
|
|
150
|
+
npx --yes sshlg-skills@latest update.
|
|
151
|
+
`);
|
|
152
|
+
console.log(`${C.bold('Next:')} restart Claude Code, then run ${C.bold('/agent-sync init')} in your project.
|
|
103
153
|
It will ask where coordination state should live before writing anything.
|
|
104
154
|
`);
|
|
105
155
|
return ok ? 0 : 1;
|
|
@@ -137,13 +187,30 @@ function offerRouters() {
|
|
|
137
187
|
}
|
|
138
188
|
|
|
139
189
|
/**
|
|
190
|
+
* Decide the fate of ~/.claude/skills/<name> after a skills-CLI run.
|
|
191
|
+
*
|
|
140
192
|
* The shadow regrows on its own: `npx skills add|update --global` auto-detects
|
|
141
193
|
* Claude Code and recreates ~/.claude/skills/<name> — often as a symlink — even when
|
|
142
|
-
* claude-code was never named as a target.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
194
|
+
* claude-code was never named as a target. What that copy IS depends on the home it
|
|
195
|
+
* landed in, and until v1.18.5 this function consulted nothing and deleted it
|
|
196
|
+
* unconditionally — the family's fail-open class (make-skill distribution.md §3) in
|
|
197
|
+
* mirror image: on a home where the plugin is NOT installed (no claude CLI, or the
|
|
198
|
+
* plugin install failed), the unconditional prune destroyed the only Claude Code
|
|
199
|
+
* channel this very run had just installed, and exited 0.
|
|
200
|
+
*
|
|
201
|
+
* - Plugin installed in this home — read from installed_plugins.json, with the
|
|
202
|
+
* marketplaces/<name> dir kept only as the fallback signal: the copy is a SHADOW.
|
|
203
|
+
* It outranks the plugin and serves the version it was copied from forever.
|
|
204
|
+
* Prune it, and name the plugin spec it would have shadowed plus the channel
|
|
205
|
+
* that owns updates.
|
|
206
|
+
* - No plugin: the copy is Claude Code's only channel. Keep it, and say so.
|
|
207
|
+
* - --force: the deliberate choice to run two channels, where the stale one wins.
|
|
208
|
+
* The copy stays even beside the plugin, and the output records the choice.
|
|
209
|
+
*
|
|
210
|
+
* The gate lives INSIDE every command that touches the skills CLI, not in a human's
|
|
211
|
+
* memory. lstatSync, because a symlink shadows exactly as a dir does.
|
|
145
212
|
*/
|
|
146
|
-
function
|
|
213
|
+
function settleClaudeChannel(force) {
|
|
147
214
|
let present = false;
|
|
148
215
|
try {
|
|
149
216
|
fs.lstatSync(SHADOW);
|
|
@@ -152,11 +219,40 @@ function pruneShadow() {
|
|
|
152
219
|
/* not there */
|
|
153
220
|
}
|
|
154
221
|
if (!present) return;
|
|
222
|
+
|
|
223
|
+
const home = os.homedir();
|
|
224
|
+
const spec = installedPluginSpec(home, NAME);
|
|
225
|
+
const marketplace = path.join(home, '.claude', 'plugins', 'marketplaces', NAME);
|
|
226
|
+
const viaMarketplaceDir = !spec && fs.existsSync(marketplace);
|
|
227
|
+
|
|
228
|
+
if (!spec && !viaMarketplaceDir) {
|
|
229
|
+
console.log(C.dim(
|
|
230
|
+
` kept ${SHADOW} — no ${NAME} plugin is installed in this home,\n` +
|
|
231
|
+
` so this plain copy is Claude Code's only channel for the skill`));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (force) {
|
|
235
|
+
console.log(C.yellow(
|
|
236
|
+
` kept ${SHADOW} beside the installed plugin (--force) — two channels\n` +
|
|
237
|
+
` on one agent, and the stale plain copy is the one Claude Code reads`));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const found = spec
|
|
241
|
+
? `the Claude Code plugin ${spec} is installed (installed_plugins.json)`
|
|
242
|
+
: `a Claude Code marketplace is registered at ${marketplace}`;
|
|
155
243
|
fs.rmSync(SHADOW, { recursive: true, force: true });
|
|
156
|
-
console.log(C.dim(
|
|
244
|
+
console.log(C.dim(
|
|
245
|
+
` pruned ${SHADOW} — ${found};\n` +
|
|
246
|
+
` a plain copy there would shadow the plugin and serve a frozen version\n` +
|
|
247
|
+
` forever. The plugin channel owns updates:\n` +
|
|
248
|
+
` claude plugin marketplace update ${NAME}\n` +
|
|
249
|
+
` claude plugin update ${spec || `${NAME}@${NAME}`}\n` +
|
|
250
|
+
` Pass --force to keep the plain copy anyway — a deliberate choice to run\n` +
|
|
251
|
+
` two channels, where the stale one wins.`));
|
|
157
252
|
}
|
|
158
253
|
|
|
159
|
-
function update() {
|
|
254
|
+
function update(argv) {
|
|
255
|
+
const force = argv.includes('--force');
|
|
160
256
|
console.log(C.bold('\nUpdating every channel'));
|
|
161
257
|
let ok = true;
|
|
162
258
|
if (has('claude')) {
|
|
@@ -166,7 +262,7 @@ function update() {
|
|
|
166
262
|
ok = run('claude', ['plugin', 'update', `${NAME}@${NAME}`]) && ok;
|
|
167
263
|
}
|
|
168
264
|
ok = run('npx', ['--yes', 'skills', 'update', NAME, '--global', '--yes']) && ok;
|
|
169
|
-
|
|
265
|
+
settleClaudeChannel(force);
|
|
170
266
|
console.log(ok ? C.green('\n✓ updated') : C.red('\n✗ a channel failed — see above'));
|
|
171
267
|
console.log('\nRestart Claude Code so it picks the new version up.');
|
|
172
268
|
return ok ? 0 : 1;
|
|
@@ -178,6 +274,6 @@ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
|
178
274
|
process.exit(0);
|
|
179
275
|
}
|
|
180
276
|
if (argv[0] === 'install') process.exit(install(argv.slice(1)));
|
|
181
|
-
if (argv[0] === 'update') process.exit(update());
|
|
277
|
+
if (argv[0] === 'update') process.exit(update(argv.slice(1)));
|
|
182
278
|
usage();
|
|
183
279
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/agent-sync",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.5",
|
|
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 && python3 test/hooks_session_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 && node test/installer_test.js",
|
|
21
21
|
"prepublishOnly": "python3 test/validate.py"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"knowledge-base",
|
|
33
33
|
"task-pipeline"
|
|
34
34
|
],
|
|
35
|
-
"homepage": "https://
|
|
35
|
+
"homepage": "https://skills.sshlg.me/skills/agent-sync/",
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
38
38
|
"url": "git+https://github.com/ssheleg/agent-sync.git"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sync",
|
|
3
3
|
"displayName": "Agent Sync",
|
|
4
|
-
"version": "1.18.
|
|
4
|
+
"version": "1.18.5",
|
|
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",
|
|
@@ -38,8 +38,12 @@ except Exception:
|
|
|
38
38
|
print("0 ."); sys.exit(0)
|
|
39
39
|
cmd = (d.get("tool_input") or {}).get("command", "")
|
|
40
40
|
is_commit, repo = 0, "."
|
|
41
|
-
# Each
|
|
42
|
-
|
|
41
|
+
# Each &&/;/||/|&/| segment is its own command; a commit anywhere in the chain counts.
|
|
42
|
+
# The single pipe was CLAIMED by this comment and never consumed (ASY-05, fixed 2026-08-29):
|
|
43
|
+
# only "||" was replaced, so `echo msg | git commit -F -` stayed one segment whose first
|
|
44
|
+
# token is `echo`, and the whole pipeline skipped the guard. Order matters: "||" and "|&"
|
|
45
|
+
# must be consumed before the bare "|", or each would be split into a stray half.
|
|
46
|
+
for seg in cmd.replace("&&", "\n").replace("||", "\n").replace("|&", "\n").replace(";", "\n").replace("|", "\n").split("\n"):
|
|
43
47
|
try:
|
|
44
48
|
toks = shlex.split(seg)
|
|
45
49
|
except ValueError:
|
|
@@ -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.18.
|
|
7
|
+
version: "1.18.5"
|
|
8
8
|
author: ssheleg
|
|
9
9
|
---
|
|
10
10
|
|