From dc86c46858e2bb8bec102edf001e5da3098a7d1a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 17:37:21 +0200 Subject: [PATCH 01/12] =?UTF-8?q?ci(mq):=20MQ=20feeder=20=E2=80=94=20enque?= =?UTF-8?q?ue=20the=20oldest=20green=20PR=20when=20the=20merge=20queue=20i?= =?UTF-8?q?s=20empty=20(PMAT-5843,=20PMAT-5846)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-cut of #5845 as one commit off main (9 commits squashed, tree identical). scripts/mq_feeder.sh plus 5-min user timer and an hourly :45 summary timer. Judges x86-main, names each skip with its reason, counts seat COMMENT reviews bound by commit_id, skips PRs with merge commits. Co-Authored-By: Claude Sonnet 5.5 --- docs/roadmaps/entries/PMAT-5843.yaml | 19 +++++ docs/roadmaps/entries/PMAT-5846.yaml | 19 +++++ scripts/mq_feeder.sh | 87 +++++++++++++++++++++++ scripts/systemd/mq-feeder-summary.service | 5 ++ scripts/systemd/mq-feeder-summary.timer | 6 ++ scripts/systemd/mq-feeder.service | 5 ++ scripts/systemd/mq-feeder.timer | 8 +++ 7 files changed, 149 insertions(+) create mode 100644 docs/roadmaps/entries/PMAT-5843.yaml create mode 100644 docs/roadmaps/entries/PMAT-5846.yaml create mode 100755 scripts/mq_feeder.sh create mode 100644 scripts/systemd/mq-feeder-summary.service create mode 100644 scripts/systemd/mq-feeder-summary.timer 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/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 diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh new file mode 100755 index 0000000000..e13b82667a --- /dev/null +++ b/scripts/mq_feeder.sh @@ -0,0 +1,87 @@ +#!/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}" +# 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}}}}}' +# 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" +} +# 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(\"[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})" +} +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"; } + 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 '')")" '' + 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 +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 +cands=$(pick "$json") || exit 2 +[ "$cands" = QUEUE_BUSY ] && exit 0 +[ -n "$cands" ] || exit 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 + 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 + 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 + 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:$why" +exit 0 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 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 83bc930256e0ea7517b8b6fa89d57324a53f7b9e Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 17:44:16 +0200 Subject: [PATCH 02/12] fix(mq-feeder): units run the installed path %h/.local/libexec/mq-feeder, not a worktree (203/EXEC) Co-Authored-By: Claude Sonnet 5.5 --- scripts/systemd/mq-feeder-summary.service | 2 +- scripts/systemd/mq-feeder.service | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/systemd/mq-feeder-summary.service b/scripts/systemd/mq-feeder-summary.service index fef22b25d4..077884f34d 100644 --- a/scripts/systemd/mq-feeder-summary.service +++ b/scripts/systemd/mq-feeder-summary.service @@ -2,4 +2,4 @@ Description=mq-feeder hourly summary line (PMAT-5843) [Service] Type=oneshot -ExecStart=/mnt/nvme-raid0/wt/w5382-mqf/scripts/mq_feeder.sh --summary +ExecStart=%h/.local/libexec/mq-feeder/mq_feeder.sh --summary diff --git a/scripts/systemd/mq-feeder.service b/scripts/systemd/mq-feeder.service index 6282f5e7c5..14af345c30 100644 --- a/scripts/systemd/mq-feeder.service +++ b/scripts/systemd/mq-feeder.service @@ -2,4 +2,4 @@ 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 +ExecStart=%h/.local/libexec/mq-feeder/mq_feeder.sh From 38b290a66f59a2c3df6356f77581eea718e8c7d5 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 17:52:46 +0200 Subject: [PATCH 03/12] fix(mq-feeder): triage quorum blockers on 83bc930256 (PMAT-5843) - gate_paths covers ci/, .github/, scripts/ci_*, ci/, lib/, guard_tree, feeder+units, Cargo/Makefile/toolchain/nextest/deny/clippy - needs_seats fails closed: unreadable files/reviews or empty head = needs seats; gate PRs need 2 distinct APPROVE seats bound to head, a 3/3 quorum receipt, and no request-changes - kicked keyed by PR+head, so a pushed fix clears it - refuses (rc 2) when the arm script is absent; scripts/mq_feeder_install.sh installs both scripts to the unit path - roadmap.yaml regenerated Co-Authored-By: Claude Sonnet 5.5 --- docs/roadmaps/roadmap.yaml | 38 ++++++++++++++++++++++++++++++++++++ scripts/mq_feeder.sh | 35 ++++++++++++++++++++++----------- scripts/mq_feeder_install.sh | 14 +++++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100755 scripts/mq_feeder_install.sh diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index ef47935f92..373f966bad 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -25359,3 +25359,41 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 5510 +- 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 +- 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 diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index e13b82667a..810f2485e0 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -23,14 +23,25 @@ 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 FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). +# Wide on purpose: anything the PR gate, merge queue, guard sweep or this feeder runs or reads. +gate_paths() { grep -qE '^(\.github/|ci/|scripts/(check_|ci_|ci/|lib/|guard_tree|arm_pr_automerge|mq_feeder|systemd/)|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } +# needs_seats PR HEAD -> prints the reason and returns 0 unless the PR is non-gate OR holds a verified 3/3 quorum +# receipt plus 2 distinct APPROVE seat reviews bound to HEAD and no REQUEST-CHANGES bound to HEAD. FAILS CLOSED: +# an unreadable file list or review list, or an empty HEAD, means "needs seats". +needs_seats() { + local f rv a rc q + [ -n "$2" ] || { echo "needs seats (head unreadable)"; return 0; } + f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || { echo "needs seats (files unreadable)"; return 0; } + [ -n "$f" ] || { echo "needs seats (file list empty)"; return 0; } gate_paths "$f" || return 1 - 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})" + # reviews bound to HEAD (commit_id), COMMENTED/APPROVED only; one body per line, newlines folded + rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } + rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST-CHANGES|REQUEST CHANGES' <<<"$rv") + a=$(grep -iE '^(COMMENTED|APPROVED)' <<<"$rv" | grep -iE '[Ss]eat' | grep -iE 'APPROVE' | grep -viE 'REQUEST-?[ _]?CHANGES|NOT AGREED' | sort -u | wc -l) + q=$(grep -ciE 'quorum[^0-9]*3/3|3/3[^a-z]*quorum|AGREED 3/3' <<<"$rv") + { [ "${rc:-0}" -eq 0 ] && [ "${a:-0}" -ge 2 ] && [ "${q:-0}" -ge 1 ]; } && return 1 + echo "needs seats (gate content; bound to ${2:0:10}: $a/2 distinct APPROVE seats, quorum 3/3 receipt $q/1, $rc request-changes)" } 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 @@ -47,6 +58,7 @@ if [ "${1:-}" = --self-test ]; then 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; } + for p in ci/explicit-test-commands.d/1.cmd scripts/guard_tree.sh scripts/ci_mg_reuse.sh scripts/lib/x.sh scripts/ci/x.sh scripts/mq_feeder.sh scripts/systemd/mq-feeder.service Cargo.toml Makefile ci/vendor/sovereign-ci.yml; do gate_paths "$p" || { echo "FAIL gate_paths $p"; fail=1; }; done [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" fi [ "${1:-}" = --dry-run ] && dry=1 @@ -56,6 +68,7 @@ if [ "${1:-}" = --summary ]; then inbox "summary 1h: $t ticks, $e enqueued, $k kicked"; exit 0 fi mkdir -p "$STATE" || exit 2 +[ -x "$ARM" ] || { inbox "mq-feeder: arm script $ARM missing or not executable (install both scripts with scripts/mq_feeder_install.sh)"; 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 @@ -66,10 +79,10 @@ cands=$(pick "$json") || exit 2 [ -n "$cands" ] || exit 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 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 + grep -qx "$pr $head" "$STATE/kicked" 2>/dev/null && continue # kicked at THIS head: a pushed fix is a new head and clears it + n=$((n + 1)); [ "$n" -le "$TRIES" ] || break + if 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 @@ -77,7 +90,7 @@ for pr in $cands; do 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 + case "$st" in KICKED*) echo "$pr $head" >> "$STATE/kicked"; inbox "queue was empty: enqueued #$pr, then $st within ${MQ_FEEDER_RECHECK:-30}s; skipped at this head until a new push" ;; *) inbox "queue was empty: enqueued oldest green PR #$pr ($st)" ;; esac exit 0 fi 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 diff --git a/scripts/mq_feeder_install.sh b/scripts/mq_feeder_install.sh new file mode 100755 index 0000000000..f5acf84c06 --- /dev/null +++ b/scripts/mq_feeder_install.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# mq_feeder_install.sh -- PMAT-5843. Install the feeder AND the arm script it calls into the path the user units run +# (%h/.local/libexec/mq-feeder). The feeder resolves arm_pr_automerge.sh next to itself, so both must be there. +# Run from a checkout of origin/main; re-run after either script changes (forjar converges the same files on the hosts). +# Usage: mq_feeder_install.sh [DEST] +set -euo pipefail +src="$(cd "$(dirname "$0")" && pwd)" +dest="${1:-$HOME/.local/libexec/mq-feeder}" +mkdir -p "$dest" +for s in mq_feeder.sh arm_pr_automerge.sh; do + install -m 0755 "$src/$s" "$dest/$s" +done +sha256sum "$dest/mq_feeder.sh" "$dest/arm_pr_automerge.sh" > "$dest/INSTALLED.sha256" +echo "installed to $dest" From a1b0efc624ab15c963a936199f82e441b28bd781 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:03:35 +0200 Subject: [PATCH 04/12] fix(mq-feeder): seats dedupe by name, quorum receipt negation guard, gate_paths = all of scripts/, RED/GREEN selftests (PMAT-5843) Triage quorum 3/3 REQUEST-CHANGES at 38b290a66f. Selftests stub gh and cover armable, repost, negated quorum, 2/3, request-changes, no receipt, unreadable files, non-gate; three mutations turn them RED. DET002 disable on the runtime window (infra-66). Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index 810f2485e0..daf25b22e7 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -24,8 +24,18 @@ pick() { 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). -# Wide on purpose: anything the PR gate, merge queue, guard sweep or this feeder runs or reads. -gate_paths() { grep -qE '^(\.github/|ci/|scripts/(check_|ci_|ci/|lib/|guard_tree|arm_pr_automerge|mq_feeder|systemd/)|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } +# Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. +gate_paths() { grep -qE '^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } +# seat_names REVIEWS -> distinct seat names (one per line) of COMMENTED/APPROVED reviews that read "seat ... APPROVE" +# and carry no REQUEST-CHANGES / NOT AGREED / NOT text. Dedupes by seat NAME, so a repost of one seat counts once. +seat_names() { + grep -iE '^(COMMENTED|APPROVED)' <<<"$1" | grep -iE 'seat' | grep -iE 'APPROVE' | grep -viE 'REQUEST-?[ _]?CHANGES|NOT[ -]+(AGREED|APPROVE)|\bNOT\b' \ + | sed -nE 's/.*[Ss][Ee][Aa][Tt][: ]+([A-Za-z0-9_.@-]+).*/\1/p' | tr 'A-Z' 'a-z' | sort -u +} +# quorum_receipts REVIEWS -> count of reviews stating "quorum 3/3" / "AGREED 3/3" with no negation and no other n/3 figure +quorum_receipts() { + grep -iE '^(COMMENTED|APPROVED)' <<<"$1" | grep -iE 'quorum[^0-9]{0,12}3/3|AGREED 3/3' | grep -viE '\bNOT\b|REQUEST-?[ _]?CHANGES|[0-2]/3' | wc -l +} # needs_seats PR HEAD -> prints the reason and returns 0 unless the PR is non-gate OR holds a verified 3/3 quorum # receipt plus 2 distinct APPROVE seat reviews bound to HEAD and no REQUEST-CHANGES bound to HEAD. FAILS CLOSED: # an unreadable file list or review list, or an empty HEAD, means "needs seats". @@ -38,10 +48,10 @@ needs_seats() { # reviews bound to HEAD (commit_id), COMMENTED/APPROVED only; one body per line, newlines folded rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST-CHANGES|REQUEST CHANGES' <<<"$rv") - a=$(grep -iE '^(COMMENTED|APPROVED)' <<<"$rv" | grep -iE '[Ss]eat' | grep -iE 'APPROVE' | grep -viE 'REQUEST-?[ _]?CHANGES|NOT AGREED' | sort -u | wc -l) - q=$(grep -ciE 'quorum[^0-9]*3/3|3/3[^a-z]*quorum|AGREED 3/3' <<<"$rv") + a=$(seat_names "$rv" | wc -l) + q=$(quorum_receipts "$rv") { [ "${rc:-0}" -eq 0 ] && [ "${a:-0}" -ge 2 ] && [ "${q:-0}" -ge 1 ]; } && return 1 - echo "needs seats (gate content; bound to ${2:0:10}: $a/2 distinct APPROVE seats, quorum 3/3 receipt $q/1, $rc request-changes)" + echo "needs seats (gate content; bound to ${2:0:10}: $a/2 distinct named APPROVE seats, quorum 3/3 receipt $q/1, $rc request-changes)" } 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 @@ -59,11 +69,27 @@ if [ "${1:-}" = --self-test ]; then 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; } for p in ci/explicit-test-commands.d/1.cmd scripts/guard_tree.sh scripts/ci_mg_reuse.sh scripts/lib/x.sh scripts/ci/x.sh scripts/mq_feeder.sh scripts/systemd/mq-feeder.service Cargo.toml Makefile ci/vendor/sovereign-ci.yml; do gate_paths "$p" || { echo "FAIL gate_paths $p"; fail=1; }; done + # gate_paths: scripts the explicit-test fragments run are gate content (triage 38b290a66f) + for p in scripts/contracts_gate.sh scripts/mutate_foo_guard.sh scripts/lib_baseline_ratchet.sh scripts/pv_bin.sh scripts/pmat_bin.sh scripts/tests/x.sh; do gate_paths "$p" || { echo "FAIL gate_paths $p"; fail=1; }; done + # needs_seats with a stubbed gh: REVIEWS is "STATEbody" lines, FILES the PR file list + gh() { case "$2" in */files) [ "${CASE:-}" = filefail ] && return 1; printf '%s\n' "${FILES:-scripts/ci_x.sh}";; */reviews) printf '%b' "${REVIEWS:-}";; esac; } + ns() { REPO=r needs_seats 1 H >/dev/null; echo $?; } # 0 = needs seats (not armable), 1 = armable + Q3='COMMENTED\tquorum 3/3 AGREED\n' + eq 'seats: two named + 3/3 -> armable' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n$Q3" ns)" 1 + eq 'seats: repost of one seat (same name, different text) -> needs' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w1 APPROVE again\n$Q3" ns)" 0 + eq 'seats: negated quorum "NOT 3/3" + repost -> needs' "$(REVIEWS='COMMENTED\tSeat (a) APPROVE; quorum NOT 3/3\nCOMMENTED\tSeat (a) APPROVE; quorum NOT 3/3 \nCOMMENTED\tSeat (b) APPROVE\n' ns)" 0 + eq 'seats: two seats + only a negated quorum receipt -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\nCOMMENTED\tquorum NOT 3/3\n' ns)" 0 + eq 'seats: quorum 2/3 -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\nCOMMENTED\tquorum 2/3 AGREED\n' ns)" 0 + eq 'seats: request-changes bound to head -> needs' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n${Q3}CHANGES_REQUESTED\tseat w3 RC\n" ns)" 0 + eq 'seats: no 3/3 receipt -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' ns)" 0 + eq 'seats: files unreadable -> needs (fail closed)' "$(CASE=filefail ns)" 0 + eq 'seats: non-gate PR -> armable' "$(FILES=docs/x.md ns)" 1 + unset -f gh [ "$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)) + mkdir -p "$STATE"; since=$(($(date +%s) - 3600)) # bashrs disable-line=DET002 (runtime window, not a build stamp) 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 From 1539bf88d6c5e0e497a62c6c13a0c49af0f5bc35 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:08:33 +0200 Subject: [PATCH 05/12] fix(mq-feeder): positive anchors for seat and quorum receipt, case table (PMAT-5843) Triage RC at a1b0efc624: quorum_receipts counted 'pending|required|missing', 'need quorum 3/3' and a bare 'AGREED 3/3'; seat_names gave 0 names for 'Seat (a) APPROVE' and the name 'approve' for 'Seat APPROVE'. Now only 'seat APPROVE' and 'quorum 3/3 AGREED|APPROVED' count, with a wider negation list. 14 new rows; three mutations turn them RED. Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index daf25b22e7..8c1760f21f 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -26,15 +26,20 @@ pick() { # gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). # Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. gate_paths() { grep -qE '^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } -# seat_names REVIEWS -> distinct seat names (one per line) of COMMENTED/APPROVED reviews that read "seat ... APPROVE" -# and carry no REQUEST-CHANGES / NOT AGREED / NOT text. Dedupes by seat NAME, so a repost of one seat counts once. +# Review format the feeder accepts (positive anchors only; anything else = no seat / no receipt): +# seat: "seat APPROVE" ( a word that is not APPROVE/APPROVED) +# quorum: "quorum 3/3 AGREED" (or APPROVED); no negation word in the same body +# seat_names REVIEWS -> distinct seat names (one per line); dedupes by NAME, so a repost of one seat counts once. seat_names() { - grep -iE '^(COMMENTED|APPROVED)' <<<"$1" | grep -iE 'seat' | grep -iE 'APPROVE' | grep -viE 'REQUEST-?[ _]?CHANGES|NOT[ -]+(AGREED|APPROVE)|\bNOT\b' \ - | sed -nE 's/.*[Ss][Ee][Aa][Tt][: ]+([A-Za-z0-9_.@-]+).*/\1/p' | tr 'A-Z' 'a-z' | sort -u + grep -iE '^(COMMENTED|APPROVED)' <<<"$1" \ + | grep -viE '\b(NOT|NO|NEED|NEEDS|PENDING|REQUIRED|MISSING|AWAITING|WITHOUT|UNTIL|IF)\b|REQUEST-?[ _]?CHANGES' \ + | sed -nE 's/.*[Ss][Ee][Aa][Tt][: ]+([A-Za-z0-9_.@-]+)[ :,;-]+[Aa][Pp][Pp][Rr][Oo][Vv][Ee].*/\1/p' \ + | tr 'A-Z' 'a-z' | grep -vxE 'approve|approved' | sort -u } -# quorum_receipts REVIEWS -> count of reviews stating "quorum 3/3" / "AGREED 3/3" with no negation and no other n/3 figure +# quorum_receipts REVIEWS -> count of reviews with the positive anchor "quorum 3/3 AGREED|APPROVED" and no negation/other n/3 quorum_receipts() { - grep -iE '^(COMMENTED|APPROVED)' <<<"$1" | grep -iE 'quorum[^0-9]{0,12}3/3|AGREED 3/3' | grep -viE '\bNOT\b|REQUEST-?[ _]?CHANGES|[0-2]/3' | wc -l + grep -iE '^(COMMENTED|APPROVED)' <<<"$1" | grep -iE 'quorum[: ]+3/3[ :,;-]+(AGREED|APPROVED?)' \ + | grep -viE '\b(NOT|NO|NEED|NEEDS|PENDING|REQUIRED|MISSING|AWAITING|WITHOUT|UNTIL|IF)\b|REQUEST-?[ _]?CHANGES|[0-2]/3' | wc -l } # needs_seats PR HEAD -> prints the reason and returns 0 unless the PR is non-gate OR holds a verified 3/3 quorum # receipt plus 2 distinct APPROVE seat reviews bound to HEAD and no REQUEST-CHANGES bound to HEAD. FAILS CLOSED: @@ -84,6 +89,18 @@ if [ "${1:-}" = --self-test ]; then eq 'seats: no 3/3 receipt -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' ns)" 0 eq 'seats: files unreadable -> needs (fail closed)' "$(CASE=filefail ns)" 0 eq 'seats: non-gate PR -> armable' "$(FILES=docs/x.md ns)" 1 + # round 3 (a1b0efc624): positive anchors; each row fails under the matching mutation + S2='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' + for q in 'quorum 3/3 pending' 'quorum 3/3 required' 'quorum 3/3 missing' 'need quorum 3/3' 'AGREED 3/3' 'quorum 3/3' 'quorum NOT 3/3' 'quorum 3/3 AGREED if seats'; do + eq "quorum row '$q' is not a receipt" "$(REVIEWS="${S2}COMMENTED\t$q\n" ns)" 0 + done + eq 'quorum row "quorum 3/3 APPROVED" is a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 APPROVED\n" ns)" 1 + eq 'seat "Seat APPROVE" (no name) x2 -> needs' "$(REVIEWS="COMMENTED\tSeat APPROVE\nCOMMENTED\tSeat APPROVE\n$Q3" ns)" 0 + eq 'seat "Seat (a) APPROVE" x2 (no name) -> needs' "$(REVIEWS="COMMENTED\tSeat (a) APPROVE\nCOMMENTED\tSeat (b) APPROVE\n$Q3" ns)" 0 + eq 'seat "seat w1 APPROVE" twice with NOT 3/3 repost -> needs (named seat, not vacuous)' "$(REVIEWS='COMMENTED\tseat w1 APPROVE; quorum NOT 3/3\nCOMMENTED\tseat w1 APPROVE; quorum NOT 3/3 \n' ns)" 0 + eq 'seat names: two named' "$(seat_names "$(printf 'COMMENTED\tseat w1 APPROVE\nCOMMENTED\tSeat: W2 APPROVE\nCOMMENTED\tseat w1 APPROVE again')" | tr '\n' ,)" w1,w2, + eq 'seat "seat APPROVE APPROVE" yields no name' "$(seat_names "$(printf 'COMMENTED\tseat APPROVE APPROVE')")" '' + eq 'seat "seat w1 reviewing APPROVE" (no direct anchor) yields no name' "$(seat_names "$(printf 'COMMENTED\tseat w1 reviewing APPROVE')")" '' unset -f gh [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" fi From 194142497e307d16af53ec286e6cc9e7e6b2df68 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:27:58 +0200 Subject: [PATCH 06/12] fix(mq-feeder): verdict direction and anchored seat/receipt lines (PMAT-5843) Triage/aprender-57 RC at 1539bf88d6. B1: a receipt now needs 'quorum 3/3 [AGREED] PASS|APPROVE(D)'; FAIL/REJECT/BLOCK/CANNOT and friends are negations and count as request-changes. B2: seat and receipt lines are anchored at the body start and must end at end/punctuation/@sha, so quoted or carried-over text, 'APPROVE once X lands' and trailing prose never count. 27 new case-table rows; mutations (loose END, dropped negations) turn them RED. Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index 8c1760f21f..0a85d890c0 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -26,20 +26,22 @@ pick() { # gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). # Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. gate_paths() { grep -qE '^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } -# Review format the feeder accepts (positive anchors only; anything else = no seat / no receipt): -# seat: "seat APPROVE" ( a word that is not APPROVE/APPROVED) -# quorum: "quorum 3/3 AGREED" (or APPROVED); no negation word in the same body +# Review format the feeder accepts. Positive anchors AT THE START OF THE BODY; one review = one seat OR one receipt: +# seat: "seat APPROVE" ( a word that is not APPROVE/APPROVED) +# quorum: "quorum 3/3 [AGREED] PASS" (or APPROVE/APPROVED); PASS/APPROVE is the verdict direction +# followed by end of body, punctuation, or " @". A body with a quote mark, a negation/deferral/failure word or a +# carried-over marker anywhere is rejected, so quoted evidence and "APPROVE once X lands" never count. +NEG='\b(NOT|NO|NEED|NEEDS|PENDING|REQUIRED|MISSING|AWAITING|WITHOUT|UNTIL|IF|ONCE|AFTER|WHEN|UNLESS|CANNOT|FAIL|FAILS|FAILED|REJECT|REJECTED|BLOCK|BLOCKS|BLOCKING|CARRIED|PREVIOUS|EARLIER|PRIOR)\b|REQUEST[-_ ]?CHANGES|[0-2]/3|"'"'"'`' +END='([.;:,!]|[[:space:]]+@[0-9a-f]{7,40}|[[:space:]]*$)' # seat_names REVIEWS -> distinct seat names (one per line); dedupes by NAME, so a repost of one seat counts once. seat_names() { - grep -iE '^(COMMENTED|APPROVED)' <<<"$1" \ - | grep -viE '\b(NOT|NO|NEED|NEEDS|PENDING|REQUIRED|MISSING|AWAITING|WITHOUT|UNTIL|IF)\b|REQUEST-?[ _]?CHANGES' \ - | sed -nE 's/.*[Ss][Ee][Aa][Tt][: ]+([A-Za-z0-9_.@-]+)[ :,;-]+[Aa][Pp][Pp][Rr][Oo][Vv][Ee].*/\1/p' \ + grep -iE "^(COMMENTED|APPROVED)"$'\t'"seat[: ]+[A-Za-z0-9_.@-]+[ :,;-]+APPROVED?${END}" <<<"$1" | grep -viE "$NEG" \ + | sed -nE 's/^[A-Z]+\t[Ss][Ee][Aa][Tt][: ]+([A-Za-z0-9_.@-]+)[ :,;-]+[Aa][Pp][Pp].*/\1/p' \ | tr 'A-Z' 'a-z' | grep -vxE 'approve|approved' | sort -u } -# quorum_receipts REVIEWS -> count of reviews with the positive anchor "quorum 3/3 AGREED|APPROVED" and no negation/other n/3 +# quorum_receipts REVIEWS -> count of reviews that are exactly a "quorum 3/3 [AGREED] PASS|APPROVE(D)" receipt (see above) quorum_receipts() { - grep -iE '^(COMMENTED|APPROVED)' <<<"$1" | grep -iE 'quorum[: ]+3/3[ :,;-]+(AGREED|APPROVED?)' \ - | grep -viE '\b(NOT|NO|NEED|NEEDS|PENDING|REQUIRED|MISSING|AWAITING|WITHOUT|UNTIL|IF)\b|REQUEST-?[ _]?CHANGES|[0-2]/3' | wc -l + grep -iE "^(COMMENTED|APPROVED)"$'\t'"quorum[: ]+3/3[ :,;-]+(AGREED[ :,;-]+)?(PASS|APPROVED?)${END}" <<<"$1" | grep -viE "$NEG" | wc -l } # needs_seats PR HEAD -> prints the reason and returns 0 unless the PR is non-gate OR holds a verified 3/3 quorum # receipt plus 2 distinct APPROVE seat reviews bound to HEAD and no REQUEST-CHANGES bound to HEAD. FAILS CLOSED: @@ -52,7 +54,7 @@ needs_seats() { gate_paths "$f" || return 1 # reviews bound to HEAD (commit_id), COMMENTED/APPROVED only; one body per line, newlines folded rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } - rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST-CHANGES|REQUEST CHANGES' <<<"$rv") + rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b' <<<"$rv") a=$(seat_names "$rv" | wc -l) q=$(quorum_receipts "$rv") { [ "${rc:-0}" -eq 0 ] && [ "${a:-0}" -ge 2 ] && [ "${q:-0}" -ge 1 ]; } && return 1 @@ -79,7 +81,7 @@ if [ "${1:-}" = --self-test ]; then # needs_seats with a stubbed gh: REVIEWS is "STATEbody" lines, FILES the PR file list gh() { case "$2" in */files) [ "${CASE:-}" = filefail ] && return 1; printf '%s\n' "${FILES:-scripts/ci_x.sh}";; */reviews) printf '%b' "${REVIEWS:-}";; esac; } ns() { REPO=r needs_seats 1 H >/dev/null; echo $?; } # 0 = needs seats (not armable), 1 = armable - Q3='COMMENTED\tquorum 3/3 AGREED\n' + Q3='COMMENTED\tquorum 3/3 AGREED PASS\n' eq 'seats: two named + 3/3 -> armable' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n$Q3" ns)" 1 eq 'seats: repost of one seat (same name, different text) -> needs' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w1 APPROVE again\n$Q3" ns)" 0 eq 'seats: negated quorum "NOT 3/3" + repost -> needs' "$(REVIEWS='COMMENTED\tSeat (a) APPROVE; quorum NOT 3/3\nCOMMENTED\tSeat (a) APPROVE; quorum NOT 3/3 \nCOMMENTED\tSeat (b) APPROVE\n' ns)" 0 @@ -91,7 +93,7 @@ if [ "${1:-}" = --self-test ]; then eq 'seats: non-gate PR -> armable' "$(FILES=docs/x.md ns)" 1 # round 3 (a1b0efc624): positive anchors; each row fails under the matching mutation S2='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' - for q in 'quorum 3/3 pending' 'quorum 3/3 required' 'quorum 3/3 missing' 'need quorum 3/3' 'AGREED 3/3' 'quorum 3/3' 'quorum NOT 3/3' 'quorum 3/3 AGREED if seats'; do + for q in 'quorum 3/3 pending' 'quorum 3/3 required' 'quorum 3/3 missing' 'need quorum 3/3' 'AGREED 3/3' 'quorum 3/3' 'quorum NOT 3/3'; do eq "quorum row '$q' is not a receipt" "$(REVIEWS="${S2}COMMENTED\t$q\n" ns)" 0 done eq 'quorum row "quorum 3/3 APPROVED" is a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 APPROVED\n" ns)" 1 @@ -101,6 +103,19 @@ if [ "${1:-}" = --self-test ]; then eq 'seat names: two named' "$(seat_names "$(printf 'COMMENTED\tseat w1 APPROVE\nCOMMENTED\tSeat: W2 APPROVE\nCOMMENTED\tseat w1 APPROVE again')" | tr '\n' ,)" w1,w2, eq 'seat "seat APPROVE APPROVE" yields no name' "$(seat_names "$(printf 'COMMENTED\tseat APPROVE APPROVE')")" '' eq 'seat "seat w1 reviewing APPROVE" (no direct anchor) yields no name' "$(seat_names "$(printf 'COMMENTED\tseat w1 reviewing APPROVE')")" '' + # round 4 (1539bf88d6): verdict direction (B1) and quoted/carried-over text (B2) must not count. Each row: two real + # seats plus ONLY the bad quorum line -> needs; or ONE real seat plus the bad seat line + a real receipt -> needs. + for q in 'quorum 3/3 AGREED: FAIL (B1 blocks)' 'quorum 3/3 AGREED to reject' 'quorum 3/3 AGREED on 2 BLOCKING findings' 'quorum 3/3 AGREED?' 'quorum 3/3 AGREED cannot arm yet' 'quorum 3/3 REJECT' 'quorum 3/3 AGREED' 'quorum 3/3 AGREED PASS once B1 lands' 'on "quorum 3/3 AGREED PASS" quoted' 'carried from round 3: quorum 3/3 AGREED PASS'; do + eq "B1/B2 receipt row '$q' is not a receipt" "$(REVIEWS="${S2}COMMENTED\t$q\n" ns)" 0 + done + eq 'receipt row with IF is not a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 PASS, if seats\n" ns)" 0 + eq 'receipt row with CANNOT is not a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 PASS; cannot arm\n" ns)" 0 + eq 'receipt row with trailing prose (END anchor) is not a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 PASS the following\n" ns)" 0 + eq 'seat row with trailing prose (END anchor) is not a seat' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w9 APPROVE the following\n$Q3" ns)" 0 + for s in 'carried from round 3: seat w9 APPROVE' 'seat w9 APPROVE once B1 lands' 'seat w9 approved round 3, round 4 BLOCKING' 'on "seat w9 APPROVE" pasted evidence' 'see seat w9 APPROVE above' 'seat w9 APPROVE but FAIL'; do + eq "B2 seat row '$s' is not a seat" "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\t$s\n$Q3" ns)" 0 + done + eq 'positive: seat with trailing @sha and receipt with PASS -> armable' "$(REVIEWS='COMMENTED\tseat w1 APPROVE @abcdef1234\nCOMMENTED\tseat w2 APPROVE.\nCOMMENTED\tquorum 3/3 PASS\n' ns)" 1 unset -f gh [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" fi From 0e96e3a5e4d54a4ecaac48bda02f44772c49afb1 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:31:38 +0200 Subject: [PATCH 07/12] fix(mq-feeder): never enqueue gate content or a PR without a head-bound 3/3 receipt; END anchors to end of body (PMAT-5843) #5849 was enqueued at 16:18Z although its s13 verdict was REFUSE. The feeder now skips gate-content PRs and any PR lacking a quorum 3/3 PASS receipt at its head; seat counting is removed. Subprocess self-test proves REFUSE / no receipt / gate content never reach ARM or post 'enqueued'. END: trailing prose rejected. Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 117 ++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index 0a85d890c0..c9d26820b7 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -26,39 +26,32 @@ pick() { # gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). # Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. gate_paths() { grep -qE '^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } -# Review format the feeder accepts. Positive anchors AT THE START OF THE BODY; one review = one seat OR one receipt: -# seat: "seat APPROVE" ( a word that is not APPROVE/APPROVED) +# Review format the feeder accepts (a quorum receipt only; seats are not read). Positive anchors AT THE START OF THE BODY: # quorum: "quorum 3/3 [AGREED] PASS" (or APPROVE/APPROVED); PASS/APPROVE is the verdict direction -# followed by end of body, punctuation, or " @". A body with a quote mark, a negation/deferral/failure word or a +# followed ONLY by optional punctuation and an optional " @" up to the end of the body. A body with a quote mark, a negation/deferral/failure word or a # carried-over marker anywhere is rejected, so quoted evidence and "APPROVE once X lands" never count. NEG='\b(NOT|NO|NEED|NEEDS|PENDING|REQUIRED|MISSING|AWAITING|WITHOUT|UNTIL|IF|ONCE|AFTER|WHEN|UNLESS|CANNOT|FAIL|FAILS|FAILED|REJECT|REJECTED|BLOCK|BLOCKS|BLOCKING|CARRIED|PREVIOUS|EARLIER|PRIOR)\b|REQUEST[-_ ]?CHANGES|[0-2]/3|"'"'"'`' -END='([.;:,!]|[[:space:]]+@[0-9a-f]{7,40}|[[:space:]]*$)' -# seat_names REVIEWS -> distinct seat names (one per line); dedupes by NAME, so a repost of one seat counts once. -seat_names() { - grep -iE "^(COMMENTED|APPROVED)"$'\t'"seat[: ]+[A-Za-z0-9_.@-]+[ :,;-]+APPROVED?${END}" <<<"$1" | grep -viE "$NEG" \ - | sed -nE 's/^[A-Z]+\t[Ss][Ee][Aa][Tt][: ]+([A-Za-z0-9_.@-]+)[ :,;-]+[Aa][Pp][Pp].*/\1/p' \ - | tr 'A-Z' 'a-z' | grep -vxE 'approve|approved' | sort -u -} +END='[.;:,!]?([[:space:]]+@[0-9a-f]{7,40})?[.;:,!]?[[:space:]]*$' # quorum_receipts REVIEWS -> count of reviews that are exactly a "quorum 3/3 [AGREED] PASS|APPROVE(D)" receipt (see above) quorum_receipts() { grep -iE "^(COMMENTED|APPROVED)"$'\t'"quorum[: ]+3/3[ :,;-]+(AGREED[ :,;-]+)?(PASS|APPROVED?)${END}" <<<"$1" | grep -viE "$NEG" | wc -l } -# needs_seats PR HEAD -> prints the reason and returns 0 unless the PR is non-gate OR holds a verified 3/3 quorum -# receipt plus 2 distinct APPROVE seat reviews bound to HEAD and no REQUEST-CHANGES bound to HEAD. FAILS CLOSED: -# an unreadable file list or review list, or an empty HEAD, means "needs seats". +# needs_seats PR HEAD -> prints the skip reason and returns 0 (skip) unless the PR is NON-gate content AND holds a +# "quorum 3/3 [AGREED] PASS" receipt review bound to HEAD with no request-changes/FAIL/BLOCKING bound to HEAD. +# Gate-content PRs are never enqueued by the feeder (operator/arbiter arms them; 16:18Z #5849 was enqueued +# without a receipt). FAILS CLOSED: unreadable file list or review list, or an empty HEAD, means skip. needs_seats() { - local f rv a rc q - [ -n "$2" ] || { echo "needs seats (head unreadable)"; return 0; } - f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || { echo "needs seats (files unreadable)"; return 0; } - [ -n "$f" ] || { echo "needs seats (file list empty)"; return 0; } - gate_paths "$f" || return 1 - # reviews bound to HEAD (commit_id), COMMENTED/APPROVED only; one body per line, newlines folded + local f rv rc q + [ -n "$2" ] || { echo "skip (head unreadable)"; return 0; } + f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || { echo "skip (files unreadable)"; return 0; } + [ -n "$f" ] || { echo "skip (file list empty)"; return 0; } + gate_paths "$f" && { echo "skip (gate content: the feeder never enqueues a gate change)"; return 0; } + # reviews bound to HEAD (commit_id); one body per line, newlines folded rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b' <<<"$rv") - a=$(seat_names "$rv" | wc -l) q=$(quorum_receipts "$rv") - { [ "${rc:-0}" -eq 0 ] && [ "${a:-0}" -ge 2 ] && [ "${q:-0}" -ge 1 ]; } && return 1 - echo "needs seats (gate content; bound to ${2:0:10}: $a/2 distinct named APPROVE seats, quorum 3/3 receipt $q/1, $rc request-changes)" + { [ "${rc:-0}" -eq 0 ] && [ "${q:-0}" -ge 1 ]; } && return 1 + echo "skip (no quorum receipt at ${2:0:10}: quorum 3/3 PASS receipts $q/1, $rc request-changes)" } 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 @@ -79,44 +72,54 @@ if [ "${1:-}" = --self-test ]; then # gate_paths: scripts the explicit-test fragments run are gate content (triage 38b290a66f) for p in scripts/contracts_gate.sh scripts/mutate_foo_guard.sh scripts/lib_baseline_ratchet.sh scripts/pv_bin.sh scripts/pmat_bin.sh scripts/tests/x.sh; do gate_paths "$p" || { echo "FAIL gate_paths $p"; fail=1; }; done # needs_seats with a stubbed gh: REVIEWS is "STATEbody" lines, FILES the PR file list - gh() { case "$2" in */files) [ "${CASE:-}" = filefail ] && return 1; printf '%s\n' "${FILES:-scripts/ci_x.sh}";; */reviews) printf '%b' "${REVIEWS:-}";; esac; } - ns() { REPO=r needs_seats 1 H >/dev/null; echo $?; } # 0 = needs seats (not armable), 1 = armable + gh() { case "$2" in */files) [ "${CASE:-}" = filefail ] && return 1; printf '%s\n' "${FILES:-docs/x.md}";; */reviews) printf '%b' "${REVIEWS:-}";; esac; } + ns() { REPO=r needs_seats 1 H >/dev/null; echo $?; } # 0 = skip (not armable), 1 = armable Q3='COMMENTED\tquorum 3/3 AGREED PASS\n' - eq 'seats: two named + 3/3 -> armable' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n$Q3" ns)" 1 - eq 'seats: repost of one seat (same name, different text) -> needs' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w1 APPROVE again\n$Q3" ns)" 0 - eq 'seats: negated quorum "NOT 3/3" + repost -> needs' "$(REVIEWS='COMMENTED\tSeat (a) APPROVE; quorum NOT 3/3\nCOMMENTED\tSeat (a) APPROVE; quorum NOT 3/3 \nCOMMENTED\tSeat (b) APPROVE\n' ns)" 0 - eq 'seats: two seats + only a negated quorum receipt -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\nCOMMENTED\tquorum NOT 3/3\n' ns)" 0 - eq 'seats: quorum 2/3 -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\nCOMMENTED\tquorum 2/3 AGREED\n' ns)" 0 - eq 'seats: request-changes bound to head -> needs' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n${Q3}CHANGES_REQUESTED\tseat w3 RC\n" ns)" 0 - eq 'seats: no 3/3 receipt -> needs' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' ns)" 0 - eq 'seats: files unreadable -> needs (fail closed)' "$(CASE=filefail ns)" 0 - eq 'seats: non-gate PR -> armable' "$(FILES=docs/x.md ns)" 1 - # round 3 (a1b0efc624): positive anchors; each row fails under the matching mutation - S2='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' - for q in 'quorum 3/3 pending' 'quorum 3/3 required' 'quorum 3/3 missing' 'need quorum 3/3' 'AGREED 3/3' 'quorum 3/3' 'quorum NOT 3/3'; do - eq "quorum row '$q' is not a receipt" "$(REVIEWS="${S2}COMMENTED\t$q\n" ns)" 0 - done - eq 'quorum row "quorum 3/3 APPROVED" is a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 APPROVED\n" ns)" 1 - eq 'seat "Seat APPROVE" (no name) x2 -> needs' "$(REVIEWS="COMMENTED\tSeat APPROVE\nCOMMENTED\tSeat APPROVE\n$Q3" ns)" 0 - eq 'seat "Seat (a) APPROVE" x2 (no name) -> needs' "$(REVIEWS="COMMENTED\tSeat (a) APPROVE\nCOMMENTED\tSeat (b) APPROVE\n$Q3" ns)" 0 - eq 'seat "seat w1 APPROVE" twice with NOT 3/3 repost -> needs (named seat, not vacuous)' "$(REVIEWS='COMMENTED\tseat w1 APPROVE; quorum NOT 3/3\nCOMMENTED\tseat w1 APPROVE; quorum NOT 3/3 \n' ns)" 0 - eq 'seat names: two named' "$(seat_names "$(printf 'COMMENTED\tseat w1 APPROVE\nCOMMENTED\tSeat: W2 APPROVE\nCOMMENTED\tseat w1 APPROVE again')" | tr '\n' ,)" w1,w2, - eq 'seat "seat APPROVE APPROVE" yields no name' "$(seat_names "$(printf 'COMMENTED\tseat APPROVE APPROVE')")" '' - eq 'seat "seat w1 reviewing APPROVE" (no direct anchor) yields no name' "$(seat_names "$(printf 'COMMENTED\tseat w1 reviewing APPROVE')")" '' - # round 4 (1539bf88d6): verdict direction (B1) and quoted/carried-over text (B2) must not count. Each row: two real - # seats plus ONLY the bad quorum line -> needs; or ONE real seat plus the bad seat line + a real receipt -> needs. - for q in 'quorum 3/3 AGREED: FAIL (B1 blocks)' 'quorum 3/3 AGREED to reject' 'quorum 3/3 AGREED on 2 BLOCKING findings' 'quorum 3/3 AGREED?' 'quorum 3/3 AGREED cannot arm yet' 'quorum 3/3 REJECT' 'quorum 3/3 AGREED' 'quorum 3/3 AGREED PASS once B1 lands' 'on "quorum 3/3 AGREED PASS" quoted' 'carried from round 3: quorum 3/3 AGREED PASS'; do - eq "B1/B2 receipt row '$q' is not a receipt" "$(REVIEWS="${S2}COMMENTED\t$q\n" ns)" 0 + eq 'receipt on a non-gate PR -> armable' "$(REVIEWS="$Q3" ns)" 1 + eq 'no receipt (non-gate PR) -> skip' "$(REVIEWS='' ns)" 0 + eq 'two seats but no receipt -> skip' "$(REVIEWS='COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w2 APPROVE\n' ns)" 0 + eq 'gate content + receipt -> skip (never enqueued by the feeder)' "$(FILES=scripts/x.sh REVIEWS="$Q3" ns)" 0 + eq 'gate content among docs + receipt -> skip' "$(FILES=$'docs/x.md\n.github/workflows/ci.yml' REVIEWS="$Q3" ns)" 0 + eq 'files unreadable -> skip (fail closed)' "$(CASE=filefail REVIEWS="$Q3" ns)" 0 + eq 'receipt + request-changes at head -> skip' "$(REVIEWS="${Q3}CHANGES_REQUESTED\tseat w3 RC\n" ns)" 0 + eq 'receipt + FAIL review at head -> skip' "$(REVIEWS="${Q3}COMMENTED\tseat w3 FAIL\n" ns)" 0 + eq 'receipt "quorum 3/3 APPROVED" -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 APPROVED\n' ns)" 1 + eq 'receipt with trailing @sha -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS @abcdef1234\n' ns)" 1 + eq 'receipt with punctuation -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS.\n' ns)" 1 + eq 'receipt with punctuation then @sha -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS. @abcdef1234\n' ns)" 1 + for q in 'quorum 3/3 pending' 'quorum 3/3 required' 'quorum 3/3 missing' 'need quorum 3/3' 'AGREED 3/3' 'quorum 3/3' 'quorum NOT 3/3' 'quorum 2/3 AGREED PASS' \ + 'quorum 3/3 AGREED: FAIL (B1 blocks)' 'quorum 3/3 AGREED to reject' 'quorum 3/3 AGREED on 2 BLOCKING findings' 'quorum 3/3 AGREED?' 'quorum 3/3 AGREED cannot arm yet' \ + 'quorum 3/3 REJECT' 'quorum 3/3 PASS, if seats' 'quorum 3/3 PASS; cannot arm' 'quorum 3/3 PASS the following' 'quorum 3/3 PASS. Nothing was verified by me' \ + 'quorum 3/3 PASS @abcdef1234 (RC on a later head)' 'carried from round 3: quorum 3/3 PASS' 'quorum 3/3 PASS once B1 lands' 'see quorum 3/3 PASS above' 'on "quorum 3/3 PASS" pasted evidence' 'quorum 3/3 PASS but FAIL'; do + eq "row '$q' is not a receipt" "$(REVIEWS="COMMENTED\t$q\n" ns)" 0 done - eq 'receipt row with IF is not a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 PASS, if seats\n" ns)" 0 - eq 'receipt row with CANNOT is not a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 PASS; cannot arm\n" ns)" 0 - eq 'receipt row with trailing prose (END anchor) is not a receipt' "$(REVIEWS="${S2}COMMENTED\tquorum 3/3 PASS the following\n" ns)" 0 - eq 'seat row with trailing prose (END anchor) is not a seat' "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\tseat w9 APPROVE the following\n$Q3" ns)" 0 - for s in 'carried from round 3: seat w9 APPROVE' 'seat w9 APPROVE once B1 lands' 'seat w9 approved round 3, round 4 BLOCKING' 'on "seat w9 APPROVE" pasted evidence' 'see seat w9 APPROVE above' 'seat w9 APPROVE but FAIL'; do - eq "B2 seat row '$s' is not a seat" "$(REVIEWS="COMMENTED\tseat w1 APPROVE\nCOMMENTED\t$s\n$Q3" ns)" 0 - done - eq 'positive: seat with trailing @sha and receipt with PASS -> armable' "$(REVIEWS='COMMENTED\tseat w1 APPROVE @abcdef1234\nCOMMENTED\tseat w2 APPROVE.\nCOMMENTED\tquorum 3/3 PASS\n' ns)" 1 unset -f gh + # subprocess: the whole feeder against a stub gh and a stub ARM. REFUSE (rc 3), a missing receipt and gate content must NEVER + # call ARM or post "enqueued"; an ARM rc 0 control with a receipt does enqueue (proves the harness can see an enqueue). + d=$(mktemp -d) || exit 2 + cat > "$d/gh" <<'GHEOF' +#!/usr/bin/env bash +case "$*" in + *rate_limit*) echo 5000;; + *"mergeQueue(branch"*) printf '{"data":{"repository":{"mergeQueue":{"entries":{"totalCount":0}},"pullRequests":{"nodes":[{"number":7,"isDraft":false,"baseRefName":"main","mergeStateStatus":"CLEAN","autoMergeRequest":null,"mergeQueueEntry":null}]}}}}';; + *graphql*) echo QUEUED;; + "pr view"*) echo deadbeefdeadbeef;; + */files*) printf '%s\n' "$T_FILES";; + */reviews*) printf '%b' "$T_REVIEWS";; + */commits*) echo 0;; +esac +GHEOF + printf '#!/usr/bin/env bash\necho "$1" >> "$T_MARK"\n[ "$T_ARMRC" = 3 ] && echo "refuse: guard check not green"\nexit "$T_ARMRC"\n' > "$d/arm" + chmod +x "$d/gh" "$d/arm" + run_feeder() { rm -f "${d:?}/mark" "${d:?}/inbox" "${d:?}/st/kicked"; : > "$d/inbox" + T_FILES=$1 T_REVIEWS=$2 T_ARMRC=$3 T_MARK="$d/mark" PATH="$d:$PATH" MQ_FEEDER_STATE="$d/st" MQ_FEEDER_INBOX="$d/inbox" MQ_FEEDER_ARM="$d/arm" MQ_FEEDER_RECHECK=0 bash "$0" >/dev/null 2>&1 + echo "armed=$([ -e "$d/mark" ] && echo yes || echo no) enq=$(grep -c 'enqueued oldest' "$d/inbox")"; } + eq 'feeder: REFUSE (arm rc 3) with a receipt never enqueues' "$(run_feeder docs/x.md "$Q3" 3)" 'armed=yes enq=0' + eq 'feeder: no receipt never calls ARM' "$(run_feeder docs/x.md '' 0)" 'armed=no enq=0' + eq 'feeder: gate content with a receipt never calls ARM' "$(run_feeder scripts/x.sh "$Q3" 0)" 'armed=no enq=0' + eq 'feeder: request-changes at head never calls ARM' "$(run_feeder docs/x.md "${Q3}CHANGES_REQUESTED\tx\n" 0)" 'armed=no enq=0' + eq 'feeder control: receipt + ARM rc 0 enqueues' "$(run_feeder docs/x.md "$Q3" 0)" 'armed=yes enq=1' + rm -rf "${d:?}" [ "$fail" -eq 0 ] && echo "ok: self-test"; exit "$fail" fi [ "${1:-}" = --dry-run ] && dry=1 From d156e7080b04ebe0a004e6b604b65c45b194b4ff Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:35:26 +0200 Subject: [PATCH 08/12] fix(mq-feeder): read previous_filename (rename out of a gate path), widen gate_paths and the blocker words (PMAT-5843) Triage 0e96e3a5e4: needs_seats read only .filename, so scripts/check_guard.sh renamed to docs/old_guard.sh was armable. The stub gh now runs the real --jq filter. gate_paths adds rustfmt/gitignore/codecov/build.rs/any Cargo.toml; the request-changes regex adds BLOCKER, BLOCKS, NO-GO, DO NOT MERGE. Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index c9d26820b7..1ee20c4548 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -25,7 +25,7 @@ pick() { } # gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). # Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. -gate_paths() { grep -qE '^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } +gate_paths() { grep -qE '(^|/)(Cargo\.toml$|build\.rs$|\.?rustfmt\.toml$|\.gitignore$|\.?codecov\.yml$)|^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } # Review format the feeder accepts (a quorum receipt only; seats are not read). Positive anchors AT THE START OF THE BODY: # quorum: "quorum 3/3 [AGREED] PASS" (or APPROVE/APPROVED); PASS/APPROVE is the verdict direction # followed ONLY by optional punctuation and an optional " @" up to the end of the body. A body with a quote mark, a negation/deferral/failure word or a @@ -43,12 +43,12 @@ quorum_receipts() { needs_seats() { local f rv rc q [ -n "$2" ] || { echo "skip (head unreadable)"; return 0; } - f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[].filename' 2>/dev/null) || { echo "skip (files unreadable)"; return 0; } + f=$(gh api "repos/$REPO/pulls/$1/files" --paginate --jq '.[]|.filename, (.previous_filename // empty)' 2>/dev/null) || { echo "skip (files unreadable)"; return 0; } [ -n "$f" ] || { echo "skip (file list empty)"; return 0; } gate_paths "$f" && { echo "skip (gate content: the feeder never enqueues a gate change)"; return 0; } # reviews bound to HEAD (commit_id); one body per line, newlines folded rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } - rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b' <<<"$rv") + rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b|\bBLOCKER|\bBLOCKS\b|NO-GO|DO NOT MERGE' <<<"$rv") q=$(quorum_receipts "$rv") { [ "${rc:-0}" -eq 0 ] && [ "${q:-0}" -ge 1 ]; } && return 1 echo "skip (no quorum receipt at ${2:0:10}: quorum 3/3 PASS receipts $q/1, $rc request-changes)" @@ -72,7 +72,10 @@ if [ "${1:-}" = --self-test ]; then # gate_paths: scripts the explicit-test fragments run are gate content (triage 38b290a66f) for p in scripts/contracts_gate.sh scripts/mutate_foo_guard.sh scripts/lib_baseline_ratchet.sh scripts/pv_bin.sh scripts/pmat_bin.sh scripts/tests/x.sh; do gate_paths "$p" || { echo "FAIL gate_paths $p"; fail=1; }; done # needs_seats with a stubbed gh: REVIEWS is "STATEbody" lines, FILES the PR file list - gh() { case "$2" in */files) [ "${CASE:-}" = filefail ] && return 1; printf '%s\n' "${FILES:-docs/x.md}";; */reviews) printf '%b' "${REVIEWS:-}";; esac; } + # FILES lines are "name" or "nameprevious_name"; the stub feeds them through the REAL --jq filter, so the filter is under test + gh() { case "$2" in */files) [ "${CASE:-}" = filefail ] && return 1; local flt=; while [ $# -gt 0 ]; do [ "$1" = --jq ] && flt=$2; shift; done + printf '%s\n' "${FILES:-docs/x.md}" | jq -Rn '[inputs|split("\t")|{filename:.[0]}+(if length>1 then {previous_filename:.[1]} else {} end)]' | jq -r "$flt";; + */reviews) printf '%b' "${REVIEWS:-}";; esac; } ns() { REPO=r needs_seats 1 H >/dev/null; echo $?; } # 0 = skip (not armable), 1 = armable Q3='COMMENTED\tquorum 3/3 AGREED PASS\n' eq 'receipt on a non-gate PR -> armable' "$(REVIEWS="$Q3" ns)" 1 @@ -83,6 +86,14 @@ if [ "${1:-}" = --self-test ]; then eq 'files unreadable -> skip (fail closed)' "$(CASE=filefail REVIEWS="$Q3" ns)" 0 eq 'receipt + request-changes at head -> skip' "$(REVIEWS="${Q3}CHANGES_REQUESTED\tseat w3 RC\n" ns)" 0 eq 'receipt + FAIL review at head -> skip' "$(REVIEWS="${Q3}COMMENTED\tseat w3 FAIL\n" ns)" 0 + eq 'rename OUT of a gate path + receipt -> skip (previous_filename)' "$(FILES=$'docs/old_guard.sh\tscripts/check_guard.sh' REVIEWS="$Q3" ns)" 0 + eq 'plain non-gate rename + receipt -> armable' "$(FILES=$'docs/b.md\tdocs/a.md' REVIEWS="$Q3" ns)" 1 + for p in rustfmt.toml .rustfmt.toml .gitignore codecov.yml build.rs crates/foo/Cargo.toml crates/foo/build.rs; do + eq "gate path $p + receipt -> skip" "$(FILES=$p REVIEWS="$Q3" ns)" 0 + done + for r in 'seat w3 BLOCKER' 'NO-GO' 'seat w3 BLOCKS' 'DO NOT MERGE'; do + eq "review '$r' at head -> skip" "$(REVIEWS="${Q3}COMMENTED\t$r\n" ns)" 0 + done eq 'receipt "quorum 3/3 APPROVED" -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 APPROVED\n' ns)" 1 eq 'receipt with trailing @sha -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS @abcdef1234\n' ns)" 1 eq 'receipt with punctuation -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS.\n' ns)" 1 From 104054c49dfeb9c9f44cae2813cd22acae7dabc8 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:37:17 +0200 Subject: [PATCH 09/12] fix(mq-feeder): gate subdir clippy/deny/toolchain/.cargo; catch do-not-merge, NO GO, changes requested (PMAT-5843) Triage d156e7080b: 62 tracked subdir files (crates/*/.clippy.toml etc.) were armable; RC words missed do-not-merge, 'NO GO', "don't merge", 'changes requested'. Rows red under both old patterns. Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index 1ee20c4548..7489aef584 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -25,7 +25,7 @@ pick() { } # gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). # Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. -gate_paths() { grep -qE '(^|/)(Cargo\.toml$|build\.rs$|\.?rustfmt\.toml$|\.gitignore$|\.?codecov\.yml$)|^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } +gate_paths() { grep -qE '(^|/)(Cargo\.toml$|build\.rs$|\.?clippy\.toml$|deny\.toml$|rust-toolchain|\.cargo/|\.?rustfmt\.toml$|\.gitignore$|\.?codecov\.yml$)|^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } # Review format the feeder accepts (a quorum receipt only; seats are not read). Positive anchors AT THE START OF THE BODY: # quorum: "quorum 3/3 [AGREED] PASS" (or APPROVE/APPROVED); PASS/APPROVE is the verdict direction # followed ONLY by optional punctuation and an optional " @" up to the end of the body. A body with a quote mark, a negation/deferral/failure word or a @@ -48,7 +48,7 @@ needs_seats() { gate_paths "$f" && { echo "skip (gate content: the feeder never enqueues a gate change)"; return 0; } # reviews bound to HEAD (commit_id); one body per line, newlines folded rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } - rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b|\bBLOCKER|\bBLOCKS\b|NO-GO|DO NOT MERGE' <<<"$rv") + rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b|\bBLOCKER|\bBLOCKS\b|NO[- ]?GO|DO[- ]?N(OT|.?T)[- ]MERGE|CHANGES REQUESTED' <<<"$rv") q=$(quorum_receipts "$rv") { [ "${rc:-0}" -eq 0 ] && [ "${q:-0}" -ge 1 ]; } && return 1 echo "skip (no quorum receipt at ${2:0:10}: quorum 3/3 PASS receipts $q/1, $rc request-changes)" @@ -94,6 +94,12 @@ if [ "${1:-}" = --self-test ]; then for r in 'seat w3 BLOCKER' 'NO-GO' 'seat w3 BLOCKS' 'DO NOT MERGE'; do eq "review '$r' at head -> skip" "$(REVIEWS="${Q3}COMMENTED\t$r\n" ns)" 0 done + for p in crates/foo/.clippy.toml crates/foo/clippy.toml crates/foo/deny.toml crates/foo/rust-toolchain.toml crates/foo/.cargo/config.toml; do + eq "gate path $p + receipt -> skip" "$(FILES=$p REVIEWS="$Q3" ns)" 0 + done + for r in 'do-not-merge' 'NO GO' "don't merge" 'changes requested' 'do not merge'; do + eq "review '$r' at head -> skip" "$(REVIEWS="${Q3}COMMENTED\t$r\n" ns)" 0 + done eq 'receipt "quorum 3/3 APPROVED" -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 APPROVED\n' ns)" 1 eq 'receipt with trailing @sha -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS @abcdef1234\n' ns)" 1 eq 'receipt with punctuation -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 AGREED PASS.\n' ns)" 1 From 47d814eadf723b7f251f5d2de8e03793bcf0ba4c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 18:38:58 +0200 Subject: [PATCH 10/12] fix(mq-feeder): veto words accept - _ or space separators; list is best-effort (PMAT-5843) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/mq_feeder.sh b/scripts/mq_feeder.sh index 7489aef584..931983a68e 100755 --- a/scripts/mq_feeder.sh +++ b/scripts/mq_feeder.sh @@ -26,6 +26,7 @@ pick() { # gate_paths FILES -> 0 when any path is gate content (a gate change needs 2 non-author seats bound to the head). # Wide on purpose: every path under scripts/, ci/ and .github/ (the explicit-test fragments run arbitrary scripts) plus root build/lint config. gate_paths() { grep -qE '(^|/)(Cargo\.toml$|build\.rs$|\.?clippy\.toml$|deny\.toml$|rust-toolchain|\.cargo/|\.?rustfmt\.toml$|\.gitignore$|\.?codecov\.yml$)|^(\.github/|ci/|scripts/|Cargo\.toml$|Cargo\.lock$|Makefile$|rust-toolchain|\.config/nextest|\.cargo/|deny\.toml$|clippy\.toml$|\.clippy\.toml$|\.pmat-gates\.toml$)' <<<"$1"; } +# (The request-changes word list is best-effort; the hard vetoes are the CHANGES_REQUESTED state and not arming.) # Review format the feeder accepts (a quorum receipt only; seats are not read). Positive anchors AT THE START OF THE BODY: # quorum: "quorum 3/3 [AGREED] PASS" (or APPROVE/APPROVED); PASS/APPROVE is the verdict direction # followed ONLY by optional punctuation and an optional " @" up to the end of the body. A body with a quote mark, a negation/deferral/failure word or a @@ -48,7 +49,7 @@ needs_seats() { gate_paths "$f" && { echo "skip (gate content: the feeder never enqueues a gate change)"; return 0; } # reviews bound to HEAD (commit_id); one body per line, newlines folded rv=$(gh api "repos/$REPO/pulls/$1/reviews" --paginate --jq ".[]|select(.commit_id==\"$2\" and (.state==\"COMMENTED\" or .state==\"APPROVED\" or .state==\"CHANGES_REQUESTED\"))|[.state,(.body|gsub(\"[\\n\\r]+\";\" \"))]|@tsv" 2>/dev/null) || { echo "needs seats (reviews unreadable)"; return 0; } - rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b|\bBLOCKER|\bBLOCKS\b|NO[- ]?GO|DO[- ]?N(OT|.?T)[- ]MERGE|CHANGES REQUESTED' <<<"$rv") + rc=$(grep -ciE '^CHANGES_REQUESTED|REQUEST[-_ ]?CHANGES|\bFAIL|\bREJECT|\bBLOCKING\b|\bBLOCKER|\bBLOCKS\b|NO[-_ ]?GO|DO[-_ ]?N(OT|.?T)[-_ ]MERGE|CHANGES[-_ ]REQUESTED' <<<"$rv") q=$(quorum_receipts "$rv") { [ "${rc:-0}" -eq 0 ] && [ "${q:-0}" -ge 1 ]; } && return 1 echo "skip (no quorum receipt at ${2:0:10}: quorum 3/3 PASS receipts $q/1, $rc request-changes)" @@ -97,7 +98,7 @@ if [ "${1:-}" = --self-test ]; then for p in crates/foo/.clippy.toml crates/foo/clippy.toml crates/foo/deny.toml crates/foo/rust-toolchain.toml crates/foo/.cargo/config.toml; do eq "gate path $p + receipt -> skip" "$(FILES=$p REVIEWS="$Q3" ns)" 0 done - for r in 'do-not-merge' 'NO GO' "don't merge" 'changes requested' 'do not merge'; do + for r in 'changes-requested' 'changes_requested' 'do_not_merge' 'NO_GO' 'do-not-merge' 'NO GO' "don't merge" 'changes requested' 'do not merge'; do eq "review '$r' at head -> skip" "$(REVIEWS="${Q3}COMMENTED\t$r\n" ns)" 0 done eq 'receipt "quorum 3/3 APPROVED" -> armable' "$(REVIEWS='COMMENTED\tquorum 3/3 APPROVED\n' ns)" 1 From 1bc05401c631c42833cc117d05fb56f39a1f8a1b Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 22:27:42 +0200 Subject: [PATCH 11/12] fix(mq-feeder): mq_feeder_install.sh refuses an empty, relative or '..' DEST (bashrs SEC010) (PMAT-5843) Co-Authored-By: Claude Sonnet 5.5 --- scripts/mq_feeder_install.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/mq_feeder_install.sh b/scripts/mq_feeder_install.sh index f5acf84c06..f31a843ce4 100755 --- a/scripts/mq_feeder_install.sh +++ b/scripts/mq_feeder_install.sh @@ -5,7 +5,15 @@ # Usage: mq_feeder_install.sh [DEST] set -euo pipefail src="$(cd "$(dirname "$0")" && pwd)" -dest="${1:-$HOME/.local/libexec/mq-feeder}" +dest="${1-$HOME/.local/libexec/mq-feeder}" +# SEC010 (bashrs): the install target is an argument, so it must be absolute (so an explicit empty arg is refused too) and free of `..`. +case "$dest" in + /*) ;; + *) echo "mq_feeder_install: DEST must be an absolute path, got '$dest'" >&2; exit 2 ;; +esac +case "/$dest/" in + */../*) echo "mq_feeder_install: DEST must not contain '..': $dest" >&2; exit 2 ;; +esac mkdir -p "$dest" for s in mq_feeder.sh arm_pr_automerge.sh; do install -m 0755 "$src/$s" "$dest/$s" From cb904b61f21a23c559d6c5a7458b4240c2b9627b Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 22:44:32 +0200 Subject: [PATCH 12/12] chore(roadmap): regenerate roadmap.yaml after main merge (Refs #5876) Co-Authored-By: Claude Sonnet 5.5 --- docs/roadmaps/roadmap.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 574ad024b7..fd9b5d5d2c 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -25390,6 +25390,13 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe 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 - id: PMAT-5873 github_issue: 5873 item_type: task