feat: nudge resolved offsite claims

This commit is contained in:
codex-bot-andresmgsl 2026-07-23 13:17:39 +00:00
parent e9928f156f
commit bc3c1bd5cf
4 changed files with 67 additions and 3 deletions

View file

@ -12,6 +12,7 @@ so entries say what changed, cite the issue, and stop.
- `actions/runner-isolated` — a `pull_request`-triggered job may never run on a self-hosted runner (#58).
- The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52).
- `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68).
- `issueflow-reconcile` — nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69).
## 0.1.0 — 2026-07-22

View file

@ -107,7 +107,9 @@ the cross-repo draft link, then clears it at handoff in the same comment that
reports whether that PR merged or closed. The machine reads the flag and
never writes it. It stops only the claim-reclaim clock: missing assignees are
still flagged, queue-label conflicts and missing queue state are still
repaired, and epic-completion and PR-side stale behavior are unchanged.
repaired, and epic-completion and PR-side stale behavior are unchanged. The
sweep tells the assignee once when every visible cross-referenced PR has
closed; it only tells, and never clears the flag or changes the claim.
## Scope — which surface? (PRs and issues, any number)

View file

@ -192,6 +192,27 @@ epic_decision() { # $1 refs, $2 states
fi
}
offsite_cross_referenced_prs() { # timeline JSON on stdin -> owner/repo#N
jq -r '
.[]
| select(.event == "cross-referenced")
| .source.issue
| select(.pull_request != null)
| select(.repository.full_name != null and .number != null)
| "\(.repository.full_name)#\(.number)"
' | sort -u
}
offsite_resolved_decision() { # PR states on stdin -> NUDGE | QUIET
local states
states="$(cat)"
if [ -n "$states" ] && ! grep -Eq '^(OPEN|UNKNOWN)$' <<<"$states"; then
echo NUDGE
else
echo QUIET
fi
}
# API edge. Marker comments make warnings and nudges idempotent across sweeps.
ensure_comment() { # $1 issue, $2 marker, $3 message
local n="$1" marker="$2" message="$3"
@ -210,6 +231,21 @@ reference_states() {
done
}
offsite_pr_states() {
local ref repo number state
while IFS= read -r ref; do
[ -n "$ref" ] || continue
repo="${ref%#*}"
number="${ref##*#}"
state="$(gh api "repos/$repo/pulls/$number" --jq '.state' 2>/dev/null || echo UNKNOWN)"
case "$state" in open) echo OPEN ;; closed) echo CLOSED ;; *) echo UNKNOWN ;; esac
done
}
offsite_timeline() { # unreadable timelines are deliberately silent
gh api --paginate "repos/$REPO/issues/$1/timeline" 2>/dev/null || return 1
}
last_issue_activity() {
local n="$1" created="$2" latest
latest="$({
@ -269,6 +305,18 @@ reconcile_issue() {
fi
log "#$n: stale claim reclaimed -> ready" ;;
esac
if has_issue_label offsite; then
local timeline
if timeline="$(offsite_timeline "$n")"; then
refs="$(offsite_cross_referenced_prs <<<"$timeline")"
states="$(offsite_pr_states <<<"$refs")"
if [ "$(offsite_resolved_decision <<<"$states")" = NUDGE ]; then
ensure_comment "$n" offsite-resolved \
"$(tr '\n' ' ' <<<"$refs" | sed 's/[[:space:]]*$//') is closed; this issue's \`offsite\` flag is still up. Clear it and close the issue, or say what is still outstanding. @$(jq -r '.assignees[0].login' <<<"$ISSUE_JSON")"
log "#$n: resolved offsite PRs nudged"
fi
fi
fi
elif has_issue_label blocked; then
refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"

View file

@ -76,6 +76,21 @@ check "both quiet flags still produce one exemption verdict" 0 "EXEMPT" \
check "blocked does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"blocked"
check "ready does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"ready"
check "empty labels do not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt </dev/null
check "one closed offsite PR nudges" 0 "NUDGE" offsite_resolved_decision <<<"CLOSED"
check "two closed offsite PRs nudge" 0 "NUDGE" offsite_resolved_decision <<< $'CLOSED\nCLOSED'
check "one open offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision <<< $'CLOSED\nOPEN'
check "no visible offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision </dev/null
check "an unreadable offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision <<< $'CLOSED\nUNKNOWN'
timeline='[
{"event":"cross-referenced","source":{"issue":{"number":112,"repository":{"full_name":"heavy-duty/rig"},"pull_request":{"url":"x"}}}},
{"event":"cross-referenced","source":{"issue":{"number":7,"repository":{"full_name":"heavy-duty/rig"}}}},
{"event":"mentioned","source":{"issue":{"number":9,"repository":{"full_name":"heavy-duty/box"},"pull_request":{"url":"x"}}}},
{"event":"assigned"}
]'
check "offsite timeline extracts only cross-referenced PRs" 0 "heavy-duty/rig#112" \
offsite_cross_referenced_prs <<<"$timeline"
check "empty offsite timeline extracts nothing" 0 "" offsite_cross_referenced_prs <<<"[]"
# Invariant 3: blocked declarations parse and release only when all close.
refs="$(blocked_references <<< $'Context #99. Blocked by #12 (first), #7 (second). Blocks #44.')"
@ -276,8 +291,6 @@ check "offsite plus needs-ruling stays claimed" 1 "" \
grep -q 'reclaimed' <<<"$offsite_both"
check "a one-hour claim stays claimed for the ordinary age reason" 0 "KEEP" \
claim_decision 1 false 3600
check "the offsite exemption flag is named once at its issueflow decision point" 0 "1" \
grep -c 'offsite' "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
check "no reconciler mutation names offsite (#68 D4)" 1 "" \
grep -E 'gh (issue|pr) edit.*offsite' \
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" \