fix(work-items): github adapter lease-coordination robustness (#370)#686
Conversation
renew-lease revived an expired lease and reclaim removed non-holder assignees. Both are lease-coordination correctness gaps in code relocated byte-for-byte by #224; a single hardening pass addresses both (sibling of #367 for local-markdown). Finding 1 (P1): renew-lease confirmed the handle still matched the active (non-superseded) lease but never checked liveness, so a crashed/delayed holder could PATCH a fresh renewed_at past TTL and reclaim an item another worker had handed off. It now checks wit_lease_is_live before patching and returns a conflict (exit 7) for an expired lease. Finding 2 (P1): on the expired + no-activity path reclaim removed ALL assignees, silently unassigning a manually-added user or a concurrent claimer (whose live lease then stayed while the frontier saw the item as unassigned). Removal is scoped to the lease holder, and ownership is revalidated immediately before mutating so a concurrent claim during the activity-check window aborts as a no-op rather than stripping the new owner. Extracts the shared active-lease selection to wit_select_active_lease (lib/lease.sh), reused by both verbs. New offline tests drive both fixes against a stubbed gh (github adapter conformance needs a live target); CONTRACT.md "Lease protocol" documents both semantics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 8s —— View job PR Review: fix(work-items): github adapter lease-coordination robustness (#370)Tasks:
SummaryBoth fixes are correct. The TOCTOU revalidation in Finding 1 — Minor consistency:
|
- Set the executable bit on lease-coordination.test.sh (matches every other seam script; Windows did not record the local chmod). - CHANGELOG: reword so a wrapped line no longer starts with `+`, which markdownlint read as a plus-style list marker (MD004). - lease-coordination.test.sh: drop the issue-number reference from the header comment (comment-hygiene issue-reference rule). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 4s —— View job PR Review: fix(work-items): github adapter lease-coordination robustness (#370)Tasks:
SummaryThis PR carries two commits. The primary commit ( The prior review in this thread gave an Approve with four minor/cosmetic findings. All four remain non-blocking and none were expected to be addressed. I've re-examined the code independently below. Correctness — renew-lease.shThe liveness gate is correctly placed after the active-lease identity check, not before. Order matters: checking liveness before identity would allow a live-but-stale session to prove its handle is current and then discover it isn't — this path is correct. The The gate uses Correctness — reclaim.shRevalidation: Lines 68–75 re-read leases and confirm Holder-only removal: Line 82 reads Idempotency guard: The fresh Correctness — lib/lease.sh extractionThe output vars are reset at function entry (lines 43–44), which is load-bearing: a call that finds no active lease must leave both empty, not inherit a previous call's results. The Test coverage
One observation (non-blocking): the Hygiene commit
All three are correct and minimal. VerdictApprove. Both fixes are correct, the TOCTOU revalidation and liveness gate close the two P1 correctness gaps precisely, the test discrimination proofs are strong, and the |
Address the reviewer's non-blocking observation: the timeline read carries --paginate too, so its path-based case arm must precede the generic --paginate arm. Document the ordering so a future reorder does not silently misroute the stub. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 49s —— View job PR Review: fix(work-items): github adapter lease-coordination robustness (#370)Tasks:
SummaryBoth fixes are correct and well-structured. The liveness gate in Correctness —
|
|
Post-green review pass — 3 rounds, all Approve, no blocking findings.
No inline review threads to resolve (bot posts as PR-level comments). Flipping to ready. |
|
Claude finished @kyle-sexton's task in 2m 43s —— View job PR Review — fix(work-items): github adapter lease-coordination robustness (#370) Tasks:
SummaryThree prior review rounds all landed at Approve; I've read all changed files independently. Both fixes are correct, the extraction is clean, and the test discrimination proofs are rigorous. I have one new observation not flagged before (see Finding 1), and confirm the one known gap (concurrent-renew revalidation path, filed as #687) remains the only non-blocking open item. Finding 1 —
|
| Scenario | Cases |
|---|---|
| Expired active → renew refuses (no PATCH) | 1–2 |
| Live active → renew accepts | 3–5 |
| Holder-only removal with co-assignee | 6–9 |
| Concurrent supersede during activity window → no-op | 10–13 |
Discrimination proofs (pre-fix failures) confirm none of the 13 passing cases are vacuously true.
Known gap (non-blocking, filed as #687): the same-ID-but-now-live revalidation path in reclaim — where a concurrent renew-lease updates renewed_at on the same comment during the activity window — is not exercised. The implementation handles it correctly (WIT_ACTIVE_LEASE_JSON from the second read picks up the updated renewed_at; fresh epoch detects live). A test scenario can be constructed with lease-comments-1 (expired alice, ID 123) and lease-comments-2 (same ID 123, renewed_at in $FUTURE).
Test coverage — lib/lease.test.sh
Four new wit_select_active_lease cases cover the load-bearing invariants: single active lease, back-off masking, newest-wins, and empty-on-all-superseded. These are exactly the edge cases both verbs depend on.
CONTRACT.md / CHANGELOG / plugin.json
- "Lease protocol" additions accurately document both new semantics and are consistent with the implementation.
- CHANGELOG entry at
0.14.3is clear and technically precise. - Version bump
0.14.2→0.14.3is correct scope for a patch fix.
Verdict
Approve. Both fixes are correct, the TOCTOU revalidation and liveness gate close the two P1 gaps precisely, and the wit_select_active_lease extraction eliminates a third copy of the selection loop. Test discrimination proofs are strong. The one known test gap (concurrent-renew path) is non-blocking and already tracked in #687.
…ranch Address the review's non-blocking coverage finding. The existing revalidation scenarios exercised only the ID-changed (concurrent supersede) branch; add a parallel scenario where the revalidation read returns the SAME active lease id but with renewed_at now in the future (a concurrent renew-lease that completed during the activity window). reclaim must abort as a no-op (reclaimed:false), unassigning nothing and superseding nothing. Both revalidation branches are now independently verified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 4s —— View job PR Review: fix(work-items): github adapter lease-coordination robustness (#370)Tasks:
SummaryThis synchronize was triggered by commit Correctness of the new scenario (lines 147–162)The scenario exercises the second revalidation branch at if wit_lease_is_live "$WIT_ACTIVE_LEASE_JSON" "$(date -u +%s)"; then
emit false "lease renewed during reclaim"
fiStub construction:
Trace through
All four assertions follow correctly:
The key correctness detail is that the liveness check at line 73 uses Coverage now completeThe two revalidation branches are now independently verified:
Both branches assert identical behavioral invariants: VerdictApprove. The new scenario is correctly constructed, the stub interaction correctly models a concurrent in-place renewal (counter file → |
Summary
Follow-up from #224. Closes two lease-coordination correctness gaps in the GitHub adapter's
renew-lease.shandreclaim.sh— both in code relocated byte-for-byte from medley by #224 (not changed there), handled as the single hardening pass the issue calls for. No new adapter capability; a bug fix, so a patch bump (0.14.2→0.14.3). This is the GitHub-adapter sibling of #367 (local-markdown), shipped earlier this session.Fix
Finding 1 (P1) —
renew-leaserevived an expired lease.renew-leaseselected the active (newest non-superseded) lease, confirmedactive_id == lease_comment_id, and PATCHed a freshrenewed_at— but never checked liveness first. A worker retaining its handle pastrenewed_at + ttl_hours, with no newer lease comment, still matchedactive_id, so the PATCH revived an expired lease and defeated TTL-based handoff: a crashed/delayed worker could reclaim an item another worker had reasonably treated as expired.renew-lease.shnow checkswit_lease_is_liveimmediately before patching and returns a conflict (exit 7) for an expired-but-not-superseded active lease instead of reviving it. Recovery from an expired lease is a fresh claim/reclaim, not a revive of the dead handle.Finding 2 (P1) —
reclaimremoved assignees that did not hold the expired lease. On the expired-lease + no-activity path,reclaimread every assignee ([.assignees[].login]) and removed each one — not just the lease'sholder. A user manually assigned after the old lease, or a concurrent claimer added before this assignee snapshot, was silently unassigned; in the concurrent case that claimer's live lease stayed while the frontier treated the item as unassigned (two workers on one item).reclaim.shnow removes only the expired lease'sholder(guarded on a fresh assignee read so an already-unassigned holder is a no-op), leaving co-assignees untouched.reclaimre-reads the leases and confirms this is still the active and still expired lease; if a concurrent claim won the item in that window, reclaim is a no-op (reclaimed: false) — never a mutation that strips the new owner.Both verbs now share
wit_select_active_lease(extracted tolib/lease.sh) instead of a third copy of the active-lease selection loop.CONTRACT.md's "Lease protocol" documents both semantics (renew refuses an expired lease; reclaim is holder-only and revalidated).Verification
New offline test
adapters/github/lease-coordination.test.shdrives both fixes against a stubbedgh(the GitHub-adapter conformance binding requires a live target, so these decision-path cases live in a dedicated test — mirroring the sibling'sclaim-integrity.test.shprecedent). It asserts on the recorded write/remove call-log, not just exit codes.Against the fix —
bash adapters/github/lease-coordination.test.sh, exit 0 (cases 14–17 cover the concurrent in-place-renew revalidation branch, added per review):Discrimination proof — the (13-case) test against the pre-fix
renew-lease.sh/reclaim.shatorigin/main(exit 1), confirming the cases fail without the fix and are not passing for the wrong reason:Direct unit coverage of the extracted helper added to
lib/lease.test.sh(newest-non-superseded selection, superseded back-off not masking an earlier active lease, empty out-params when none) — 15/15 pass. Fullplugins/work-items/**/*.test.shsweep green; ShellCheck clean (--rcfile .shellcheckrc) on every changed script; changed files carry no trailing whitespace or tabs.Closes #370
Related
local-markdownlease/assignee-integrity fix shipped earlier this session (same lease-integrity class, different adapter).🤖 Generated with a Claude Code implementation subagent (issue #370)