diff --git a/LABELS.md b/LABELS.md index 68d6fac..6a5af1e 100644 --- a/LABELS.md +++ b/LABELS.md @@ -174,7 +174,11 @@ take. It is hand-set doctrine only: nothing in `actions/` sets, clears, reads, or validates it, and no reconciler enforces the assignee requirement. An `attention` issue without an assignee is therefore a board bug, not a demand; anyone may assign it or remove the flag. It never composes with -`post-merge`, whose released claim has no assignee to answer the demand. +`post-merge`, whose released claim has no assignee to answer the demand. The +one machine-clear exception is the derived `claimed` → `post-merge` +transition: releasing the assignee clears a carried `attention` in the same +edit. A hand-created `post-merge` + `attention` composition is flagged, not +rewritten. The three signals are mutually distinct: `attention` means an assignee owes a move; `needs-ruling` means a human owes a decision under diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index f5cca51..3ddb687 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -281,7 +281,7 @@ last_issue_activity() { reconcile_issue() { local n="$1" decision refs cross_refs states age assignees open_pr=false label owners - local merged_ref=false unchecked="" + local merged_ref=false unchecked="" remove_claimed=claimed decision="$(queue_decision <<<"$ISSUE_LABELS")" case "$decision" in ADD_NEEDS_TRIAGE) @@ -307,12 +307,16 @@ $unchecked The merge releases the claim; no builder owes a draft. Triage owes completion in a follow-up comment that names the owner and wake condition." owners="$(jq -r '[.assignees[].login] | join(",")' <<<"$ISSUE_JSON")" + # `attention` is a demand for the assigned builder. The derived + # transition releases that builder, so carrying the demand forward + # would create an impossible parked-for state (#175 D4). + has_issue_label attention && remove_claimed=claimed,attention if [ -n "$owners" ]; then run gh issue edit "$n" -R "$REPO" --remove-assignee "$owners" \ - --remove-label claimed --add-label post-merge >/dev/null + --remove-label "$remove_claimed" --add-label post-merge >/dev/null else run gh issue edit "$n" -R "$REPO" \ - --remove-label claimed --add-label post-merge >/dev/null + --remove-label "$remove_claimed" --add-label post-merge >/dev/null fi log "#$n: merged Refs PR -> post-merge; claim released" else @@ -359,10 +363,10 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in fi elif has_issue_label post-merge; then assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")" - if [ "$assignees" -gt 0 ]; then + if [ "$assignees" -gt 0 ] || has_issue_label attention; then ensure_comment "$n" post-merge-assigned \ - 'This `post-merge` issue has an assignee. The sweep will not undo a hand-assignment; triage must either clear it or move the issue back into buildable queue state.' - log "#$n: assigned post-merge issue flagged" + 'This `post-merge` issue has an assignee or `attention`. The sweep will not undo hand-set intent; triage must clear the invalid composition or move the issue back into buildable queue state.' + log "#$n: assigned or attention-bearing post-merge issue flagged" fi elif has_issue_label blocked; then refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 9a89551..7679134 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -314,6 +314,7 @@ printf '[]\n' >"$(cfix 35)" transition="$(issue_probe 35 claimed 1 false true $'- [x] built\n- [ ] verify dispatch\n * [ ] confirm warning clears')" check "merged Refs + unchecked criteria transitions in the sweep body" 0 "" \ grep -q 'merged Refs PR -> post-merge; claim released' <<<"$transition" +# shellcheck disable=SC2016 # positional parameters belong to bash -c check "...names every remaining criterion verbatim in the comment" 0 "" \ bash -c 'grep -qF -- "- [ ] verify dispatch" "$1" && grep -qF -- " * [ ] confirm warning clears" "$1"' _ "$TMP/posted-35" @@ -337,6 +338,52 @@ check "assigned post-merge is flagged" 0 "" \ check "...and the hand-assignment is not repaired" 1 "" \ grep -qF -- 'issue edit 37' "$TMP/issue-edits" +# -- non-triggers stay byte-for-byte outside the transition ------------------ +recent_timeline() { + jq -n --arg at "$(iso_at $((INOW - 60)))" \ + '[{"event":"assigned","created_at":$at}]' >"$(tfix "$1")" + printf '[]\n' >"$(cfix "$1")" +} +edit_count_before="$(wc -l <"$TMP/issue-edits")" +recent_timeline 38 +open_refs="$(issue_probe 38 claimed 1 true false '- [ ] verify after merge')" +check "open Refs PR leaves the issue exactly as found" 0 "" \ + test -z "$open_refs" +# shellcheck disable=SC2016 # positional parameters belong to bash -c +check "...with no edit or comment" 0 "" \ + bash -c 'test "$1" -eq "$(wc -l <"$2")" && test ! -f "$3"' _ \ + "$edit_count_before" "$TMP/issue-edits" "$TMP/posted-38" + +recent_timeline 39 +merged_closes="$(issue_probe 39 claimed 1 false false '- [ ] verify after merge')" +check "merged Closes PR leaves a recent claim exactly as found" 0 "" \ + test -z "$merged_closes" +# shellcheck disable=SC2016 # positional parameters belong to bash -c +check "...with no edit or comment" 0 "" \ + bash -c 'test "$1" -eq "$(wc -l <"$2")" && test ! -f "$3"' _ \ + "$edit_count_before" "$TMP/issue-edits" "$TMP/posted-39" + +recent_timeline 40 +all_checked="$(issue_probe 40 claimed 1 false true '- [x] verified after merge')" +check "merged Refs with zero unchecked boxes leaves the issue exactly as found" 0 "" \ + test -z "$all_checked" +# shellcheck disable=SC2016 # positional parameters belong to bash -c +check "...with no edit or comment" 0 "" \ + bash -c 'test "$1" -eq "$(wc -l <"$2")" && test ! -f "$3"' _ \ + "$edit_count_before" "$TMP/issue-edits" "$TMP/posted-40" + +printf '[]\n' >"$(cfix 41)" +attention_transition="$(issue_probe 41 $'claimed\nattention' 1 false true '- [ ] verify')" +check "derived post-merge transition clears attention with the released claim" 0 "" \ + grep -qF -- '--remove-label claimed,attention --add-label post-merge' "$TMP/issue-edits" +check "...still completes the transition" 0 "" \ + grep -qF 'merged Refs PR -> post-merge; claim released' <<<"$attention_transition" + +printf '[]\n' >"$(cfix 42)" +issue_probe 42 $'post-merge\nattention' 0 >/dev/null +check "hand-created post-merge plus attention is flagged, not rewritten" 0 "" \ + grep -qF '' "$TMP/posted-42" + # -- offsite stops only the reclaim clock ------------------------------------ offsite="$(issue_probe 25 $'claimed\noffsite')" check "a 10-day-quiet offsite claim is not reclaimed" 1 "" \ diff --git a/test/labels-reconcile.test.sh b/test/labels-reconcile.test.sh index 2c41628..ed3439c 100755 --- a/test/labels-reconcile.test.sh +++ b/test/labels-reconcile.test.sh @@ -77,6 +77,9 @@ expect "a 300-char reason passes through whole" 300 "${#exact_reason}" # -- a missing core taxonomy row is visible without mutating labels ---------- core_rows="$(core_label_rows)" core_names="$(cut -d'|' -f1 <<<"$core_rows")" +expect "post-merge core row is byte-exact" \ + "post-merge|006B75|Refs-linked PR merged; post-merge criteria remain and triage owns completion" \ + "$(grep '^post-merge|' <<<"$core_rows")" expect "a complete core taxonomy does not warn" "" \ "$(missing_core_labels_warning "$core_rows" "$core_names")" expect "one missing core label is named exactly" \