fix: harden issueflow reconciliation edges

This commit is contained in:
codex-bot-andresmgsl 2026-07-23 10:49:36 +00:00
parent 70db91fa1a
commit 43092576ef
3 changed files with 52 additions and 6 deletions

View file

@ -89,6 +89,8 @@ author_decision() { # $1 = true when author is triage; labels on stdin
claim_decision() { # $1 assignee count, $2 linked open PR, $3 age seconds claim_decision() { # $1 assignee count, $2 linked open PR, $3 age seconds
local assignees="$1" open_pr="$2" age="$3" local assignees="$1" open_pr="$2" age="$3"
# Staleness wins over missing ownership: a stale unassigned claim is
# derivably reclaimable, while a recent unassigned claim needs triage.
if [ "$open_pr" = false ] && [ "$age" -gt "$STALE_AFTER" ]; then echo RECLAIM if [ "$open_pr" = false ] && [ "$age" -gt "$STALE_AFTER" ]; then echo RECLAIM
elif [ "$assignees" -eq 0 ]; then echo FLAG_UNASSIGNED elif [ "$assignees" -eq 0 ]; then echo FLAG_UNASSIGNED
else echo KEEP else echo KEEP
@ -99,9 +101,34 @@ claim_decision_at() { # $1 assignee count, $2 linked open PR, $3 last activity e
claim_decision "$1" "$2" "$((NOW - $3))" claim_decision "$1" "$2" "$((NOW - $3))"
} }
claim_reclaim_marker() { # $1 = last activity epoch
printf 'claim-reclaimed-%s\n' "$1"
}
blocked_references() { # body on stdin -> issue numbers, one per line blocked_references() { # body on stdin -> issue numbers, one per line
sed -nE 's/.*Blocked by[[:space:]]+//Ip' \ # Dependency declarations sometimes soft-wrap after a comma. Continue
| sed -E 's/[.;][[:space:]].*$//' \ # through the first sentence terminator; if prose omits one, conservatively
# retain later references so ambiguity can keep an issue blocked, never
# promote it prematurely.
awk '
{
line = $0
lower = tolower(line)
if (!active) {
marker = "blocked by"
start = index(lower, marker)
if (!start) next
line = substr(line, start + length(marker))
active = 1
}
if (line ~ /[.;]/) {
sub(/[.;].*/, "", line)
print line
exit
}
print line
}
' \
| { grep -Eo '#[0-9]+' || true; } | tr -d '#' | sort -nu | { grep -Eo '#[0-9]+' || true; } | tr -d '#' | sort -nu
} }
@ -186,7 +213,10 @@ reconcile_issue() {
ensure_comment "$n" claimed-unassigned \ ensure_comment "$n" claimed-unassigned \
'This issue is `claimed` but has no assignee. The sweep cannot infer an owner; triage must repair the claim.' ;; 'This issue is `claimed` but has no assignee. The sweep cannot infer an owner; triage must repair the claim.' ;;
RECLAIM) RECLAIM)
ensure_comment "$n" claim-reclaimed \ # The last-activity epoch identifies a claim episode. A fixed marker
# hid the required comment when the same issue was later claimed and
# reclaimed again.
ensure_comment "$n" "$(claim_reclaim_marker "$age")" \
'This claim has no linked open PR and no activity for 48 hours. The sweep is reclaiming it for the ready queue.' 'This claim has no linked open PR and no activity for 48 hours. The sweep is reclaiming it for the ready queue.'
owners="$(jq -r '[.assignees[].login] | join(",")' <<<"$ISSUE_JSON")" owners="$(jq -r '[.assignees[].login] | join(",")' <<<"$ISSUE_JSON")"
if [ -n "$owners" ]; then if [ -n "$owners" ]; then

View file

@ -258,9 +258,9 @@ Both actor lists are whitespace-separated. `triage-actors` names the identities
allowed to mint issues without the sweep applying `needs-triage`. Label rows use exactly allowed to mint issues without the sweep applying `needs-triage`. Label rows use exactly
`name|color|description`; blank lines are ignored and extra pipes are refused. `name|color|description`; blank lines are ignored and extra pipes are refused.
There are no comment lines: every non-blank line must be the `panel=` There are no comment lines: every non-blank line must be the `panel=`
setting or a label row, so `#`-prefixed prose is a parse failure, not a setting, the `triage-actors=` setting, or a label row, so `#`-prefixed prose
comment (rig #13's conversion found this the hard way — keep the file data is a parse failure, not a comment (rig #13's conversion found this the hard
only). way — keep the file data only).
Core state, blocker, work-queue, and release labels come from ceremony. Scope Core state, blocker, work-queue, and release labels come from ceremony. Scope
rows remain consumer-owned because paths and surfaces differ by repository. rows remain consumer-owned because paths and surfaces differ by repository.

View file

@ -31,8 +31,10 @@ check "dogfood caller wakes on issue events" 0 " issues:" \
dogfood_pr_step="$(sed -n \ dogfood_pr_step="$(sed -n \
'/name: reconcile state + stale (dogfood/,/name: reconcile issue flow/p' \ '/name: reconcile state + stale (dogfood/,/name: reconcile issue flow/p' \
"$ROOT/.github/workflows/labels.yml")" "$ROOT/.github/workflows/labels.yml")"
# shellcheck disable=SC2016 # GitHub expressions are asserted as literals
check "dogfood PR reconcile receives repository" 0 ' REPO: ${{ github.repository }}' \ check "dogfood PR reconcile receives repository" 0 ' REPO: ${{ github.repository }}' \
grep -F ' REPO: ${{ github.repository }}' <<<"$dogfood_pr_step" grep -F ' REPO: ${{ github.repository }}' <<<"$dogfood_pr_step"
# shellcheck disable=SC2016 # GitHub expressions are asserted as literals
check "dogfood PR reconcile receives token" 0 ' GH_TOKEN: ${{ github.token }}' \ check "dogfood PR reconcile receives token" 0 ' GH_TOKEN: ${{ github.token }}' \
grep -F ' GH_TOKEN: ${{ github.token }}' <<<"$dogfood_pr_step" grep -F ' GH_TOKEN: ${{ github.token }}' <<<"$dogfood_pr_step"
@ -58,10 +60,24 @@ check "injected clock: exact stale boundary stays claimed" 0 "KEEP" \
check "injected clock: past stale boundary is reclaimed" 0 "RECLAIM" \ check "injected clock: past stale boundary is reclaimed" 0 "RECLAIM" \
bash -c 'ISSUEFLOW_NOW=100000 ISSUEFLOW_STALE_HOURS=1 source "$1"; claim_decision_at 1 false 96399' _ \ bash -c 'ISSUEFLOW_NOW=100000 ISSUEFLOW_STALE_HOURS=1 source "$1"; claim_decision_at 1 false 96399' _ \
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
# shellcheck disable=SC2016 # expansion belongs to the isolated bash -c process
check "invalid injected clock fails loudly" 1 "ISSUEFLOW_NOW must be UTC epoch seconds" \
bash -c 'ISSUEFLOW_NOW=garbage source "$1"' _ \
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
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
# 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.')"
check "blocked declaration extracts only declared refs" 0 $'7\n12' printf '%s\n' "$refs" check "blocked declaration extracts only declared refs" 0 $'7\n12' printf '%s\n' "$refs"
body=$'Blocked by #12 (first),\n#7 (soft-wrapped second). Blocks #44.'
check "soft-wrapped blocker declaration retains continuation refs" 0 "" test \
"$(blocked_references <<<"$body")" = $'7\n12'
body=$'Blocked by #12 (known)\nFollow-up context mentions #7 without a sentence boundary'
check "unterminated blocker prose errs toward retaining dependencies" 0 "" test \
"$(blocked_references <<<"$body")" = $'7\n12'
body="Part of #1. Blocked by #11 (needs a ceremony tag to pin), #12 (must be executed from the guide), #19 (the conversion vendors the doctrine). Blocks #14, #15 (they inherit the pilot's lessons)." body="Part of #1. Blocked by #11 (needs a ceremony tag to pin), #12 (must be executed from the guide), #19 (the conversion vendors the doctrine). Blocks #14, #15 (they inherit the pilot's lessons)."
check "real issue 13 inline blockers parse" 0 "" test \ check "real issue 13 inline blockers parse" 0 "" test \
"$(blocked_references <<<"$body")" = $'11\n12\n19' "$(blocked_references <<<"$body")" = $'11\n12\n19'