feat: exempt offsite claims from reclaim clock
This commit is contained in:
parent
dfcfd45563
commit
6a9b501e05
6 changed files with 76 additions and 12 deletions
10
BUILDER.md
10
BUILDER.md
|
|
@ -18,7 +18,8 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
|
||||
- Assign yourself, swap `ready` → `claimed`, and comment that you are
|
||||
starting. The claim is a promise of a draft PR soon — a claim with no PR
|
||||
and no activity is what the staleness sweep reclaims.
|
||||
and no activity is what the staleness sweep reclaims unless `offsite`
|
||||
records that its PR lives in another repository.
|
||||
- **Abandoning is fine; ghosting is not.** If you stop, say where you got to,
|
||||
push the branch if it holds anything useful, unassign, and restore
|
||||
`ready`.
|
||||
|
|
@ -28,9 +29,12 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
- Branch per issue; open the PR **as a draft early**, `Closes #N` in the
|
||||
body. `Closes #N` does not cross repos: when the PR is in a different repo
|
||||
from its authorizing issue, use `Part of <owner>/<repo>#N` instead, and
|
||||
comment on that issue with the draft PR link as soon as the draft opens.
|
||||
in the same step set `offsite` and comment on that issue with the draft PR
|
||||
link as soon as the draft opens.
|
||||
Triage closes the authorizing issue by hand when its acceptance criteria
|
||||
are met; the cross-repo merge never closes it. This codifies the linkage
|
||||
are met; at that handoff the builder reports whether the cross-repo PR
|
||||
merged or closed and clears `offsite` in the same comment. The cross-repo
|
||||
merge never closes the authorizing issue. This codifies the linkage
|
||||
builders already used on rig#112 and ceremony #13/#16 rather than adding a
|
||||
new review obligation. Drafts are invisible to the reviewer panel on
|
||||
purpose — the draft phase is yours.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ so entries say what changed, cite the issue, and stop.
|
|||
- Cross-repo doctrine: the panel is the PR's repo's roster, a review request is authorization but not panel membership, and `Part of <repo>#N` replaces the `Closes #N` that cannot cross repos (#57).
|
||||
- `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).
|
||||
|
||||
## 0.1.0 — 2026-07-22
|
||||
|
||||
|
|
|
|||
10
LABELS.md
10
LABELS.md
|
|
@ -66,6 +66,7 @@ comments, unassigns the stale owner, and restores `ready`.
|
|||
|---|---|---|
|
||||
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied |
|
||||
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) |
|
||||
| `offsite` | `#CFD3D7` | issue deliverable is a PR in another repository; set by the builder with the draft link and cleared by the builder at handoff |
|
||||
| `needs-ruling` | `#D4C5F9` | a human decision is required; the question, options and a recommendation are in the flagging comment. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply |
|
||||
| `release` | `#0E8A16` | release flow, versioning, packaging work — and the ceremony PR itself |
|
||||
| `merge-next` | `#0E8A16` | head of the merge queue — merge this one next. Queue order is *intent*: never set by the reconciler, only cleared by it |
|
||||
|
|
@ -99,6 +100,15 @@ activity, so it resets its own window and never repeats within a quiet
|
|||
week. Label churn is not activity — the clock reads comments, reviews and
|
||||
commits, or the sweep would reset itself.
|
||||
|
||||
`offsite` is issue-only and records that a claimed issue's deliverable lives
|
||||
in another repository, where a closing reference cannot make a local open PR
|
||||
visible to the sweep (#68). The builder sets it in the same step that posts
|
||||
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.
|
||||
|
||||
## Scope — which surface? (PRs and issues, any number)
|
||||
|
||||
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The
|
||||
|
|
|
|||
|
|
@ -101,6 +101,19 @@ claim_decision() { # $1 assignee count, $2 linked open PR, $3 age seconds
|
|||
fi
|
||||
}
|
||||
|
||||
claim_clock_exempt() { # labels on stdin -> EXEMPT | SWEEP
|
||||
local labels
|
||||
labels="$(cat)"
|
||||
# Cross-repo work has no local closing PR by construction (#68), so its
|
||||
# deliberate silence must share the one claim-clock gate with rulings.
|
||||
if grep -qxF offsite <<<"$labels" \
|
||||
|| [ "$(ruling_stale_exempt <<<"$labels")" = EXEMPT ]; then
|
||||
echo EXEMPT
|
||||
else
|
||||
echo SWEEP
|
||||
fi
|
||||
}
|
||||
|
||||
claim_decision_at() { # $1 assignee count, $2 linked open PR, $3 last activity epoch
|
||||
claim_decision "$1" "$2" "$((NOW - $3))"
|
||||
}
|
||||
|
|
@ -229,12 +242,10 @@ reconcile_issue() {
|
|||
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
|
||||
grep -qxF "$n" <<<"${OPEN_PR_ISSUES:-}" && open_pr=true
|
||||
age="$(last_issue_activity "$n" "$(jq -r '.created_at' <<<"$ISSUE_JSON")")"
|
||||
if [ "$(ruling_stale_exempt <<<"$ISSUE_LABELS")" = EXEMPT ]; then
|
||||
# Waiting on a human is legitimately quiet (#50 D10): the reclaim
|
||||
# clock does not run under a pending ruling — the same treatment
|
||||
# `blocked` gets by never reaching this branch at all. Only the clock
|
||||
# stops: an unassigned claim is still a repair the decision must see,
|
||||
# so it runs on a zero age rather than being skipped.
|
||||
if [ "$(claim_clock_exempt <<<"$ISSUE_LABELS")" = EXEMPT ]; then
|
||||
# Legitimately quiet work does not run the reclaim clock. Only the
|
||||
# clock stops: an unassigned claim is still a repair the decision must
|
||||
# see, so it runs on a zero age rather than being skipped.
|
||||
decision="$(claim_decision "$assignees" "$open_pr" 0)"
|
||||
else
|
||||
decision="$(claim_decision_at "$assignees" "$open_pr" "$age")"
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ blocker:unrequested|E99695|Somebody still owes a verdict and nobody was asked fo
|
|||
merge-next|0E8A16|Head of the merge queue — merge this one next (set by hand/agent, cleared here)
|
||||
stale|B60205|No activity for 48h — needs a poke (sweep-managed)
|
||||
blocked|6A737D|Waiting on another PR or issue to land first
|
||||
offsite|CFD3D7|Issue deliverable is a PR in another repository — claim clock paused
|
||||
needs-ruling|D4C5F9|A human decision is pending — question, options and a recommendation are in the comment
|
||||
release|0E8A16|Release flow and version/packaging work
|
||||
needs-triage|FBCA04|Did not come through triage — owes normalization or conversion to a discussion
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@ check "reclaim marker is stable within a claim episode" 0 "claim-reclaimed-96399
|
|||
claim_reclaim_marker 96399
|
||||
check "a later claim episode receives a new reclaim marker" 0 "claim-reclaimed-99999" \
|
||||
claim_reclaim_marker 99999
|
||||
check "offsite exempts the claim clock" 0 "EXEMPT" claim_clock_exempt <<<"offsite"
|
||||
check "needs-ruling still exempts through the shared gate" 0 "EXEMPT" \
|
||||
claim_clock_exempt <<<"needs-ruling"
|
||||
check "both quiet flags still produce one exemption verdict" 0 "EXEMPT" \
|
||||
claim_clock_exempt <<< $'offsite\nneeds-ruling'
|
||||
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
|
||||
|
||||
# Invariant 3: blocked declarations parse and release only when all close.
|
||||
refs="$(blocked_references <<< $'Context #99. Blocked by #12 (first), #7 (second). Blocks #44.')"
|
||||
|
|
@ -155,6 +163,10 @@ check "claimed plus a pending ruling is a healthy issue" 0 "KEEP" \
|
|||
queue_decision <<< $'claimed\nneeds-ruling'
|
||||
check "a ruling flag alone is still invariant 1's violation" 0 "ADD_NEEDS_TRIAGE" \
|
||||
queue_decision <<< $'needs-ruling'
|
||||
check "claimed plus offsite is a healthy issue" 0 "KEEP" \
|
||||
queue_decision <<< $'claimed\noffsite'
|
||||
check "offsite alone still needs triage" 0 "ADD_NEEDS_TRIAGE" \
|
||||
queue_decision <<<"offsite"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The ruling pass on the issue surface (#52), against a recording stub: the
|
||||
|
|
@ -201,13 +213,16 @@ issue_stub_gh() {
|
|||
fi
|
||||
}
|
||||
|
||||
issue_probe() { # $1 = issue number, $2 = labels → reconcile_issue's log lines
|
||||
issue_probe() { # $1 issue, $2 labels, $3 assignees (default 1), $4 open PR
|
||||
(
|
||||
local assignees="${3:-1}" open_pr="${4:-false}" assignee_json='[]'
|
||||
[ "$assignees" -eq 0 ] || assignee_json='[{"login":"owner-bot"}]'
|
||||
REPO=owner/repo NOW="$INOW"
|
||||
ISSUE_LABELS="$2"
|
||||
ISSUE_JSON="$(jq -n --arg at "$(iso_at $((INOW - 10 * 86400)))" \
|
||||
'{created_at: $at, assignees: [{login: "owner-bot"}], body: ""}')"
|
||||
OPEN_PR_ISSUES=""
|
||||
--argjson assignees "$assignee_json" \
|
||||
'{created_at: $at, assignees: $assignees, body: ""}')"
|
||||
if [ "$open_pr" = true ]; then OPEN_PR_ISSUES="$1"; else OPEN_PR_ISSUES=""; fi
|
||||
run() { "$@"; }
|
||||
gh() { issue_stub_gh "$@"; }
|
||||
reconcile_issue "$1" 2>&1
|
||||
|
|
@ -246,6 +261,28 @@ control="$(issue_probe 22 claimed)"
|
|||
check "the flag-free control is reclaimed (the clock still runs elsewhere)" 0 "" \
|
||||
grep -q 'stale claim reclaimed -> ready' <<<"$control"
|
||||
|
||||
# -- offsite stops only the reclaim clock ------------------------------------
|
||||
offsite="$(issue_probe 25 $'claimed\noffsite')"
|
||||
check "a 10-day-quiet offsite claim is not reclaimed" 1 "" \
|
||||
grep -q 'reclaimed' <<<"$offsite"
|
||||
offsite_unassigned="$(issue_probe 26 $'claimed\noffsite' 0)"
|
||||
check "an unassigned offsite claim is still flagged" 0 "" \
|
||||
grep -q 'issueflow:claimed-unassigned' "$TMP/posted-26"
|
||||
offsite_open="$(issue_probe 27 $'claimed\noffsite' 1 true)"
|
||||
check "an offsite claim with an open PR stays claimed" 1 "" \
|
||||
grep -q 'reclaimed' <<<"$offsite_open"
|
||||
offsite_both="$(issue_probe 28 $'claimed\noffsite\nneeds-ruling')"
|
||||
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" \
|
||||
"$ROOT/actions/labels-reconcile/labels-reconcile.sh"
|
||||
|
||||
# -- an already-applied stale heals off, and no edit names the flag ----------
|
||||
jq -n --arg l "$(iso_at $((INOW - 3600)))" \
|
||||
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l}]' >"$(tfix 23)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue