muse-crew 0.4.5 → 0.4.7
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/lib/AGENTS.md +5 -4
- package/lib/merge-lock.sh +149 -68
- package/lib/orphan-sweep.sh +38 -34
- package/lib/publish-npm.sh +141 -105
- package/lib/test-merge-lock.sh +77 -34
- package/lib/test-orphan-sweep.sh +19 -5
- package/lib/test-publish-skip.sh +192 -0
- package/lib/worktree-lifecycle.sh +46 -8
- package/package.json +1 -1
- package/workflows/bugfix.js +144 -36
- package/workflows/chore.js +141 -35
- package/workflows/standard.js +144 -36
package/lib/AGENTS.md
CHANGED
|
@@ -4,12 +4,13 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
4
4
|
|
|
5
5
|
- `build-registry.js` — deterministic extractor that generates `workflows/registry.json` (workflow step registry) from the workflow files' `meta` blocks at release time; invoked by `crew-release.sh` deploy
|
|
6
6
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
7
|
-
- `merge-lock.sh` — serialized merge lock for concurrent agents;
|
|
8
|
-
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving.
|
|
7
|
+
- `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
|
|
8
|
+
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/status/post-deploy/refresh-lock/lock-status over git worktrees (`.worktrees/<id>`, branch `task/<id>`). The crew registry (`<repo>/.worktrees/.registry/<id>`) is the source of truth for task→branch/path — never reconstruct it from git state. Prepare fails closed on dirty `main`; cleanup is forgiving. `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or `LOCKED by <holder> since <ts> (pid <pid>)`, always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure.
|
|
9
9
|
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight) on a scratch repo
|
|
10
|
-
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the
|
|
10
|
+
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \\u2014 escape; extracts the shipped block by anchor)
|
|
11
11
|
- `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
|
|
12
|
-
- `
|
|
12
|
+
- `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
|
|
13
|
+
- `orphan-sweep.sh` — find and clean stale worktrees and merge locks: parses `merge-lock.sh status` key=value fields (never a PID); a lock whose task has a running session is untouched regardless of lease age; a lock with no running session is broken only when its lease is expired (via merge-lock.sh release so the break lands in the audit log)
|
|
13
14
|
- `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
14
15
|
- `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
|
|
15
16
|
- `test-orphan-sweep.sh` — regression tests for orphan-sweep.sh (active-run guard, verified removal, fail-closed)
|
package/lib/merge-lock.sh
CHANGED
|
@@ -1,16 +1,89 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# merge-lock.sh — serialized merge lock for the crew's repos.
|
|
3
|
+
#
|
|
4
|
+
# Time-based holder lease (bug 2fc8f52f): the old PID-liveness staleness
|
|
5
|
+
# check never excluded anyone. The recorded PID was the short-lived shell
|
|
6
|
+
# that ran the acquire, so for essentially the whole Integrate→Publish→
|
|
7
|
+
# post-deploy hold window the holder's PID was dead, every concurrent
|
|
8
|
+
# acquire took the STALE path, and two Publish phases both believed they
|
|
9
|
+
# owned version assignment. PID is the wrong liveness signal (orphan-sweep
|
|
10
|
+
# already treats it as such) — the lease is time-based and refreshed by
|
|
11
|
+
# the workflow while the hold is live.
|
|
12
|
+
#
|
|
13
|
+
# The lock file ($CREW_REPO/.worktrees/.merge-lock) is key=value:
|
|
14
|
+
# task_id=<task id that holds the lock>
|
|
15
|
+
# holder=<opaque task+run identity, never a PID>
|
|
16
|
+
# acquired_at=<epoch seconds; refresh rewrites it to extend the lease>
|
|
17
|
+
# lease_seconds=<lease window; MERGE_LOCK_LEASE_SECONDS, default 600>
|
|
18
|
+
#
|
|
19
|
+
# The lock is HELD while now - acquired_at < lease_seconds. An expired
|
|
20
|
+
# (or malformed) lease is broken with logging and re-acquired. Every op
|
|
21
|
+
# appends one line to $CREW_HOME/.merge-lock.log (the audit trail).
|
|
22
|
+
#
|
|
3
23
|
# Usage:
|
|
4
|
-
# merge-lock.sh acquire <task_id>
|
|
5
|
-
# merge-lock.sh
|
|
6
|
-
# merge-lock.sh
|
|
7
|
-
# merge-lock.sh status
|
|
8
|
-
# merge-lock.sh force-release
|
|
24
|
+
# merge-lock.sh acquire <task_id> <holder> — acquire the lock (0 ok, 1 held)
|
|
25
|
+
# merge-lock.sh refresh <task_id> — extend the lease (holder task only)
|
|
26
|
+
# merge-lock.sh release <task_id> — release the lock (holder task only)
|
|
27
|
+
# merge-lock.sh status — key=value holder/age/remaining, or UNLOCKED
|
|
28
|
+
# merge-lock.sh force-release — break the lock (admin use only)
|
|
9
29
|
|
|
10
30
|
set -euo pipefail
|
|
11
31
|
|
|
12
32
|
LOCK_DIR="${CREW_REPO:-$HOME/workspace/ts-spaces/orchestra-dashboard}/.worktrees"
|
|
13
33
|
LOCK_FILE="$LOCK_DIR/.merge-lock"
|
|
34
|
+
LOG_FILE="${CREW_HOME:-$HOME/workspace/.jarvis}/.merge-lock.log"
|
|
35
|
+
LEASE_SECONDS="${MERGE_LOCK_LEASE_SECONDS:-600}"
|
|
36
|
+
|
|
37
|
+
log_op() { # log_op <op> <detail> — audit trail; logging never breaks a lock op
|
|
38
|
+
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
|
39
|
+
printf '%s op=%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" "$2" >> "$LOG_FILE" 2>/dev/null || true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# read_lock — parse the lock file into lock_task_id / lock_holder /
|
|
43
|
+
# lock_acquired_at / lock_lease_seconds. Returns 1 when there is no lock.
|
|
44
|
+
read_lock() {
|
|
45
|
+
lock_task_id=""; lock_holder=""; lock_acquired_at=""; lock_lease_seconds=""
|
|
46
|
+
[ -f "$LOCK_FILE" ] || return 1
|
|
47
|
+
local key val
|
|
48
|
+
while IFS='=' read -r key val; do
|
|
49
|
+
case "$key" in
|
|
50
|
+
task_id) lock_task_id="$val" ;;
|
|
51
|
+
holder) lock_holder="$val" ;;
|
|
52
|
+
acquired_at) lock_acquired_at="$val" ;;
|
|
53
|
+
lease_seconds) lock_lease_seconds="$val" ;;
|
|
54
|
+
esac
|
|
55
|
+
done < "$LOCK_FILE"
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
lock_age() { # seconds since acquired_at, or "unknown" when unparsable
|
|
60
|
+
if [[ "${lock_acquired_at:-}" =~ ^[0-9]+$ ]]; then
|
|
61
|
+
echo $(( $(date +%s) - lock_acquired_at ))
|
|
62
|
+
else
|
|
63
|
+
echo "unknown"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lock_remaining() { # lease seconds remaining (<=0 means expired), or "unknown"
|
|
68
|
+
local age="${1:-$(lock_age)}" lease="$lock_lease_seconds"
|
|
69
|
+
[[ "$lease" =~ ^[0-9]+$ ]] || lease="$LEASE_SECONDS"
|
|
70
|
+
if [ "$age" = "unknown" ]; then
|
|
71
|
+
echo "unknown"
|
|
72
|
+
else
|
|
73
|
+
echo $(( lease - age ))
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# write_lock <task_id> <holder> — atomic create via noclobber; 0 on success.
|
|
78
|
+
# A concurrent writer wins exactly one create; the loser retries the read.
|
|
79
|
+
write_lock() {
|
|
80
|
+
local task_id="$1" holder="$2"
|
|
81
|
+
mkdir -p "$LOCK_DIR"
|
|
82
|
+
( set -C
|
|
83
|
+
printf 'task_id=%s\nholder=%s\nacquired_at=%s\nlease_seconds=%s\n' \
|
|
84
|
+
"$task_id" "$holder" "$(date +%s)" "$LEASE_SECONDS" > "$LOCK_FILE"
|
|
85
|
+
) 2>/dev/null
|
|
86
|
+
}
|
|
14
87
|
|
|
15
88
|
cmd="${1:-}"
|
|
16
89
|
task_id="${2:-}"
|
|
@@ -18,90 +91,98 @@ task_id="${2:-}"
|
|
|
18
91
|
case "$cmd" in
|
|
19
92
|
acquire)
|
|
20
93
|
[ -z "$task_id" ] && { echo "ERROR: task_id required"; exit 1; }
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
echo "ACQUIRED by $task_id"
|
|
94
|
+
holder="${3:-$task_id}"
|
|
95
|
+
if write_lock "$task_id" "$holder"; then
|
|
96
|
+
log_op acquire "task_id=$task_id holder=$holder result=ACQUIRED"
|
|
97
|
+
echo "ACQUIRED by $task_id (holder $holder)"
|
|
25
98
|
exit 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# cleanup mechanical: a stopped/failed workflow's lock is reclaimed on
|
|
35
|
-
# the next acquire, not via manual release or a background sweeper.
|
|
36
|
-
if [ -z "$lock_pid" ] || [ "$lock_pid" = "-" ] || ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
37
|
-
echo "STALE: lock held by $holder since $acquired_at (pid $lock_pid dead) — breaking" >&2
|
|
38
|
-
rm -f "$LOCK_FILE"
|
|
39
|
-
if (set -C; echo "$task_id $(date -u +%Y-%m-%dT%H:%M:%SZ) $owner_pid" > "$LOCK_FILE") 2>/dev/null; then
|
|
40
|
-
echo "ACQUIRED by $task_id (reclaimed stale lock from $holder)"
|
|
41
|
-
exit 0
|
|
42
|
-
fi
|
|
43
|
-
# Lost the race: another task acquired it after we broke the stale lock.
|
|
44
|
-
holder=$(cut -d' ' -f1 "$LOCK_FILE" 2>/dev/null || echo "unknown")
|
|
45
|
-
acquired_at=$(cut -d' ' -f2 "$LOCK_FILE" 2>/dev/null || echo "unknown")
|
|
46
|
-
lock_pid=$(awk '{print $3}' "$LOCK_FILE" 2>/dev/null || echo "-")
|
|
99
|
+
fi
|
|
100
|
+
# A lock file exists (or raced into existence) — read it.
|
|
101
|
+
if ! read_lock; then
|
|
102
|
+
# Raced with a release between create and read: retry the create once.
|
|
103
|
+
if write_lock "$task_id" "$holder"; then
|
|
104
|
+
log_op acquire "task_id=$task_id holder=$holder result=ACQUIRED"
|
|
105
|
+
echo "ACQUIRED by $task_id (holder $holder)"
|
|
106
|
+
exit 0
|
|
47
107
|
fi
|
|
48
|
-
echo "
|
|
108
|
+
read_lock || { echo "ERROR: lock state unreadable"; log_op acquire "task_id=$task_id holder=$holder result=ERROR"; exit 1; }
|
|
109
|
+
fi
|
|
110
|
+
remaining="$(lock_remaining)"
|
|
111
|
+
if [ "$remaining" != "unknown" ] && [ "$remaining" -gt 0 ]; then
|
|
112
|
+
log_op acquire "task_id=$task_id holder=$holder result=HELD holder_task=$lock_task_id holder_id=$lock_holder remaining=${remaining}s"
|
|
113
|
+
echo "HELD by $lock_task_id (holder $lock_holder, ${remaining}s of lease remaining)"
|
|
49
114
|
exit 1
|
|
50
115
|
fi
|
|
116
|
+
# Expired or malformed lease — break it (logged) and re-acquire.
|
|
117
|
+
echo "STALE: lease for ${lock_task_id:-unknown} (holder ${lock_holder:-unknown}) expired — breaking" >&2
|
|
118
|
+
log_op acquire "task_id=$task_id holder=$holder result=STALE-BREAK previous_task=${lock_task_id:-unknown} previous_holder=${lock_holder:-unknown}"
|
|
119
|
+
rm -f "$LOCK_FILE"
|
|
120
|
+
if write_lock "$task_id" "$holder"; then
|
|
121
|
+
log_op acquire "task_id=$task_id holder=$holder result=ACQUIRED-RECLAIMED previous_task=${lock_task_id:-unknown}"
|
|
122
|
+
echo "ACQUIRED by $task_id (holder $holder; reclaimed expired lease from ${lock_task_id:-unknown})"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
# Lost the race: whoever won the create holds it now.
|
|
126
|
+
read_lock 2>/dev/null || true
|
|
127
|
+
log_op acquire "task_id=$task_id holder=$holder result=HELD-RACE holder_task=${lock_task_id:-unknown}"
|
|
128
|
+
echo "HELD by ${lock_task_id:-unknown} (holder ${lock_holder:-unknown}) — lost reclaim race"
|
|
129
|
+
exit 1
|
|
130
|
+
;;
|
|
131
|
+
refresh)
|
|
132
|
+
[ -z "$task_id" ] && { echo "ERROR: task_id required"; exit 1; }
|
|
133
|
+
if ! read_lock; then
|
|
134
|
+
log_op refresh "task_id=$task_id result=NO-LOCK"
|
|
135
|
+
echo "ERROR: no lock to refresh"
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
if [ "$lock_task_id" != "$task_id" ]; then
|
|
139
|
+
log_op refresh "task_id=$task_id result=NOT-HOLDER holder_task=$lock_task_id"
|
|
140
|
+
echo "ERROR: lock held by $lock_task_id, not $task_id"
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
# Extend the lease: acquired_at=now, holder and lease window unchanged.
|
|
144
|
+
printf 'task_id=%s\nholder=%s\nacquired_at=%s\nlease_seconds=%s\n' \
|
|
145
|
+
"$lock_task_id" "$lock_holder" "$(date +%s)" "${lock_lease_seconds:-$LEASE_SECONDS}" > "$LOCK_FILE"
|
|
146
|
+
log_op refresh "task_id=$task_id holder=$lock_holder result=REFRESHED"
|
|
147
|
+
echo "REFRESHED by $task_id (holder $lock_holder)"
|
|
148
|
+
exit 0
|
|
51
149
|
;;
|
|
52
150
|
release)
|
|
53
151
|
[ -z "$task_id" ] && { echo "ERROR: task_id required"; exit 1; }
|
|
54
|
-
if
|
|
55
|
-
|
|
56
|
-
if [ "$holder" = "$task_id" ]; then
|
|
57
|
-
rm -f "$LOCK_FILE"
|
|
58
|
-
echo "RELEASED by $task_id"
|
|
59
|
-
exit 0
|
|
60
|
-
else
|
|
61
|
-
echo "ERROR: lock held by $holder, not $task_id"
|
|
62
|
-
exit 1
|
|
63
|
-
fi
|
|
64
|
-
else
|
|
152
|
+
if ! read_lock; then
|
|
153
|
+
log_op release "task_id=$task_id result=NOT-LOCKED"
|
|
65
154
|
echo "RELEASED (was not locked)"
|
|
66
155
|
exit 0
|
|
67
156
|
fi
|
|
157
|
+
if [ "$lock_task_id" = "$task_id" ]; then
|
|
158
|
+
rm -f "$LOCK_FILE"
|
|
159
|
+
log_op release "task_id=$task_id holder=$lock_holder result=RELEASED"
|
|
160
|
+
echo "RELEASED by $task_id"
|
|
161
|
+
exit 0
|
|
162
|
+
fi
|
|
163
|
+
log_op release "task_id=$task_id result=NOT-HOLDER holder_task=$lock_task_id"
|
|
164
|
+
echo "ERROR: lock held by $lock_task_id, not $task_id"
|
|
165
|
+
exit 1
|
|
68
166
|
;;
|
|
69
167
|
status)
|
|
70
|
-
if
|
|
71
|
-
holder=$(cut -d' ' -f1 "$LOCK_FILE" 2>/dev/null || echo "unknown")
|
|
72
|
-
acquired_at=$(cut -d' ' -f2 "$LOCK_FILE" 2>/dev/null || echo "unknown")
|
|
73
|
-
lock_pid=$(awk '{print $3}' "$LOCK_FILE" 2>/dev/null || echo "-")
|
|
74
|
-
echo "LOCKED by $holder since $acquired_at (pid $lock_pid)"
|
|
75
|
-
else
|
|
168
|
+
if ! read_lock; then
|
|
76
169
|
echo "UNLOCKED"
|
|
170
|
+
exit 0
|
|
77
171
|
fi
|
|
172
|
+
printf 'locked=true\ntask_id=%s\nholder=%s\nacquired_at=%s\nlease_seconds=%s\nage_seconds=%s\nremaining_seconds=%s\n' \
|
|
173
|
+
"$lock_task_id" "$lock_holder" "$lock_acquired_at" "${lock_lease_seconds:-$LEASE_SECONDS}" \
|
|
174
|
+
"$(lock_age)" "$(lock_remaining)"
|
|
78
175
|
exit 0
|
|
79
176
|
;;
|
|
80
177
|
force-release)
|
|
178
|
+
read_lock 2>/dev/null || true
|
|
179
|
+
log_op force-release "result=FORCE-RELEASED previous_task=${lock_task_id:-unknown}"
|
|
81
180
|
rm -f "$LOCK_FILE"
|
|
82
181
|
echo "FORCE RELEASED"
|
|
83
182
|
exit 0
|
|
84
183
|
;;
|
|
85
|
-
refresh)
|
|
86
|
-
[ -z "$task_id" ] && { echo "ERROR: task_id required"; exit 1; }
|
|
87
|
-
new_pid="${3:-$PPID}"
|
|
88
|
-
if [ -f "$LOCK_FILE" ]; then
|
|
89
|
-
holder=$(cut -d' ' -f1 "$LOCK_FILE" 2>/dev/null || echo "")
|
|
90
|
-
if [ "$holder" = "$task_id" ]; then
|
|
91
|
-
echo "$task_id $(date -u +%Y-%m-%dT%H:%M:%SZ) $new_pid" > "$LOCK_FILE"
|
|
92
|
-
echo "REFRESHED by $task_id"
|
|
93
|
-
exit 0
|
|
94
|
-
else
|
|
95
|
-
echo "ERROR: lock held by $holder, not $task_id"
|
|
96
|
-
exit 1
|
|
97
|
-
fi
|
|
98
|
-
else
|
|
99
|
-
echo "ERROR: no lock to refresh"
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
;;
|
|
103
184
|
*)
|
|
104
|
-
echo "Usage: merge-lock.sh {acquire|release|refresh|status|force-release}
|
|
185
|
+
echo "Usage: merge-lock.sh {acquire|release|refresh|status|force-release} <task_id> [holder]"
|
|
105
186
|
exit 1
|
|
106
187
|
;;
|
|
107
188
|
esac
|
package/lib/orphan-sweep.sh
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
6
|
# orphan-sweep.sh report — list orphans (read-only for worktrees);
|
|
7
|
-
#
|
|
7
|
+
# expired-lease locks are released
|
|
8
8
|
# orphan-sweep.sh clean — remove safe-to-clean orphans (merged branches only),
|
|
9
|
-
# release
|
|
9
|
+
# release expired-lease merge locks, and remove pins whose
|
|
10
10
|
# task has no running session
|
|
11
11
|
#
|
|
12
12
|
# Active-run knowledge is injected by the caller, never fetched here:
|
|
@@ -44,7 +44,7 @@ set -euo pipefail
|
|
|
44
44
|
REPO="${CREW_REPO:-$HOME/workspace/ts-spaces/orchestra-dashboard}"
|
|
45
45
|
WORKTREE_DIR="$REPO/.worktrees"
|
|
46
46
|
LOCK_FILE="$WORKTREE_DIR/.merge-lock"
|
|
47
|
-
|
|
47
|
+
MERGE_LOCK_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/merge-lock.sh"
|
|
48
48
|
|
|
49
49
|
cmd="${1:-report}"
|
|
50
50
|
found=0
|
|
@@ -100,40 +100,44 @@ if [ "$cmd" != "clean" ] && [ "$have_active_data" -eq 0 ]; then
|
|
|
100
100
|
echo "NOTE: no active-run data provided; ACTIVE checks skipped"
|
|
101
101
|
fi
|
|
102
102
|
|
|
103
|
-
# --- Stale merge lock ---
|
|
103
|
+
# --- Stale merge lock (lease-based; bug 2fc8f52f) ---
|
|
104
|
+
# Owner identity comes from merge-lock.sh status key=value fields — never
|
|
105
|
+
# from a PID. Fail-closed: a lock whose task has a running session is
|
|
106
|
+
# untouched regardless of lease age; a lock whose task has no running
|
|
107
|
+
# session is broken only when its lease is expired. The break goes through
|
|
108
|
+
# merge-lock.sh release (or force-release for an unparseable file with no
|
|
109
|
+
# identifiable holder) so it lands in the merge-lock audit log.
|
|
104
110
|
if [ -f "$LOCK_FILE" ]; then
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
found=1
|
|
111
|
+
lock_status="$("$MERGE_LOCK_SCRIPT" status 2>/dev/null || echo UNLOCKED)"
|
|
112
|
+
if [ "$lock_status" = "UNLOCKED" ]; then
|
|
113
|
+
: # lock file vanished between the check and the read — nothing held
|
|
109
114
|
else
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
echo "STALE_LOCK: held ${age_min}m by $lock_holder (pid ${lock_pid:-none} dead) — threshold ${STALE_LOCK_MIN}m"
|
|
127
|
-
found=1
|
|
128
|
-
rm -f "$LOCK_FILE"
|
|
129
|
-
if [ ! -f "$LOCK_FILE" ]; then
|
|
130
|
-
echo " → released (dead PID)"
|
|
131
|
-
else
|
|
132
|
-
echo " → FAILED: lock file still present after rm"
|
|
133
|
-
any_failed=1
|
|
134
|
-
fi
|
|
135
|
-
fi
|
|
115
|
+
lock_holder=$(printf '%s\n' "$lock_status" | sed -n 's/^task_id=//p' | head -1)
|
|
116
|
+
lock_remaining=$(printf '%s\n' "$lock_status" | sed -n 's/^remaining_seconds=//p' | head -1)
|
|
117
|
+
lock_age=$(printf '%s\n' "$lock_status" | sed -n 's/^age_seconds=//p' | head -1)
|
|
118
|
+
if task_is_active "$lock_holder"; then
|
|
119
|
+
echo "ACTIVE_LOCK: held by ${lock_holder:-unknown} (active run on dashboard) — skipping"
|
|
120
|
+
found=1
|
|
121
|
+
elif [ "${lock_remaining:-unknown}" != "unknown" ] && [ "$lock_remaining" -gt 0 ]; then
|
|
122
|
+
echo "HELD_LOCK: held by ${lock_holder:-unknown} (lease ${lock_remaining}s remaining) — skipping"
|
|
123
|
+
found=1
|
|
124
|
+
else
|
|
125
|
+
echo "STALE_LOCK: held by ${lock_holder:-unknown} (age ${lock_age:-unknown}s, lease expired, no running session)"
|
|
126
|
+
found=1
|
|
127
|
+
if [ -n "$lock_holder" ]; then
|
|
128
|
+
rel_out=$("$MERGE_LOCK_SCRIPT" release "$lock_holder" 2>&1) || true
|
|
129
|
+
else
|
|
130
|
+
rel_out=$("$MERGE_LOCK_SCRIPT" force-release 2>&1) || true
|
|
136
131
|
fi
|
|
132
|
+
case "$rel_out" in
|
|
133
|
+
*RELEASED*)
|
|
134
|
+
echo " → released (lease expired)"
|
|
135
|
+
;;
|
|
136
|
+
*)
|
|
137
|
+
echo " → FAILED: could not release lock: $rel_out"
|
|
138
|
+
any_failed=1
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
137
141
|
fi
|
|
138
142
|
fi
|
|
139
143
|
fi
|