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
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
elif [ "$assignees" -eq 0 ]; then echo FLAG_UNASSIGNED
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_reclaim_marker() { # $1 = last activity epoch
printf 'claim-reclaimed-%s\n' "$1"
}
blocked_references() { # body on stdin -> issue numbers, one per line
sed -nE 's/.*Blocked by[[:space:]]+//Ip' \
| sed -E 's/[.;][[:space:]].*$//' \
# Dependency declarations sometimes soft-wrap after a comma. Continue
# 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
}
@ -186,7 +213,10 @@ reconcile_issue() {
ensure_comment "$n" claimed-unassigned \
'This issue is `claimed` but has no assignee. The sweep cannot infer an owner; triage must repair the claim.' ;;
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.'
owners="$(jq -r '[.assignees[].login] | join(",")' <<<"$ISSUE_JSON")"
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
`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=`
setting or a label row, so `#`-prefixed prose is a parse failure, not a
comment (rig #13's conversion found this the hard way — keep the file data
only).
setting, the `triage-actors=` setting, or a label row, so `#`-prefixed prose
is a parse failure, not a comment (rig #13's conversion found this the hard
way — keep the file data only).
Core state, blocker, work-queue, and release labels come from ceremony. Scope
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 \
'/name: reconcile state + stale (dogfood/,/name: reconcile issue flow/p' \
"$ROOT/.github/workflows/labels.yml")"
# shellcheck disable=SC2016 # GitHub expressions are asserted as literals
check "dogfood PR reconcile receives repository" 0 ' REPO: ${{ github.repository }}' \
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 }}' \
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" \
bash -c 'ISSUEFLOW_NOW=100000 ISSUEFLOW_STALE_HOURS=1 source "$1"; claim_decision_at 1 false 96399' _ \
"$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.
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"
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)."
check "real issue 13 inline blockers parse" 0 "" test \
"$(blocked_references <<<"$body")" = $'11\n12\n19'