Merge pull request #70 from codex-bot-andresmgsl/build/68-offsite-claim-exemption

feat: exempt offsite claims from reclaim clock
This commit is contained in:
Daniel Marin 2026-07-23 14:07:44 +01:00 committed by GitHub
commit e9928f156f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 76 additions and 12 deletions

View file

@ -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 - 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 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, - **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 push the branch if it holds anything useful, unassign, and restore
`ready`. `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 - 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 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 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 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 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 new review obligation. Drafts are invisible to the reviewer panel on
purpose — the draft phase is yours. purpose — the draft phase is yours.

View file

@ -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). - 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). - `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). - 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 ## 0.1.0 — 2026-07-22

View file

@ -66,6 +66,7 @@ comments, unassigns the stale owner, and restores `ready`.
|---|---|---| |---|---|---|
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied | | `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) | | `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 | | `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 | | `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 | | `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 week. Label churn is not activity — the clock reads comments, reviews and
commits, or the sweep would reset itself. 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) ## Scope — which surface? (PRs and issues, any number)
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The

View file

@ -101,6 +101,19 @@ claim_decision() { # $1 assignee count, $2 linked open PR, $3 age seconds
fi 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_at() { # $1 assignee count, $2 linked open PR, $3 last activity epoch
claim_decision "$1" "$2" "$((NOW - $3))" claim_decision "$1" "$2" "$((NOW - $3))"
} }
@ -229,12 +242,10 @@ reconcile_issue() {
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")" assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
grep -qxF "$n" <<<"${OPEN_PR_ISSUES:-}" && open_pr=true grep -qxF "$n" <<<"${OPEN_PR_ISSUES:-}" && open_pr=true
age="$(last_issue_activity "$n" "$(jq -r '.created_at' <<<"$ISSUE_JSON")")" age="$(last_issue_activity "$n" "$(jq -r '.created_at' <<<"$ISSUE_JSON")")"
if [ "$(ruling_stale_exempt <<<"$ISSUE_LABELS")" = EXEMPT ]; then if [ "$(claim_clock_exempt <<<"$ISSUE_LABELS")" = EXEMPT ]; then
# Waiting on a human is legitimately quiet (#50 D10): the reclaim # Legitimately quiet work does not run the reclaim clock. Only the
# clock does not run under a pending ruling — the same treatment # clock stops: an unassigned claim is still a repair the decision must
# `blocked` gets by never reaching this branch at all. Only the clock # see, so it runs on a zero age rather than being skipped.
# 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)" decision="$(claim_decision "$assignees" "$open_pr" 0)"
else else
decision="$(claim_decision_at "$assignees" "$open_pr" "$age")" decision="$(claim_decision_at "$assignees" "$open_pr" "$age")"

View file

@ -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) 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) stale|B60205|No activity for 48h — needs a poke (sweep-managed)
blocked|6A737D|Waiting on another PR or issue to land first 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 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 release|0E8A16|Release flow and version/packaging work
needs-triage|FBCA04|Did not come through triage — owes normalization or conversion to a discussion needs-triage|FBCA04|Did not come through triage — owes normalization or conversion to a discussion

View file

@ -68,6 +68,14 @@ check "reclaim marker is stable within a claim episode" 0 "claim-reclaimed-96399
claim_reclaim_marker 96399 claim_reclaim_marker 96399
check "a later claim episode receives a new reclaim marker" 0 "claim-reclaimed-99999" \ check "a later claim episode receives a new reclaim marker" 0 "claim-reclaimed-99999" \
claim_reclaim_marker 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. # Invariant 3: blocked declarations parse and release only when all close.
refs="$(blocked_references <<< $'Context #99. Blocked by #12 (first), #7 (second). Blocks #44.')" 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' queue_decision <<< $'claimed\nneeds-ruling'
check "a ruling flag alone is still invariant 1's violation" 0 "ADD_NEEDS_TRIAGE" \ check "a ruling flag alone is still invariant 1's violation" 0 "ADD_NEEDS_TRIAGE" \
queue_decision <<< $'needs-ruling' 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 # The ruling pass on the issue surface (#52), against a recording stub: the
@ -201,13 +213,16 @@ issue_stub_gh() {
fi 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" REPO=owner/repo NOW="$INOW"
ISSUE_LABELS="$2" ISSUE_LABELS="$2"
ISSUE_JSON="$(jq -n --arg at "$(iso_at $((INOW - 10 * 86400)))" \ ISSUE_JSON="$(jq -n --arg at "$(iso_at $((INOW - 10 * 86400)))" \
'{created_at: $at, assignees: [{login: "owner-bot"}], body: ""}')" --argjson assignees "$assignee_json" \
OPEN_PR_ISSUES="" '{created_at: $at, assignees: $assignees, body: ""}')"
if [ "$open_pr" = true ]; then OPEN_PR_ISSUES="$1"; else OPEN_PR_ISSUES=""; fi
run() { "$@"; } run() { "$@"; }
gh() { issue_stub_gh "$@"; } gh() { issue_stub_gh "$@"; }
reconcile_issue "$1" 2>&1 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 "" \ check "the flag-free control is reclaimed (the clock still runs elsewhere)" 0 "" \
grep -q 'stale claim reclaimed -> ready' <<<"$control" 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"
issue_probe 26 $'claimed\noffsite' 0 >/dev/null
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 ---------- # -- an already-applied stale heals off, and no edit names the flag ----------
jq -n --arg l "$(iso_at $((INOW - 3600)))" \ jq -n --arg l "$(iso_at $((INOW - 3600)))" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l}]' >"$(tfix 23)" '[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l}]' >"$(tfix 23)"