@ssheleg/agent-sync 1.2.2
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 +332 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/agent-sync.example.json +40 -0
- package/agent-sync.schema.json +144 -0
- package/bin/agent-sync.js +149 -0
- package/package.json +46 -0
- package/plugins/agent-sync/.claude-plugin/plugin.json +18 -0
- package/plugins/agent-sync/commands/agent-sync.md +16 -0
- package/plugins/agent-sync/hooks/_lib.sh +39 -0
- package/plugins/agent-sync/hooks/guard.sh +55 -0
- package/plugins/agent-sync/hooks/hooks.json +69 -0
- package/plugins/agent-sync/hooks/renew.sh +10 -0
- package/plugins/agent-sync/hooks/session-end.sh +14 -0
- package/plugins/agent-sync/hooks/session-start.sh +8 -0
- package/plugins/agent-sync/skills/agent-sync/SKILL.md +372 -0
- package/plugins/agent-sync/skills/agent-sync/references/adapter-contract.md +80 -0
- package/plugins/agent-sync/skills/agent-sync/references/backend-fs.md +57 -0
- package/plugins/agent-sync/skills/agent-sync/references/backend-outline.md +103 -0
- package/plugins/agent-sync/skills/agent-sync/references/hooks.md +99 -0
- package/plugins/agent-sync/skills/agent-sync/references/lease-protocol.md +145 -0
- package/plugins/agent-sync/skills/agent-sync/references/pipeline-binding.md +76 -0
- package/plugins/agent-sync/skills/agent-sync/references/roadmap.md +103 -0
- package/plugins/agent-sync/skills/agent-sync/references/two-sources.md +131 -0
- package/plugins/agent-sync/skills/agent-sync/scripts/agent_sync.py +2454 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* agent-sync installer. Zero dependencies.
|
|
6
|
+
*
|
|
7
|
+
* One channel per agent: Claude Code gets the plugin, every other agent gets the
|
|
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 pruned afterwards — that
|
|
10
|
+
* duplicate shadows the plugin and silently serves a stale skill.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const REPO = 'appvillis-com/agent-sync';
|
|
19
|
+
const NAME = 'agent-sync';
|
|
20
|
+
const SHADOW = path.join(os.homedir(), '.claude', 'skills', NAME);
|
|
21
|
+
|
|
22
|
+
const C = {
|
|
23
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
24
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
25
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
26
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
27
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function run(cmd, args) {
|
|
31
|
+
process.stdout.write(C.dim(` $ ${cmd} ${args.join(' ')}\n`));
|
|
32
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
33
|
+
return r.status === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function has(cmd) {
|
|
37
|
+
return spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], {
|
|
38
|
+
stdio: 'ignore',
|
|
39
|
+
}).status === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function usage() {
|
|
43
|
+
console.log(`
|
|
44
|
+
${C.bold('agent-sync')} — coordination for concurrent agents
|
|
45
|
+
|
|
46
|
+
npx @ssheleg/${NAME} install install for Claude Code and other agents
|
|
47
|
+
npx @ssheleg/${NAME} install --claude-only Claude Code plugin only
|
|
48
|
+
npx @ssheleg/${NAME} install --agent a,b pick agents for the skills CLI
|
|
49
|
+
npx @ssheleg/${NAME} update update every channel, then prune the shadow
|
|
50
|
+
npx @ssheleg/${NAME} --help
|
|
51
|
+
|
|
52
|
+
After installing, initialise the project — this is the step that asks where
|
|
53
|
+
coordination state should live:
|
|
54
|
+
|
|
55
|
+
${C.bold('/agent-sync init')}
|
|
56
|
+
|
|
57
|
+
It writes .claude/agent-sync.json (committed) and .env.agent-sync (gitignored,
|
|
58
|
+
mode 600), then tells you the one thing only you can do: create an API token in
|
|
59
|
+
your own knowledge-base instance and paste it into that file. The installer never
|
|
60
|
+
asks for a token and never stores one.
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function install(argv) {
|
|
65
|
+
const claudeOnly = argv.includes('--claude-only');
|
|
66
|
+
const noClaude = argv.includes('--no-claude');
|
|
67
|
+
const agentIdx = argv.indexOf('--agent');
|
|
68
|
+
const agents = agentIdx !== -1 && argv[agentIdx + 1] ? argv[agentIdx + 1].split(',') : null;
|
|
69
|
+
|
|
70
|
+
let ok = true;
|
|
71
|
+
|
|
72
|
+
if (!noClaude) {
|
|
73
|
+
console.log(C.bold('\nClaude Code — as a plugin'));
|
|
74
|
+
if (!has('claude')) {
|
|
75
|
+
console.log(C.yellow(' claude CLI not found; skipping the plugin channel'));
|
|
76
|
+
} else {
|
|
77
|
+
// The full <name>@<name> form is required; `claude plugin install agent-sync`
|
|
78
|
+
// answers "Plugin not found".
|
|
79
|
+
ok = run('claude', ['plugin', 'marketplace', 'add', REPO]) && ok;
|
|
80
|
+
ok = run('claude', ['plugin', 'install', `${NAME}@${NAME}`]) && ok;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!claudeOnly) {
|
|
85
|
+
console.log(C.bold('\nOther agents — via the skills CLI'));
|
|
86
|
+
const args = ['--yes', 'skills', 'add', REPO, '--global', '--yes'];
|
|
87
|
+
for (const a of agents || []) args.push('--agent', a);
|
|
88
|
+
ok = run('npx', args) && ok;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pruneShadow();
|
|
92
|
+
|
|
93
|
+
console.log(
|
|
94
|
+
ok
|
|
95
|
+
? C.green('\n✓ installed')
|
|
96
|
+
: C.red('\n✗ at least one channel failed — see the output above')
|
|
97
|
+
);
|
|
98
|
+
console.log(`
|
|
99
|
+
${C.bold('Next:')} restart Claude Code, then run ${C.bold('/agent-sync init')} in your project.
|
|
100
|
+
It will ask where coordination state should live before writing anything.
|
|
101
|
+
`);
|
|
102
|
+
return ok ? 0 : 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The shadow regrows on its own: `npx skills add|update --global` auto-detects
|
|
107
|
+
* Claude Code and recreates ~/.claude/skills/<name> — often as a symlink — even when
|
|
108
|
+
* claude-code was never named as a target. That copy shadows the plugin and serves a
|
|
109
|
+
* stale skill, so the prune belongs INSIDE every command that touches the skills CLI,
|
|
110
|
+
* not in a human's memory. lstatSync, because a symlink shadows exactly as a dir does.
|
|
111
|
+
*/
|
|
112
|
+
function pruneShadow() {
|
|
113
|
+
let present = false;
|
|
114
|
+
try {
|
|
115
|
+
fs.lstatSync(SHADOW);
|
|
116
|
+
present = true;
|
|
117
|
+
} catch {
|
|
118
|
+
/* not there */
|
|
119
|
+
}
|
|
120
|
+
if (!present) return;
|
|
121
|
+
fs.rmSync(SHADOW, { recursive: true, force: true });
|
|
122
|
+
console.log(C.dim(` pruned duplicate ${SHADOW}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function update() {
|
|
126
|
+
console.log(C.bold('\nUpdating every channel'));
|
|
127
|
+
let ok = true;
|
|
128
|
+
if (has('claude')) {
|
|
129
|
+
ok = run('claude', ['plugin', 'marketplace', 'update', NAME]) && ok;
|
|
130
|
+
// The full <name>@<name> id is required; `claude plugin update <name>` answers
|
|
131
|
+
// "Plugin not found".
|
|
132
|
+
ok = run('claude', ['plugin', 'update', `${NAME}@${NAME}`]) && ok;
|
|
133
|
+
}
|
|
134
|
+
ok = run('npx', ['--yes', 'skills', 'update', NAME, '--global', '--yes']) && ok;
|
|
135
|
+
pruneShadow();
|
|
136
|
+
console.log(ok ? C.green('\n✓ updated') : C.red('\n✗ a channel failed — see above'));
|
|
137
|
+
console.log('\nRestart Claude Code so it picks the new version up.');
|
|
138
|
+
return ok ? 0 : 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const argv = process.argv.slice(2);
|
|
142
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
143
|
+
usage();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
if (argv[0] === 'install') process.exit(install(argv.slice(1)));
|
|
147
|
+
if (argv[0] === 'update') process.exit(update());
|
|
148
|
+
usage();
|
|
149
|
+
process.exit(1);
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ssheleg/agent-sync",
|
|
3
|
+
"version": "1.2.2",
|
|
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
|
+
"bin": {
|
|
6
|
+
"agent-sync": "bin/agent-sync.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"plugins/",
|
|
11
|
+
"agent-sync.schema.json",
|
|
12
|
+
"agent-sync.example.json",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "python3 test/validate.py && python3 test/validate.py --self-test",
|
|
19
|
+
"prepublishOnly": "python3 test/validate.py"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"claude-code",
|
|
26
|
+
"agent-skills",
|
|
27
|
+
"multi-agent",
|
|
28
|
+
"coordination",
|
|
29
|
+
"lease",
|
|
30
|
+
"knowledge-base",
|
|
31
|
+
"task-pipeline"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://github.com/appvillis-com/agent-sync",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/appvillis-com/agent-sync.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/appvillis-com/agent-sync/issues"
|
|
40
|
+
},
|
|
41
|
+
"author": "appvillis-com",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-sync",
|
|
3
|
+
"version": "1.2.2",
|
|
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
|
+
"author": {
|
|
6
|
+
"name": "appvillis-com"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/appvillis-com/agent-sync",
|
|
9
|
+
"repository": "https://github.com/appvillis-com/agent-sync",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"multi-agent",
|
|
13
|
+
"coordination",
|
|
14
|
+
"lease",
|
|
15
|
+
"knowledge-base",
|
|
16
|
+
"task-pipeline"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Invoke the `agent-sync` skill.
|
|
7
|
+
|
|
8
|
+
Arguments: $ARGUMENTS
|
|
9
|
+
|
|
10
|
+
If no arguments were given, run the skill's default entry: check whether the
|
|
11
|
+
project is initialised, and if it is not, **ask the operator where coordination
|
|
12
|
+
state should live before doing anything else** — a knowledge cloud (and then its
|
|
13
|
+
instance URL) or local files. Never guess that answer.
|
|
14
|
+
|
|
15
|
+
If the project is already initialised, report status and name exactly one next
|
|
16
|
+
action.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared helpers for the agent-sync hooks. Sourced, never executed.
|
|
3
|
+
|
|
4
|
+
# Run a command under a time limit, portably.
|
|
5
|
+
#
|
|
6
|
+
# `timeout` is GNU coreutils and is NOT on a stock macOS. Calling it directly made
|
|
7
|
+
# three of the four hooks die with "timeout: command not found" on every macOS
|
|
8
|
+
# session — which meant leases were never renewed and never released there, the exact
|
|
9
|
+
# abandoned-lease failure this tool exists to prevent. Homebrew installs it as
|
|
10
|
+
# `gtimeout`, and neither is guaranteed, so fall back to a plain-bash watchdog.
|
|
11
|
+
run_limited() {
|
|
12
|
+
local secs="$1"; shift
|
|
13
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
14
|
+
timeout "$secs" "$@"
|
|
15
|
+
return $?
|
|
16
|
+
fi
|
|
17
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
18
|
+
gtimeout "$secs" "$@"
|
|
19
|
+
return $?
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
"$@" &
|
|
23
|
+
local pid=$!
|
|
24
|
+
( sleep "$secs"; kill -TERM "$pid" 2>/dev/null ) &
|
|
25
|
+
local watchdog=$!
|
|
26
|
+
wait "$pid" 2>/dev/null
|
|
27
|
+
local rc=$?
|
|
28
|
+
kill "$watchdog" 2>/dev/null
|
|
29
|
+
wait "$watchdog" 2>/dev/null
|
|
30
|
+
return "$rc"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Every hook is a no-op in a project that does not use agent-sync, so installing the
|
|
34
|
+
# plugin globally changes nothing elsewhere.
|
|
35
|
+
agent_sync_configured() {
|
|
36
|
+
[ -f "${CLAUDE_PROJECT_DIR:-$PWD}/.claude/agent-sync.json" ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
AGENT_SYNC_PY="${CLAUDE_PLUGIN_ROOT:-}/skills/agent-sync/scripts/agent_sync.py"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse guard. Exit 2 blocks the call and shows stderr as the reason.
|
|
3
|
+
# Any other non-zero code is NON-blocking in Claude Code, so every internal
|
|
4
|
+
# failure must also exit 2 — a crashing guard that fails open guards nothing.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="${CLAUDE_PLUGIN_ROOT}/skills/agent-sync/scripts/agent_sync.py"
|
|
7
|
+
input=$(cat)
|
|
8
|
+
|
|
9
|
+
path=$(python3 -c '
|
|
10
|
+
import json,sys
|
|
11
|
+
try:
|
|
12
|
+
d=json.load(sys.stdin)
|
|
13
|
+
except Exception:
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
ti=d.get("tool_input") or {}
|
|
16
|
+
print(ti.get("file_path") or ti.get("path") or ti.get("notebook_path") or "")
|
|
17
|
+
' <<<"$input" 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
# git commit: check every staged path instead of a single file argument.
|
|
20
|
+
if [ -z "$path" ]; then
|
|
21
|
+
cmd=$(python3 -c '
|
|
22
|
+
import json,sys
|
|
23
|
+
try:
|
|
24
|
+
d=json.load(sys.stdin)
|
|
25
|
+
except Exception:
|
|
26
|
+
sys.exit(0)
|
|
27
|
+
print((d.get("tool_input") or {}).get("command",""))
|
|
28
|
+
' <<<"$input" 2>/dev/null)
|
|
29
|
+
case "$cmd" in
|
|
30
|
+
*"git commit"*)
|
|
31
|
+
while IFS= read -r staged; do
|
|
32
|
+
[ -n "$staged" ] || continue
|
|
33
|
+
if ! python3 "$S" guard "$staged" >/dev/null 2>&1; then
|
|
34
|
+
echo "agent-sync: '$staged' is staged but this run holds no lease on it. Acquire one, or unstage it." >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
done < <(git diff --cached --name-only 2>/dev/null)
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
[ -f "${CLAUDE_PROJECT_DIR:-$PWD}/.claude/agent-sync.json" ] || exit 0
|
|
44
|
+
|
|
45
|
+
if out=$(python3 "$S" guard "$path" 2>&1); then
|
|
46
|
+
exit 0
|
|
47
|
+
else
|
|
48
|
+
code=$?
|
|
49
|
+
if [ "$code" -eq 2 ]; then
|
|
50
|
+
echo "$out" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
echo "agent-sync guard failed to run ($code): $out" >&2
|
|
54
|
+
exit 2
|
|
55
|
+
fi
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "agent-sync — lease enforcement and run lifecycle. Every hook exits 0 immediately when the project has no .claude/agent-sync.json, so installing the plugin globally changes nothing in projects that do not use it.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup|resume",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"shell": "bash",
|
|
11
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\"",
|
|
12
|
+
"timeout": 20
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"PreToolUse": [
|
|
18
|
+
{
|
|
19
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
|
20
|
+
"hooks": [
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"shell": "bash",
|
|
24
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/guard.sh\"",
|
|
25
|
+
"timeout": 20
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"if": "Bash(git commit *)",
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"shell": "bash",
|
|
36
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/guard.sh\"",
|
|
37
|
+
"timeout": 20
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"PostToolUse": [
|
|
43
|
+
{
|
|
44
|
+
"matcher": "*",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"shell": "bash",
|
|
49
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/renew.sh\"",
|
|
50
|
+
"timeout": 15,
|
|
51
|
+
"async": true
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"SessionEnd": [
|
|
57
|
+
{
|
|
58
|
+
"hooks": [
|
|
59
|
+
{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"shell": "bash",
|
|
62
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh\"",
|
|
63
|
+
"timeout": 20
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Runs after every tool call, so it must be a no-op in the common case:
|
|
3
|
+
# the script itself throttles on a timestamp file and touches the network at
|
|
4
|
+
# most once per renewIntervalSeconds.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
. "${CLAUDE_PLUGIN_ROOT}/hooks/_lib.sh"
|
|
7
|
+
S="$AGENT_SYNC_PY"
|
|
8
|
+
agent_sync_configured || exit 0
|
|
9
|
+
run_limited 10 python3 "$S" renew >/dev/null 2>&1 || true
|
|
10
|
+
exit 0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Release every lease this run holds. An abandoned lease looks like active work
|
|
3
|
+
# until its TTL expires.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
. "${CLAUDE_PLUGIN_ROOT}/hooks/_lib.sh"
|
|
6
|
+
S="$AGENT_SYNC_PY"
|
|
7
|
+
agent_sync_configured || exit 0
|
|
8
|
+
held=$(run_limited 10 python3 "$S" whoami 2>/dev/null | sed -n 's/^holds: //p')
|
|
9
|
+
[ -z "$held" ] || [ "$held" = "nothing" ] && exit 0
|
|
10
|
+
IFS=', ' read -r -a keys <<<"$held"
|
|
11
|
+
for k in "${keys[@]}"; do
|
|
12
|
+
[ -n "$k" ] && run_limited 10 python3 "$S" release "$k" >/dev/null 2>&1 || true
|
|
13
|
+
done
|
|
14
|
+
exit 0
|