Merge pull request #176 from codex-bot-andresmgsl/build/175-post-merge-queue-state

feat: add post-merge issue queue state
This commit is contained in:
Daniel Marin 2026-07-25 11:31:08 +01:00 committed by GitHub
commit ede2767232
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 347 additions and 48 deletions

View file

@ -141,7 +141,12 @@ triage bug, and the move is to say so on the issue, not to guess.
merge — a live proof of a workflow trigger, a released-artifact check,
anything whose subject does not exist until the change is on the base
branch — the same-repo PR uses `Refs #N` instead, and triage closes the
issue by hand on the evidence, exactly as it does for cross-repo work.
issue by hand on the evidence, exactly as it does for cross-repo work. The
merge releases the claim: the issue moves to `post-merge`, the builder
walks away, and triage owns verification and closure. If evidence later
requires corrective build work, triage returns it to `ready` or mints a
fresh `ready` issue; any builder claims from current `main`, and the
original builder has no special standing.
The issue body is what says so; you never judge which issues qualify, and
absent that instruction `Closes #N` remains the default. The exception was
bought the hard way: #143 carried `Closes #137` as doctrine then required,

View file

@ -52,14 +52,32 @@ strips it on sight).
| `ready` | `#0E8A16` | triaged, spec complete, unblocked — a builder can start now and succeed | triage |
| `claimed` | `#1D76DB` | a builder owns it: assignee set, a draft PR expected shortly | the claiming builder |
| `blocked` | `#6A737D` | waiting on another issue or PR (`Blocked by #N` in the body names it) | triage; anyone may correct it |
| `post-merge` | `#006B75` | the Refs-linked PR merged; post-merge acceptance criteria remain; the claim is released — nothing here is buildable and nobody owes a draft | the sweep or triage |
| `epic` | `#5319E7` | organizes other issues via a dependency-ordered task list; **builders never pick an epic** | triage |
The work-queue sweep enforces the invariant a board scan relies on: every open issue is either
`needs-triage`, `epic`, or carries exactly one of `ready` / `claimed` /
`blocked`. It flags conflicts rather than guessing intent. A `claimed` issue
`blocked` / `post-merge`. It flags conflicts rather than guessing intent. A `claimed` issue
with no open PR and no activity for 48 hours is reclaimed by the sweep: it
comments, unassigns the stale owner, and restores `ready`.
When a merged PR references a `claimed` issue with `Refs #N` and unchecked
criteria remain, the sweep moves the issue to `post-merge`, clears the
assignee, and comments with the remaining criteria verbatim. The comment says
that the claim is released and that triage owes a follow-up naming the owner
and wake condition for completion. Triage writes that full transition comment
in the same tick when it or the operator makes the move by hand. The sweep
never reclaims `post-merge`: weeks of quiet can be the state working.
`post-merge` never composes with `blocked`; the transition comment carries the
wait. It never composes with `attention`, because releasing the claim clears
the assignee and leaves nobody parked-for. An assigned `post-merge` issue is
flagged rather than repaired: a hand-assignment is intent. `needs-ruling`
still composes. When the remainder becomes buildable, triage moves
`post-merge` to `ready` or mints a fresh `ready` issue. Any builder may claim
that work from current `main`; the original builder has no special standing,
and re-entry does not set `attention`.
## Cross-cutting (PRs and issues)
| Label | Color | Meaning |
@ -155,7 +173,12 @@ unanswered `attention` is exactly the silence the 48-hour reclaim should
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.
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. 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

View file

@ -96,7 +96,12 @@ Every issue you mint carries, in this order:
rather than `Closes #N`. A criterion that survives the merge only if
someone remembers to reopen the issue is an incomplete criterion — #137's
amended body is the worked example, reopened by hand after `Closes #137`
closed it with the criterion unmet (#151).
closed it with the criterion unmet (#151). The merge moves the issue to
`post-merge` and releases the claim. The sweep writes the transition
comment when it derives the move; when triage or the operator moves it by
hand, triage writes the comment in the same tick. In either case triage
follows up with the remaining criteria, their owner, and the wake condition
for completion.
- **Test plan**: what proves it, including the cases that must fail.
- **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an
epic organizes it. Name a cross-repo dependency the same way with its
@ -126,6 +131,11 @@ itself. Keep the checklist current — a stale epic misleads every scan.
lands, and flags a blocked issue whose dependency declaration is unreadable.
- The sweep reclaims abandoned claims after 48 hours: `claimed` + no open PR
+ no activity → comment, unassign, restore `ready`.
- `post-merge` is triage's completion queue, not a parked claim. Tick verified
criteria and close under the criterion's existing contract. If corrective
build work becomes necessary, move it to `ready` or mint a fresh `ready`
issue: any builder claims from current `main`, the original builder has no
special standing, and re-entry does not set `attention`.
- Automation never guesses intent. Resolve the conflict comments it leaves on
malformed queue states, and close or extend completed epics when nudged.
- **Close obsolete issues** with the reason and a link to what obsoleted

View file

@ -21,7 +21,7 @@ ISSUEFLOW_STALE_HOURS="${ISSUEFLOW_STALE_HOURS:-48}"
}
NOW="$ISSUEFLOW_NOW"
STALE_AFTER=$((ISSUEFLOW_STALE_HOURS * 3600))
QUEUE_LABELS=(ready claimed blocked)
QUEUE_LABELS=(ready claimed blocked post-merge)
TRIAGE_ACTORS=()
# The needs-ruling invariants (#52) — one implementation for both surfaces.
@ -122,6 +122,50 @@ claim_reclaim_marker() { # $1 = last activity epoch
printf 'claim-reclaimed-%s\n' "$1"
}
refs_references() { # PR body on stdin -> local issue numbers named by Refs
awk '
{
line = $0
lower = tolower(line)
if (match(lower, /(^|[^[:alnum:]_-])refs[[:space:]:]+/)) {
line = substr(line, RSTART + RLENGTH)
if (line ~ /^(#|([[:alnum:]_.-]+\/)?[[:alnum:]_.-]+#)[0-9]+/) {
sub(/[.(;].*/, "", line)
print line
}
}
}
' | issue_references \
| awk -F '\t' '$1 == "LOCAL" { print $2 }' | sort -nu
}
unchecked_criteria() { # issue body on stdin -> unchecked task-list lines verbatim
awk '
/^[[:space:]]*([-*]|[0-9]+\.)[[:space:]]+\[[[:space:]]\]/ {
sub(/\r$/, "")
print
}
'
}
post_merge_decision() { # $1 merged Refs PR, $2 linked open PR, $3 already handled
local merged="$1" open_pr="$2" handled="$3" unchecked
unchecked="$(cat)"
if [ -n "$merged" ] && [ "$open_pr" = false ] && [ "$handled" = false ] \
&& [ -n "$unchecked" ]; then echo TRANSITION
else echo KEEP
fi
}
post_merge_pr_for_issue() { # $1 issue; records are ISSUE<TAB>PR
awk -F '\t' -v issue="$1" '$1 == issue { print $2 }' \
<<<"${MERGED_REF_PR_RECORDS:-}" | sort -n | tail -n1
}
post_merge_transition_marker() { # $1 merged PR number
printf 'post-merge-transition-pr-%s\n' "$1"
}
issue_references() { # text on stdin -> LOCAL/CROSS<TAB>reference
# A qualified reference belongs to another repository. Classify the whole
# token before extracting numbers so rig#112 can never become local #112.
@ -216,12 +260,16 @@ offsite_resolved_decision() { # PR states on stdin -> NUDGE | QUIET
# API edge. Marker comments make warnings and nudges idempotent across sweeps.
ensure_comment() { # $1 issue, $2 marker, $3 message
local n="$1" marker="$2" message="$3"
if gh api --paginate "repos/$REPO/issues/$n/comments" --jq '.[].body' \
| grep -qF "<!-- issueflow:$marker -->"; then return; fi
if issue_comment_has_marker "$n" "$marker"; then return; fi
run gh issue comment "$n" -R "$REPO" --body "<!-- issueflow:$marker -->
$message" >/dev/null
}
issue_comment_has_marker() { # $1 issue, $2 marker
gh api --paginate "repos/$REPO/issues/$1/comments" --jq '.[].body' \
| grep -qF "<!-- issueflow:$2 -->"
}
reference_states() {
local ref state
while IFS= read -r ref; do
@ -262,6 +310,8 @@ last_issue_activity() {
reconcile_issue() {
local n="$1" decision refs cross_refs states age assignees open_pr=false label owners
local merged_ref_pr="" transition_marker="" transition_handled=false
local unchecked="" remove_claimed=claimed
decision="$(queue_decision <<<"$ISSUE_LABELS")"
case "$decision" in
ADD_NEEDS_TRIAGE)
@ -269,7 +319,7 @@ reconcile_issue() {
log "#$n: needs-triage (no queue state)" ;;
FLAG_CONFLICT)
ensure_comment "$n" queue-conflict \
'The issue-flow sweep found conflicting queue labels. It cannot infer intent safely; triage must leave exactly one of `needs-triage`, `epic`, `ready`, `claimed`, or `blocked`.'
'The issue-flow sweep found conflicting queue labels. It cannot infer intent safely; triage must leave exactly one of `needs-triage`, `epic`, `ready`, `claimed`, `blocked`, or `post-merge`.'
log "#$n: conflicting queue labels; flagged"
return ;;
esac
@ -277,6 +327,35 @@ reconcile_issue() {
if has_issue_label claimed; then
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
grep -qxF "$n" <<<"${OPEN_PR_ISSUES:-}" && open_pr=true
merged_ref_pr="$(post_merge_pr_for_issue "$n")"
if [ -n "$merged_ref_pr" ]; then
transition_marker="$(post_merge_transition_marker "$merged_ref_pr")"
issue_comment_has_marker "$n" "$transition_marker" \
&& transition_handled=true
fi
unchecked="$(unchecked_criteria <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
if [ "$(post_merge_decision "$merged_ref_pr" "$open_pr" "$transition_handled" \
<<<"$unchecked")" = TRANSITION ]; then
ensure_comment "$n" "$transition_marker" \
"The Refs-linked PR merged with these acceptance criteria still unchecked:
$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 "$remove_claimed" --add-label post-merge >/dev/null
else
run gh issue edit "$n" -R "$REPO" \
--remove-label "$remove_claimed" --add-label post-merge >/dev/null
fi
log "#$n: merged Refs PR -> post-merge; claim released"
else
age="$(last_issue_activity "$n" "$(jq -r '.created_at' <<<"$ISSUE_JSON")")"
if [ "$(claim_clock_exempt <<<"$ISSUE_LABELS")" = EXEMPT ]; then
# Legitimately quiet work does not run the reclaim clock. Only the
@ -317,6 +396,14 @@ reconcile_issue() {
fi
fi
fi
fi
elif has_issue_label post-merge; then
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
if [ "$assignees" -gt 0 ] || has_issue_label attention; then
ensure_comment "$n" post-merge-assigned \
'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")")"
cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
@ -406,6 +493,22 @@ main() {
}
}' --jq '.data.repository.pullRequests.nodes[].closingIssuesReferences.nodes[].number' \
| sort -nu)"
MERGED_REF_PR_RECORDS="$(gh api graphql --paginate -f owner="$owner" -f name="$name" -f query='
query($owner: String!, $name: String!, $endCursor: String) {
repository(owner: $owner, name: $name) {
pullRequests(first: 100, states: MERGED, after: $endCursor) {
nodes { number body }
pageInfo { hasNextPage endCursor }
}
}
}' --jq '.data.repository.pullRequests.nodes[]
| .number as $pr | .body | split("\n")[]
| [$pr, .] | @tsv' \
| while IFS=$'\t' read -r pr body; do
while IFS= read -r issue; do
[ -n "$issue" ] && printf '%s\t%s\n' "$issue" "$pr"
done < <(refs_references <<<"$body")
done)"
local n
for n in $(gh api --paginate "repos/$REPO/issues?state=open&per_page=100" \

View file

@ -472,6 +472,7 @@ release|0E8A16|Release flow and version/packaging work
needs-triage|FBCA04|Did not come through triage — owes normalization or conversion to a discussion
ready|0E8A16|Triaged, spec complete, unblocked — a builder can start now and succeed
claimed|1D76DB|A builder owns it: assignee set, draft PR expected shortly
post-merge|006B75|Refs-linked PR merged; post-merge criteria remain and triage owns completion
epic|5319E7|Organizes other issues via a dependency-ordered task list — builders never pick it
EOF
}

1
changelog.d/175.md Normal file
View file

@ -0,0 +1 @@
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.

View file

@ -43,6 +43,10 @@ check "one ready queue label is valid" 0 "KEEP" queue_decision <<<"ready"
check "zero queue labels is derivably needs-triage" 0 "ADD_NEEDS_TRIAGE" queue_decision <<<"enhancement"
check "multiple queue labels are ambiguous" 0 "FLAG_CONFLICT" queue_decision <<< $'ready\nblocked'
check "needs-triage plus queue is a conflict" 0 "FLAG_CONFLICT" queue_decision <<< $'needs-triage\nready'
check "claimed plus post-merge is a conflict" 0 "FLAG_CONFLICT" \
queue_decision <<< $'claimed\npost-merge'
check "post-merge plus needs-ruling is healthy" 0 "KEEP" \
queue_decision <<< $'post-merge\nneeds-ruling'
# Invariant 2: claims have an owner and either a PR or recent activity.
check "claim with open PR stays claimed" 0 "KEEP" claim_decision 1 true 999999
@ -78,6 +82,20 @@ check "attention does not exempt a claimed issue" 0 "SWEEP" \
claim_clock_exempt <<<"attention"
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
refs_body=$'Refs #12\nAlso refs: #8 and heavy-duty/rig#4.\nCloses #99\nNot refs-ish #7\nfix refs parsing from #200\nCloses #40; refs: none\nRefs #175 (split from #150)'
check "Refs parser returns only references owned by a valid Refs marker" 0 "" \
test "$(refs_references <<<"$refs_body")" = $'8\n12\n175'
check "unchecked criteria preserve their source lines verbatim" 0 \
$'- [ ] first criterion\n * [ ] indented criterion\n1. [ ] numbered criterion' \
unchecked_criteria <<< $'- [x] done\n- [ ] first criterion\r\n * [ ] indented criterion\n1. [ ] numbered criterion'
check "merged Refs with unchecked criteria transitions" 0 "TRANSITION" \
post_merge_decision 12 false false <<<"- [ ] verify after merge"
check "open Refs does not transition" 0 "KEEP" \
post_merge_decision 12 true false <<<"- [ ] verify after merge"
check "a handled merged Refs episode does not transition again" 0 "KEEP" \
post_merge_decision 12 false true <<<"- [ ] verify after merge"
check "merged Refs with all criteria checked does not transition" 0 "KEEP" \
post_merge_decision 12 false false </dev/null
check "one closed offsite PR nudges" 0 "NUDGE" offsite_resolved_decision <<<"CLOSED"
check "two closed offsite PRs nudge" 0 "NUDGE" offsite_resolved_decision <<< $'CLOSED\nCLOSED'
check "one open offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision <<< $'CLOSED\nOPEN'
@ -234,16 +252,22 @@ issue_stub_gh() {
fi
}
issue_probe() { # $1 issue, $2 labels, $3 assignees (default 1), $4 open PR
issue_probe() { # $1 issue, $2 labels, $3 assignees, $4 open PR, $5 merged PR, $6 body
(
local assignees="${3:-1}" open_pr="${4:-false}" assignee_json='[]'
local assignees="${3:-1}" open_pr="${4:-false}" merged_ref_pr="${5:-}"
local body="${6:-}" 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)))" \
--argjson assignees "$assignee_json" \
'{created_at: $at, assignees: $assignees, body: ""}')"
--argjson assignees "$assignee_json" --arg body "$body" \
'{created_at: $at, assignees: $assignees, body: $body}')"
if [ "$open_pr" = true ]; then OPEN_PR_ISSUES="$1"; else OPEN_PR_ISSUES=""; fi
if [ -n "$merged_ref_pr" ]; then
MERGED_REF_PR_RECORDS="$(printf '%s\t%s\n' "$1" "$merged_ref_pr")"
else
MERGED_REF_PR_RECORDS=""
fi
run() { "$@"; }
gh() { issue_stub_gh "$@"; }
reconcile_issue "$1" 2>&1
@ -291,6 +315,108 @@ 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"
# -- merged Refs work releases the claim before the reclaim clock ------------
printf '[]\n' >"$(cfix 35)"
transition="$(issue_probe 35 claimed 1 false 350 $'- [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"
check "...states triage owes completion with owner and wake condition" 0 "" \
grep -qF 'Triage owes completion in a follow-up comment that names the owner and wake condition.' \
"$TMP/posted-35"
check "...unassigns and swaps claimed to post-merge" 0 "" \
grep -qF -- '--remove-assignee owner-bot --remove-label claimed --add-label post-merge' \
"$TMP/issue-edits"
printf '[]\n' >"$(cfix 36)"
post_merge_quiet="$(issue_probe 36 post-merge 0)"
check "quiet unassigned post-merge work is not reclaimed" 1 "" \
grep -q 'reclaimed' <<<"$post_merge_quiet"
check "...and causes no comment or edit" 1 "" test -f "$TMP/posted-36"
printf '[]\n' >"$(cfix 37)"
issue_probe 37 post-merge 1 >/dev/null
check "assigned post-merge is flagged" 0 "" \
grep -qF '<!-- issueflow:post-merge-assigned -->' "$TMP/posted-37"
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 380 '- [ ] 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 "" '- [ ] 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 400 '- [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 410 '- [ ] 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"
recent_timeline 43
jq -n --arg b '<!-- issueflow:post-merge-transition-pr-430 -->' \
--arg at "$(iso_at $((INOW - 60)))" \
'[{"body":$b,"created_at":$at}]' >"$(cfix 43)"
reentry_edit_count="$(wc -l <"$TMP/issue-edits")"
historical="$(issue_probe 43 claimed 1 false 430 '- [ ] corrective verification')"
check "a handled historical Refs merge cannot steal a re-entered claim" 0 "" \
test -z "$historical"
# shellcheck disable=SC2016 # positional parameters belong to bash -c
check "...and re-entry produces no edit or duplicate transition comment" 0 "" \
bash -c 'test "$1" -eq "$(wc -l <"$2")" && test ! -f "$3"' _ \
"$reentry_edit_count" "$TMP/issue-edits" "$TMP/posted-43"
printf '[]\n' >"$(cfix 44)"
second_transition="$(issue_probe 44 claimed 1 false 441 '- [ ] second verification')"
check "a later merged Refs PR gets an episode-specific transition comment" 0 "" \
grep -qF '<!-- issueflow:post-merge-transition-pr-441 -->' "$TMP/posted-44"
check "...and the later episode still transitions" 0 "" \
grep -qF 'merged Refs PR -> post-merge; claim released' <<<"$second_transition"
printf '[]\n' >"$(cfix 45)"
issue_probe 45 $'claimed\npost-merge' >/dev/null
# shellcheck disable=SC2016 # Markdown backticks are literal evidence
check "queue-conflict evidence lists every category including post-merge" 0 "" \
grep -qF 'needs-triage`, `epic`, `ready`, `claimed`, `blocked`, or `post-merge`' \
"$TMP/posted-45"
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 '<!-- issueflow:post-merge-assigned -->' "$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 "" \
@ -475,6 +601,33 @@ check "...stands down without minting" 1 "" test -s "$ARRIVAL/fixtures/edits"
check "...and the sweep still runs" 0 "" \
grep -qF 'issueflow: reconciled.' <<<"$pr_out"
# The merged-Refs transition must survive the executable's set -e path too.
# Keep this at main() granularity: the GraphQL gather and loop are the code
# a sourced decision probe cannot exercise (#91's lesson).
printf '%s\n' \
'{"data":{"repository":{"pullRequests":{"nodes":[{"number":400,"body":"Refs #40","closingIssuesReferences":{"nodes":[]}}],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
>"$ARRIVAL/fixtures/graphql.json"
printf '[{"number":40}]\n' \
>"$ARRIVAL/fixtures/repos_owner_repo_issues_state_open_per_page_100.json"
jq -n --arg at "$(iso_at "$INOW")" \
'{number:40,user:{login:"triage-one"},created_at:$at,body:"- [x] built\n- [ ] verify live label",labels:[{name:"claimed"}],assignees:[{login:"builder"}]}' \
>"$ARRIVAL/fixtures/repos_owner_repo_issues_40.json"
printf '[]\n' >"$ARRIVAL/fixtures/repos_owner_repo_issues_40_comments.json"
: >"$ARRIVAL/fixtures/edits"
subprocess_out="$(
env PATH="$ARRIVAL/stub:$PATH" GH_FIXTURES="$ARRIVAL/fixtures" \
REPO=owner/repo LABELS_CONF="$ARRIVAL/labels.conf" \
bash "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" 2>&1
)"
subprocess_rc=$?
check "executable sweep transitions merged Refs work" 0 "" \
test "$subprocess_rc" -eq 0
check "...reaches the transition through GraphQL and the issue loop" 0 "" \
grep -qF '#40: merged Refs PR -> post-merge; claim released' <<<"$subprocess_out"
check "...and performs the release edit from the executable path" 0 "" \
grep -qF -- 'issue edit 40 -R owner/repo --remove-assignee builder --remove-label claimed --add-label post-merge' \
"$ARRIVAL/fixtures/edits"
# D2 preserved: only the deliberate stand-downs changed; a genuine failure on
# the arrival path still kills the run loudly.
: >"$ARRIVAL/fixtures/repos_owner_repo_issues_91.json.error"

View file

@ -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" \