From efb35b0450a6ad8a9fb2e4c9ac8499d4cd26347b Mon Sep 17 00:00:00 2001 From: os-sam Date: Tue, 1 Sep 2026 07:23:23 +0000 Subject: [PATCH 1/2] feat(devx): refuse a governed enqueue with no head-pinned approval, in a PreToolUse hook A governed PR was enqueued with zero approvals; the merge-queue guard refused as designed, the entry popped out red, and the approval that arrived afterwards did NOT re-run it. One queue cycle burned and a false red shown to the maintainer. The queue guard stays the hard correctness line. This hook removes the wasted cycle at the only place a machine can: our own tool-call surface. It intercepts the enqueue-class calls, asks the two existing single sources -- the register's `--test` predicate and `pinnedApprovalVerdict`/`GOVERNED_APPROVERS` -- and refuses when the diff is governed and unpinned. No second mechanism, no second path list, no second approver list. Pure regeneration clears untouched. Fail-open on any unanswerable read, by design: the correctness line is elsewhere. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2 --- .../hooks/guard-governed-enqueue.selftest.sh | 318 ++++++++++ .claude/hooks/guard-governed-enqueue.sh | 556 ++++++++++++++++++ .claude/settings.json | 13 + 3 files changed, 887 insertions(+) create mode 100755 .claude/hooks/guard-governed-enqueue.selftest.sh create mode 100755 .claude/hooks/guard-governed-enqueue.sh diff --git a/.claude/hooks/guard-governed-enqueue.selftest.sh b/.claude/hooks/guard-governed-enqueue.selftest.sh new file mode 100755 index 0000000000..ac7e745d8b --- /dev/null +++ b/.claude/hooks/guard-governed-enqueue.selftest.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# Self-test for guard-governed-enqueue.sh — run it after touching that hook: +# +# .claude/hooks/guard-governed-enqueue.selftest.sh +# +# Feeds the hook the same JSON payload shape Claude Code delivers on PreToolUse +# and asserts the block/allow verdict per case, plus the load-bearing sentences +# of the refusal. Modelled on guard-shared-stash.selftest.sh; the two matrices +# are kept in the same shape so neither drifts into its own idiom. +# +# NO NETWORK. The three GitHub reads come from `OS_GOVERNED_ENQUEUE_FIXTURE` +# (documented in the hook's header as test-only injection): a directory holding +# `pull.json` / `files.json` / `reviews.json`. What is NOT stubbed is the part +# that matters — both predicates run for real, so this matrix fails if the hook +# ever stops asking the register and the queue guard and starts deciding for +# itself. +# +# Needs `jq` (to build fixtures) and `node` (the two real predicates run). No +# pnpm install, no build: measured against a worktree with no `node_modules`. +# +# ⚠️ ONE COUPLING, STATED SO A FAILURE IS NOT MISREAD. The +# `pure-regeneration-only ⇒ ALLOWED` case uses a REAL register-lifted path +# (`.claude/workflows/docs-accuracy-audit.js`, the #9866 row) rather than a +# stub, because the requirement under test is precisely "this guard must never +# re-close the zero-approval path the register clears" and a stub cannot show +# that. The consequence: the case needs that artifact to be in sync with its own +# generator on the tree it runs against — which is what the required +# `check:docs-audit-scope` gate keeps true. If this one case fails while the +# rest pass, look there first; re-run +# node scripts/pm/check-governed-merges.mjs --test .claude/workflows/docs-accuracy-audit.js +# and read what the register says before touching this matrix or the hook. + +set -uo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +hook="$here/guard-governed-enqueue.sh" +repo_root="$(cd "$here/../.." && pwd)" +pass=0 +fail=0 + +command -v jq >/dev/null 2>&1 || { echo "selftest needs jq to build payloads" >&2; exit 1; } +command -v node >/dev/null 2>&1 || { echo "selftest needs node: both predicates run for real" >&2; exit 1; } +[ -x "$hook" ] || { echo "hook is not executable: $hook" >&2; exit 1; } + +HEAD_SHA=b25f061c6a1d4e2f3c9b8a7d6e5f4a3b2c1d0e9f +OLD_SHA=0f9e8d7c6b5a4938271605f4e3d2c1b0a98877665 + +root="$(mktemp -d)" +trap 'rm -rf "$root"' EXIT INT TERM + +# fixture -> prints the directory +fixture() { + local dir="$root/$1" + mkdir -p "$dir" + jq -nc --arg s "$HEAD_SHA" '{head:{sha:$s}}' > "$dir/pull.json" + printf '%s' "$2" > "$dir/files.json" + printf '%s' "$3" > "$dir/reviews.json" + printf '%s' "$dir" +} + +files_of() { # files_of path... -> the /pulls/{n}/files body shape + local out="[]" p + for p in "$@"; do out="$(printf '%s' "$out" | jq -c --arg f "$p" '. + [{filename:$f}]')"; done + printf '%s' "$out" +} + +approved_at() { # approved_at + jq -nc --arg l "$1" --arg c "$2" '[{state:"APPROVED",user:{login:$l},commit_id:$c}]' +} + +NO_REVIEWS='[]' +GOVERNED_FILES="$(files_of AGENTS.md packages/spec/src/index.ts)" +CLEAR_FILES="$(files_of packages/spec/src/index.ts README.md)" +REGEN_FILES="$(files_of .claude/workflows/docs-accuracy-audit.js)" + +F_UNAPPROVED="$(fixture governed-unapproved "$GOVERNED_FILES" "$NO_REVIEWS")" +F_PINNED="$(fixture governed-pinned "$GOVERNED_FILES" "$(approved_at os-zhuang "$HEAD_SHA")")" +F_STALE="$(fixture governed-stale "$GOVERNED_FILES" "$(approved_at os-zhuang "$OLD_SHA")")" +F_OUTSIDER="$(fixture governed-outsider "$GOVERNED_FILES" "$(approved_at os-warren "$HEAD_SHA")")" +F_DISMISSED="$(fixture governed-dismissed "$GOVERNED_FILES" \ + "$(jq -nc --arg c "$HEAD_SHA" '[{state:"APPROVED",user:{login:"os-zhuang"},commit_id:$c},{state:"DISMISSED",user:{login:"os-zhuang"},commit_id:$c}]')")" +F_CLEAR="$(fixture not-governed "$CLEAR_FILES" "$NO_REVIEWS")" +F_REGEN="$(fixture pure-regeneration "$REGEN_FILES" "$NO_REVIEWS")" +F_EMPTY="$(fixture empty-diff '[]' "$NO_REVIEWS")" + +mcp() { # mcp [owner] [repo] + jq -nc --arg t "$1" --argjson n "$2" --arg o "${3:-objectstack-ai}" --arg r "${4:-objectstack}" \ + '{tool_name:$t,tool_input:{owner:$o,repo:$r,pullNumber:$n}}' +} +bash_call() { jq -nc --arg c "$1" '{tool_name:"Bash",tool_input:{command:$c}}'; } + +AUTO=mcp__github__enable_pr_auto_merge +MERGE=mcp__github__merge_pull_request + +# The hook is the LAST element of the pipeline, so `$?` here is the HOOK's exit +# status and not some downstream reader's. That is the only shape in which +# reading a status after a pipe is safe, and it is why nothing is piped past it. +run() { # run [env assignments…] -> allow | block | exitN + local payload="$1"; shift + local rc + printf '%s' "$payload" | env "$@" "$hook" >/dev/null 2>&1 + rc=$? + case "$rc" in + 0) printf 'allow' ;; + 2) printf 'block' ;; + *) printf 'exit%s' "$rc" ;; + esac +} + +stderr_of() { # stderr_of [env…] + local payload="$1"; shift + printf '%s' "$payload" | env "$@" "$hook" 2>&1 >/dev/null +} + +expect() { # expect