From dbce97c59e37724f57fa7c2f7c0da79b63a924d8 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 12:44:42 +0200 Subject: [PATCH 1/9] ci(mq): mq_feeder.sh + 5-min user timer enqueue the oldest green PR when the merge queue is empty (PMAT-5843) Re-reads mergeQueueEntry after 30 s; a kicked PR is recorded and skipped, reason posted to the cop inbox. Co-Authored-By: Claude Sonnet 5.5 --- docs/roadmaps/entries/PMAT-5843.yaml | 19 +++++++++ scripts/mq_feeder.sh | 64 ++++++++++++++++++++++++++++ scripts/systemd/mq-feeder.service | 5 +++ scripts/systemd/mq-feeder.timer | 8 ++++ 4 files changed, 96 insertions(+) create mode 100644 docs/roadmaps/entries/PMAT-5843.yaml create mode 100644 scripts/mq_feeder.sh create mode 100644 scripts/systemd/mq-feeder.service create mode 100644 scripts/systemd/mq-feeder.timer diff --git a/docs/roadmaps/entries/PMAT-5843.yaml b/docs/roadmaps/entries/PMAT-5843.yaml new file mode 100644 index 0000000000..252e4b79a5 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5843.yaml @@ -0,0 +1,19 @@ +- id: PMAT-5843 + github_issue: 5843 + item_type: task + title: 'MQ feeder: every 5 min enqueue the oldest green non-draft PR when the merge queue is empty' + status: planned + priority: high + assigned_to: null + created: 2026-10-21T10:55:00Z + updated: 2026-10-21T10:55:00Z + spec: null + acceptance_criteria: + - 'scripts/mq_feeder.sh --self-test green; timer live; kicked PRs re-read at 30s and skipped' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5510 diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh new file mode 100644 index 0000000000..33de2e9f02 --- /dev/null +++ b/scripts/mq_feeder.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# mq_feeder.sh -- PMAT-5843. If the merge queue on main is EMPTY, enqueue the oldest green non-draft PR +# through scripts/arm_pr_automerge.sh and post one line to the cop inbox. Run by a local systemd user timer +# every 5 min (not a workflow). Operator, verbatim (10:29Z): "a job checks MQ every 5 min; if empty, it +# enqueues the oldest green non-draft PR (arm_pr_automerge.sh) and posts to the cop inbox. Reversible, 2 h window." +# Gotcha: on a CLEAN PR `gh pr merge --squash --auto` prints "merge strategy is set by the merge queue" and +# does nothing; arm_pr_automerge.sh uses plain `gh pr merge`. This script then READS mergeQueueEntry back. +# Kill switch: touch "$STATE/disabled" (or `systemctl --user disable --now mq-feeder.timer`). +# Usage: mq_feeder.sh [--dry-run] | --self-test Exit: 0 ok/noop, 2 ENV +set -uo pipefail +REPO="${MQ_FEEDER_REPO:-paiml/aprender}" +STATE="${MQ_FEEDER_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/mq-feeder}" +INBOX="${MQ_FEEDER_INBOX:-/mnt/nvme-raid0/cop-inbox/inbox.md}" +ARM="${MQ_FEEDER_ARM:-$(dirname "$0")/arm_pr_automerge.sh}" +TRIES="${MQ_FEEDER_TRIES:-3}" +dry=0 +Q='query($o:String!,$n:String!){repository(owner:$o,name:$n){mergeQueue(branch:"main"){entries(first:1){totalCount}} pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:ASC}){nodes{number isDraft baseRefName mergeStateStatus autoMergeRequest{enabledAt} mergeQueueEntry{position}}}}}' +# candidates JSON -> "QUEUE_BUSY" | one PR number per line, oldest first (CLEAN, non-draft, base main, not armed, not queued) +pick() { + jq -r '.data.repository as $r + | if ($r.mergeQueue.entries.totalCount // 0) > 0 then "QUEUE_BUSY" + else $r.pullRequests.nodes[] | select(.isDraft==false and .baseRefName=="main" and .mergeStateStatus=="CLEAN" and .autoMergeRequest==null and .mergeQueueEntry==null) | .number end' <<<"$1" +} +inbox() { printf '%s | %s | %s | %s\n' "$(date -u +%H:%MZ)" mq-feeder PMAT-5843 "$1" >> "$INBOX"; } +if [ "${1:-}" = --self-test ]; then + fail=0 + mk() { printf '{"data":{"repository":{"mergeQueue":{"entries":{"totalCount":%s}},"pullRequests":{"nodes":[%s]}}}}' "$1" "$2"; } + pr() { printf '{"number":%s,"isDraft":%s,"baseRefName":"%s","mergeStateStatus":"%s","autoMergeRequest":%s,"mergeQueueEntry":null}' "$1" "$2" "$3" "$4" "$5"; } + eq() { [ "$2" = "$3" ] || { echo "FAIL $1: got '$2' want '$3'"; fail=1; }; } + eq 'busy queue -> nothing enqueued' "$(pick "$(mk 2 "$(pr 1 false main CLEAN null)")")" QUEUE_BUSY + eq 'oldest clean first' "$(pick "$(mk 0 "$(pr 7 false main CLEAN null),$(pr 9 false main CLEAN null)")" | head -1)" 7 + eq 'draft skipped' "$(pick "$(mk 0 "$(pr 7 true main CLEAN null),$(pr 9 false main CLEAN null)")" | head -1)" 9 + eq 'BEHIND/BLOCKED/DIRTY skipped' "$(pick "$(mk 0 "$(pr 1 false main BEHIND null),$(pr 2 false main BLOCKED null),$(pr 3 false main DIRTY null)")")" '' + eq 'non-main base skipped' "$(pick "$(mk 0 "$(pr 4 false feat CLEAN null)")")" '' + eq 'already armed skipped' "$(pick "$(mk 0 "$(pr 5 false main CLEAN '{"enabledAt":"x"}')")")" '' + eq 'no PRs' "$(pick "$(mk 0 '')")" '' + [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" +fi +[ "${1:-}" = --dry-run ] && dry=1 +mkdir -p "$STATE" || exit 2 +[ -e "$STATE/disabled" ] && exit 0 +rem=$(gh api rate_limit --jq .resources.graphql.remaining 2>/dev/null) || exit 2 +[ "${rem:-0}" -ge 500 ] || exit 0 +json=$(gh api graphql -f query="$Q" -f o="${REPO%/*}" -f n="${REPO#*/}" 2>/dev/null) || exit 2 +cands=$(pick "$json") || exit 2 +[ "$cands" = QUEUE_BUSY ] && exit 0 +[ -n "$cands" ] || exit 0 +n=0 +for pr in $cands; do + grep -qx "$pr" "$STATE/kicked" 2>/dev/null && continue # kicked by the queue once: the owner must push a fix and clear it + n=$((n + 1)); [ "$n" -le "$TRIES" ] || break + if [ "$dry" = 1 ]; then echo "would arm #$pr"; exit 0; fi + out=$(bash "$ARM" "$pr" 2>&1); rc=$? + if [ "$rc" -eq 0 ]; then + sleep "${MQ_FEEDER_RECHECK:-30}" + st=$(gh api graphql -f query='query($o:String!,$n:String!,$p:Int!){repository(owner:$o,name:$n){pullRequest(number:$p){state mergeQueueEntry{position} timelineItems(last:3,itemTypes:[REMOVED_FROM_MERGE_QUEUE_EVENT]){nodes{... on RemovedFromMergeQueueEvent{reason createdAt}}}}}}' -f o="${REPO%/*}" -f n="${REPO#*/}" -F p="$pr" --jq '.data.repository.pullRequest | if .state=="MERGED" then "merged" elif .mergeQueueEntry then "queued pos \(.mergeQueueEntry.position)" else "KICKED: \(.timelineItems.nodes[-1].reason // "unknown")" end' 2>/dev/null || echo unread) + case "$st" in KICKED*) echo "$pr" >> "$STATE/kicked"; inbox "queue was empty: enqueued #$pr, then $st within ${MQ_FEEDER_RECHECK:-30}s; skipped until removed from $STATE/kicked" ;; *) inbox "queue was empty: enqueued oldest green PR #$pr ($st)" ;; esac + exit 0 + fi + [ "$rc" -eq 3 ] && continue # refused on that PR (guard-tree not green on its head): try the next + inbox "queue empty, arm of #$pr failed rc=$rc: $(printf '%s' "$out" | tail -n 1 | cut -c1-120)"; exit 0 +done +inbox "queue empty, no PR enqueued: $((n > TRIES ? TRIES : n)) CLEAN candidate(s) refused by arm_pr_automerge.sh" +exit 0 diff --git a/scripts/systemd/mq-feeder.service b/scripts/systemd/mq-feeder.service new file mode 100644 index 0000000000..6282f5e7c5 --- /dev/null +++ b/scripts/systemd/mq-feeder.service @@ -0,0 +1,5 @@ +[Unit] +Description=mq-feeder: enqueue the oldest green PR when the merge queue is empty (PMAT-5843) +[Service] +Type=oneshot +ExecStart=/mnt/nvme-raid0/wt/w5382-mqf/scripts/mq_feeder.sh diff --git a/scripts/systemd/mq-feeder.timer b/scripts/systemd/mq-feeder.timer new file mode 100644 index 0000000000..98b888a67b --- /dev/null +++ b/scripts/systemd/mq-feeder.timer @@ -0,0 +1,8 @@ +[Unit] +Description=mq-feeder tick, every 5 min (PMAT-5843) +[Timer] +OnBootSec=2min +OnUnitActiveSec=5min +AccuracySec=10s +[Install] +WantedBy=timers.target From 7133af767d27e604f31a45916650b010a97936d4 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 12:44:55 +0200 Subject: [PATCH 2/9] ci(mq): mq_feeder.sh executable (PMAT-5843) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/mq_feeder.sh diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh old mode 100644 new mode 100755 From d53e671268697c36d29f6359896be751a199bcea Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 12:46:55 +0200 Subject: [PATCH 3/9] ci(mq): mq_feeder hourly :45 summary line (ticks, enqueued, kicked) (PMAT-5843) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 8 +++++++- scripts/systemd/mq-feeder-summary.service | 5 +++++ scripts/systemd/mq-feeder-summary.timer | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 scripts/systemd/mq-feeder-summary.service create mode 100644 scripts/systemd/mq-feeder-summary.timer diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index 33de2e9f02..c5304aef37 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -21,7 +21,7 @@ pick() { | if ($r.mergeQueue.entries.totalCount // 0) > 0 then "QUEUE_BUSY" else $r.pullRequests.nodes[] | select(.isDraft==false and .baseRefName=="main" and .mergeStateStatus=="CLEAN" and .autoMergeRequest==null and .mergeQueueEntry==null) | .number end' <<<"$1" } -inbox() { printf '%s | %s | %s | %s\n' "$(date -u +%H:%MZ)" mq-feeder PMAT-5843 "$1" >> "$INBOX"; } +inbox() { mkdir -p "$STATE"; printf '%s %s\n' "$(date +%s)" "$1" >> "$STATE/acts.log"; printf '%s | %s | %s | %s\n' "$(date -u +%H:%MZ)" mq-feeder PMAT-5843 "$1" >> "$INBOX"; } if [ "${1:-}" = --self-test ]; then fail=0 mk() { printf '{"data":{"repository":{"mergeQueue":{"entries":{"totalCount":%s}},"pullRequests":{"nodes":[%s]}}}}' "$1" "$2"; } @@ -37,8 +37,14 @@ if [ "${1:-}" = --self-test ]; then [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" fi [ "${1:-}" = --dry-run ] && dry=1 +if [ "${1:-}" = --summary ]; then + mkdir -p "$STATE"; since=$(($(date +%s) - 3600)) + t=$(awk -v s="$since" '$1>=s' "$STATE/ticks.log" 2>/dev/null | wc -l); e=$(awk -v s="$since" '$1>=s && /enqueued oldest/' "$STATE/acts.log" 2>/dev/null | wc -l); k=$(awk -v s="$since" '$1>=s && /KICKED/' "$STATE/acts.log" 2>/dev/null | wc -l) + inbox "summary 1h: $t ticks, $e enqueued, $k kicked"; exit 0 +fi mkdir -p "$STATE" || exit 2 [ -e "$STATE/disabled" ] && exit 0 +date +%s >> "$STATE/ticks.log" rem=$(gh api rate_limit --jq .resources.graphql.remaining 2>/dev/null) || exit 2 [ "${rem:-0}" -ge 500 ] || exit 0 json=$(gh api graphql -f query="$Q" -f o="${REPO%/*}" -f n="${REPO#*/}" 2>/dev/null) || exit 2 diff --git a/scripts/systemd/mq-feeder-summary.service b/scripts/systemd/mq-feeder-summary.service new file mode 100644 index 0000000000..fef22b25d4 --- /dev/null +++ b/scripts/systemd/mq-feeder-summary.service @@ -0,0 +1,5 @@ +[Unit] +Description=mq-feeder hourly summary line (PMAT-5843) +[Service] +Type=oneshot +ExecStart=/mnt/nvme-raid0/wt/w5382-mqf/scripts/mq_feeder.sh --summary diff --git a/scripts/systemd/mq-feeder-summary.timer b/scripts/systemd/mq-feeder-summary.timer new file mode 100644 index 0000000000..2048dcc0f7 --- /dev/null +++ b/scripts/systemd/mq-feeder-summary.timer @@ -0,0 +1,6 @@ +[Unit] +Description=mq-feeder summary at :45 +[Timer] +OnCalendar=*:45:00 +[Install] +WantedBy=timers.target From f097806d82e7716b0928652f8b2ce1e381ed3188 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 12:56:26 +0200 Subject: [PATCH 4/9] ci(mq): feeder judges x86-main (guard-tree is not a check run), names each skip with its reason, 'needs seats' for gate content (PMAT-5843) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index c5304aef37..d7b7bcfc5c 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -12,6 +12,8 @@ REPO="${MQ_FEEDER_REPO:-paiml/aprender}" STATE="${MQ_FEEDER_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/mq-feeder}" INBOX="${MQ_FEEDER_INBOX:-/mnt/nvme-raid0/cop-inbox/inbox.md}" ARM="${MQ_FEEDER_ARM:-$(dirname "$0")/arm_pr_automerge.sh}" +# guard-tree is a section inside x86-main, not a check run on PR heads (arm_pr_automerge.sh refused every PR as "absent"): judge x86-main +export GUARD_CHECK_NAME="${GUARD_CHECK_NAME:-x86-main}" TRIES="${MQ_FEEDER_TRIES:-3}" dry=0 Q='query($o:String!,$n:String!){repository(owner:$o,name:$n){mergeQueue(branch:"main"){entries(first:1){totalCount}} pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:ASC}){nodes{number isDraft baseRefName mergeStateStatus autoMergeRequest{enabledAt} mergeQueueEntry{position}}}}}' @@ -21,6 +23,15 @@ pick() { | if ($r.mergeQueue.entries.totalCount // 0) > 0 then "QUEUE_BUSY" else $r.pullRequests.nodes[] | select(.isDraft==false and .baseRefName=="main" and .mergeStateStatus=="CLEAN" and .autoMergeRequest==null and .mergeQueueEntry==null) | .number end' <<<"$1" } +# gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head) +gate_paths() { grep -qE '^(\.github/workflows/|ci/sections\.yml|scripts/(check_|arm_pr_automerge))' <<<"$1"; } +needs_seats() { # PR HEAD -> prints reason and returns 0 when a gate-content PR lacks 2 seats bound to HEAD + local f s; f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || return 1 + gate_paths "$f" || return 1 + s=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq "[.[]|select(.body|test(\"APPROVE @'$2'\"))]|length" 2>/dev/null || echo 0) + [ "${s:-0}" -ge 2 ] && return 1 + echo "needs seats (gate content, ${s:-0}/2 APPROVE bound to ${2:0:10})" +} inbox() { mkdir -p "$STATE"; printf '%s %s\n' "$(date +%s)" "$1" >> "$STATE/acts.log"; printf '%s | %s | %s | %s\n' "$(date -u +%H:%MZ)" mq-feeder PMAT-5843 "$1" >> "$INBOX"; } if [ "${1:-}" = --self-test ]; then fail=0 @@ -34,6 +45,8 @@ if [ "${1:-}" = --self-test ]; then eq 'non-main base skipped' "$(pick "$(mk 0 "$(pr 4 false feat CLEAN null)")")" '' eq 'already armed skipped' "$(pick "$(mk 0 "$(pr 5 false main CLEAN '{"enabledAt":"x"}')")")" '' eq 'no PRs' "$(pick "$(mk 0 '')")" '' + gate_paths $'docs/x.md\nscripts/check_foo.sh' || { echo "FAIL gate_paths check_"; fail=1; } + gate_paths $'docs/x.md\ncrates/a/src/lib.rs' && { echo "FAIL gate_paths non-gate"; fail=1; } [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" fi [ "${1:-}" = --dry-run ] && dry=1 @@ -51,11 +64,13 @@ json=$(gh api graphql -f query="$Q" -f o="${REPO%/*}" -f n="${REPO#*/}" 2>/dev/n cands=$(pick "$json") || exit 2 [ "$cands" = QUEUE_BUSY ] && exit 0 [ -n "$cands" ] || exit 0 -n=0 +n=0 why="" for pr in $cands; do grep -qx "$pr" "$STATE/kicked" 2>/dev/null && continue # kicked by the queue once: the owner must push a fix and clear it n=$((n + 1)); [ "$n" -le "$TRIES" ] || break if [ "$dry" = 1 ]; then echo "would arm #$pr"; exit 0; fi + head=$(gh pr view "$pr" --repo "$REPO" --json headRefOid --jq .headRefOid 2>/dev/null || true) + if [ -n "$head" ] && r=$(needs_seats "$pr" "$head"); then why="$why #$pr: $r;"; printf '%s skip #%s %s\n' "$(date +%s)" "$pr" "$r" >> "$STATE/acts.log"; continue; fi out=$(bash "$ARM" "$pr" 2>&1); rc=$? if [ "$rc" -eq 0 ]; then sleep "${MQ_FEEDER_RECHECK:-30}" @@ -63,8 +78,8 @@ for pr in $cands; do case "$st" in KICKED*) echo "$pr" >> "$STATE/kicked"; inbox "queue was empty: enqueued #$pr, then $st within ${MQ_FEEDER_RECHECK:-30}s; skipped until removed from $STATE/kicked" ;; *) inbox "queue was empty: enqueued oldest green PR #$pr ($st)" ;; esac exit 0 fi - [ "$rc" -eq 3 ] && continue # refused on that PR (guard-tree not green on its head): try the next + if [ "$rc" -eq 3 ]; then why="$why #$pr: $(printf '%s' "$out" | grep -m1 -E "^refuse" | cut -c1-110);"; printf '%s skip #%s %s\n' "$(date +%s)" "$pr" "$(printf '%s' "$out" | grep -m1 -E "^refuse" | cut -c1-110)" >> "$STATE/acts.log"; continue; fi inbox "queue empty, arm of #$pr failed rc=$rc: $(printf '%s' "$out" | tail -n 1 | cut -c1-120)"; exit 0 done -inbox "queue empty, no PR enqueued: $((n > TRIES ? TRIES : n)) CLEAN candidate(s) refused by arm_pr_automerge.sh" +inbox "queue empty, no PR enqueued: $((n > TRIES ? TRIES : n)) CLEAN candidate(s) refused:$why" exit 0 From 9df527f408fbfdc9e851072b7fc3ae382784e4e5 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 12:56:47 +0200 Subject: [PATCH 5/9] ci(mq): dry-run runs the skip checks too (PMAT-5843) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index d7b7bcfc5c..a9c5bc4800 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -68,9 +68,9 @@ n=0 why="" for pr in $cands; do grep -qx "$pr" "$STATE/kicked" 2>/dev/null && continue # kicked by the queue once: the owner must push a fix and clear it n=$((n + 1)); [ "$n" -le "$TRIES" ] || break - if [ "$dry" = 1 ]; then echo "would arm #$pr"; exit 0; fi head=$(gh pr view "$pr" --repo "$REPO" --json headRefOid --jq .headRefOid 2>/dev/null || true) if [ -n "$head" ] && r=$(needs_seats "$pr" "$head"); then why="$why #$pr: $r;"; printf '%s skip #%s %s\n' "$(date +%s)" "$pr" "$r" >> "$STATE/acts.log"; continue; fi + if [ "$dry" = 1 ]; then echo "would arm #$pr"; exit 0; fi out=$(bash "$ARM" "$pr" 2>&1); rc=$? if [ "$rc" -eq 0 ]; then sleep "${MQ_FEEDER_RECHECK:-30}" From 4c9c9b27c0278a2ac1c566df94d615ca25697338 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 12:57:22 +0200 Subject: [PATCH 6/9] roadmap: PMAT-5846 stale guard-tree name in arm_pr_automerge.sh Co-Authored-By: Claude Sonnet 5.5 --- docs/roadmaps/entries/PMAT-5846.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/roadmaps/entries/PMAT-5846.yaml diff --git a/docs/roadmaps/entries/PMAT-5846.yaml b/docs/roadmaps/entries/PMAT-5846.yaml new file mode 100644 index 0000000000..e0f6dd5bc1 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5846.yaml @@ -0,0 +1,19 @@ +- id: PMAT-5846 + github_issue: 5846 + item_type: task + title: 'arm_pr_automerge.sh: stale guard-tree check name (default must be x86-main)' + status: planned + priority: high + assigned_to: null + created: 2026-10-21T10:13:29Z + updated: 2026-10-21T10:13:29Z + spec: null + acceptance_criteria: + - 'default GUARD_CHECK_NAME names a check run that exists on PR heads; --self-test fixtures updated' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5510 From ba24a06ea10ff3cbc206f5184e092404e7eae854 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 13:14:04 +0200 Subject: [PATCH 7/9] fix(mq-feeder): count seat COMMENT reviews naming the head sha (one GH account cannot APPROVE) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index a9c5bc4800..feac0f73c9 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -28,9 +28,9 @@ gate_paths() { grep -qE '^(\.github/workflows/|ci/sections\.yml|scripts/(check_| needs_seats() { # PR HEAD -> prints reason and returns 0 when a gate-content PR lacks 2 seats bound to HEAD local f s; f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || return 1 gate_paths "$f" || return 1 - s=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq "[.[]|select(.body|test(\"APPROVE @'$2'\"))]|length" 2>/dev/null || echo 0) + s=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq "[.[]|select(.body|test(\"[Ss]eat\"))|select(.body|contains(\"$2\"))]|length" 2>/dev/null || echo 0) [ "${s:-0}" -ge 2 ] && return 1 - echo "needs seats (gate content, ${s:-0}/2 APPROVE bound to ${2:0:10})" + echo "needs seats (gate content, ${s:-0}/2 seat reviews bound to ${2:0:10})" } inbox() { mkdir -p "$STATE"; printf '%s %s\n' "$(date +%s)" "$1" >> "$STATE/acts.log"; printf '%s | %s | %s | %s\n' "$(date -u +%H:%MZ)" mq-feeder PMAT-5843 "$1" >> "$INBOX"; } if [ "${1:-}" = --self-test ]; then From 602ffa1b197b6f78df37a2dfa1a0afcaa97dc7ef Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 13:14:39 +0200 Subject: [PATCH 8/9] fix(mq-feeder): bind seat reviews by commit_id (bodies cite short shas) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index feac0f73c9..ca190017f3 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -28,7 +28,7 @@ gate_paths() { grep -qE '^(\.github/workflows/|ci/sections\.yml|scripts/(check_| needs_seats() { # PR HEAD -> prints reason and returns 0 when a gate-content PR lacks 2 seats bound to HEAD local f s; f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || return 1 gate_paths "$f" || return 1 - s=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq "[.[]|select(.body|test(\"[Ss]eat\"))|select(.body|contains(\"$2\"))]|length" 2>/dev/null || echo 0) + s=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq "[.[]|select(.body|test(\"[Ss]eat\"))|select(.commit_id==\"$2\")]|length" 2>/dev/null || echo 0) [ "${s:-0}" -ge 2 ] && return 1 echo "needs seats (gate content, ${s:-0}/2 seat reviews bound to ${2:0:10})" } From 915765011adfb333a9a2db78caa386b470557b44 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 13:16:46 +0200 Subject: [PATCH 9/9] feat(mq-feeder): skip PRs with merge commits (REBASE needs clean commits), named in the skip line Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index ca190017f3..e13b82667a 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -70,6 +70,8 @@ for pr in $cands; do n=$((n + 1)); [ "$n" -le "$TRIES" ] || break head=$(gh pr view "$pr" --repo "$REPO" --json headRefOid --jq .headRefOid 2>/dev/null || true) if [ -n "$head" ] && r=$(needs_seats "$pr" "$head"); then why="$why #$pr: $r;"; printf '%s skip #%s %s\n' "$(date +%s)" "$pr" "$r" >> "$STATE/acts.log"; continue; fi + mc=$(gh api "repos/$REPO/pulls/$pr/commits" --paginate --jq '[.[]|select((.parents|length)>1)]|length' 2>/dev/null | paste -sd+ | bc 2>/dev/null || true) + if [ "${mc:-0}" -gt 0 ] 2>/dev/null; then r="has $mc merge commit(s); REBASE needs clean commits (operator 07:57Z)"; why="$why #$pr: $r;"; printf '%s skip #%s %s\n' "$(date +%s)" "$pr" "$r" >> "$STATE/acts.log"; continue; fi if [ "$dry" = 1 ]; then echo "would arm #$pr"; exit 0; fi out=$(bash "$ARM" "$pr" 2>&1); rc=$? if [ "$rc" -eq 0 ]; then