You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bring this repo onto the agent-pipeline ci-only posture, so /merge-ci-only and /execute-prd-ci-only can actually reach a merge here. One PR plus one protection change, both scoped to this repo.
Filed here rather than in the plugin repo deliberately: the deliverable is a commit in this tree, so the executor produces a real, non-empty PR — it clears the pipeline's empty-diff blocker, which a zero-file PR filed elsewhere would not.
The merge is an operator click, not pipeline work.decideMerge carries an unconditionalpipeline-config-park blocker keyed on /(^|\/)\.agent-pipeline\.json$/ (gate.ts:1824-1833; class defined at gate-paths.ts:120-122). Its own text: "UNCONDITIONAL: no posture, standing authorization, reviewGate or merge.classificationParks lifts this park … EXIT: a human reviews the config change deliberately and merges it themselves — it is not pipeline work." The rationale (#485) is that this file is where a supply block names which issues the pipeline may treat as its own work scope — a config that grants scope, reachable by what it grants scope to — so it must never merge unread.
An agent can therefore draft the config, open the PR and drive it to green CI. A human merges it. That is why this issue is HITL, and no label or config change makes it AFK.
For this repo, omit ci.requiredChecks entirely — the scope-guard acceptance criterion below pins the committed config to exactly two scalar paths (repo, merge.reviewGate), and a ci.requiredChecks array adds a third. The general rule, for reference elsewhere: add it only if every context named runs on every PR — verify against a live PR head first. When unsure, omit it: an absent requiredChecks falls through to evaluateCiAllGreen, where every observed check must be completed and non-failing and a zero-check head fails closed. A named context that does not run scores absent (fail-closed) and parks every PR forever.
2. Migrate branch protection to a main-protection ruleset
deletion + non_fast_forward + pull_request, with the pull_request parameters at:
required_approving_review_count: 0
require_code_owner_review: false
require_last_push_approval: false
dismiss_stale_reviews_on_push: false
Then delete the classic rule. Do not add required_status_checks unless every listed context runs on every PR.
Ordering matters and is not optional: the ruleset migration must land before the step-1 PR can merge, because the live classic rule still requires an approving review. This step is operator-run — it mutates repo settings out-of-band, so no PR contains it and no revert undoes it. Record the pre-change values (below) before touching anything.
All four parameters matter, not just the count — each is an independent merge blocker in GitHub's own semantics, so a repo migrated to count 0 with require_last_push_approval or stale-review dismissal left on passes a count-only check and stays blocked. That is the reason to set all four; it is derived from the API contract, not from a sample.
Corrected fleet survey (verified live 2026-08-07 via gh api repos/<r>/rules/branches/main and .../branches/main/protection). An earlier revision of this issue claimed eight repos "all verify at exactly these values". That is false for seven of the eight:
repo
pull_request rule
count / codeowner / lastpush / dismiss
classic protection
ivndev001/agent-pipeline
yes
0 / false / false / false
404
ai-designer
yes
0 / false / false / true
404
db-mcp-gateway
yes (+required_status_checks)
0 / false / false / true
live, 4 contexts
developerz.ai
none
—
live, 4 contexts
minime-app
yes
0 / false / false / true
404
multi-ai-router
no ruleset at all
—
live, 5 contexts
venom-astrology
yes
0 / false / false / true
live, 0 contexts
venomtechmedellin.com
none
—
404
Only ivndev001/agent-pipeline matches all four target values. Four repos sit at count 0 with dismiss_stale_reviews_on_push: true — so that parameter is not empirically load-bearing for reaching a merge, and the target shape here is the strict one by choice, not by precedent. Three repos are not migrated at all. Do not cite the fleet as evidence that this shape is already proven.
What this trades, stated plainly
required_approving_review_count: 0 does not delete review — it transfers it from a GitHub click to the plugin's two-lens deep-review gate plus fail-closed CI. On a repo at count 1 an approving bot review already satisfies the requirement, which is the specific hazard developerz-ai/wurk's committed .maintainer.yml records as its reason for pinning auto-merge off on 2026-08-01.
This repo's current state
Verified live 2026-08-07. Public repo. No rulesets (rules/branches/main returns []). Classic protection: required_approving_review_count: 1, require_code_owner_reviews: false, require_last_push_approval: false, dismiss_stale_reviews: false, enforce_admins: false, one context bun (lint + typecheck + test). No CODEOWNERS (404).
This is the repo behind the whole spec — Din's #57 sits here, and its classic count of 1 is what the gate cannot currently see (#526).
Acceptance criteria
.agent-pipeline.json on the default branch, parsing cleanly, declaring repo and merge.reviewGate
Scope guard (fails in the UNSAFE direction).jq -r 'paths(scalars)|join(".")' .agent-pipeline.json | sort emits exactly merge.reviewGate and repo — nothing else. Goes red if the committed posture lands more permissive than specified (a stray merge.standingAuthorization, merge.classificationParks, policy.module, or a ci.requiredChecks naming a context that does not run on every PR). Every other criterion here only catches an incomplete migration; this one catches an over-broad one.
the committed repovalue equals this repo's full slug. governsRepo (config.ts:880) is an exact case-insensitive full-slug match, so a typo'd slug ships an inert config that passes every other criterion here while merge.reviewGate is never read (merge.ts:1893) and the cr-approved gate silently stands. Key presence is not enough.
main-protection ruleset with all four pull_request parameters at their target values
classic branches/main/protection returns the literal message Branch not protected — not merely "a 404". That endpoint requires repo admin, and a caller without it also gets a 404: {"message":"Not Found", … ,"status":"404"}. The two share the status code but differ in the message body (verified live 2026-08-07 against anthropics/claude-code for the non-admin case and ivndev001/agent-pipeline for the genuinely-unprotected case), which is exactly what makes matching the literal string sound and matching the bare status code unsound. Assert the message; route any other 404 to LIVE_TEST_INCONCLUSIVE, never PASS.
gh pr view <n> --json reviewDecision,mergeStateStatus returns an empty/nullreviewDecision. Null is the ci-only premise, not a failure (config.ts:106: the posture exists precisely because "reviewDecision is structurally null there"). A non-null value means the migration did not take.
Ruleset completeness (fails in the UNSAFE direction).gh api repos/<r>/rules/branches/main --jq '[.[].type]|unique|sort|join(",")' contains all three of deletion, non_fast_forward, pull_request. Classic protection carries allow_force_pushes: false and allow_deletions: false today; a pull_request-only ruleset that replaces it passes every review-related criterion here while leaving mainforce-pushable and deletable. That is the single most damaging way this migration can go wrong, and nothing else on this list detects it.
No bypass actors (fails in the UNSAFE direction). For every ruleset_id returned above, gh api repos/<r>/rulesets/<id> --jq '[.bypass_actors[]?]|length' returns 0. A bypass actor silently reopens every gate the ruleset appears to close. Reference shape, live on ivndev001/agent-pipeline: rule_types: [deletion, non_fast_forward, pull_request], bypass_actors: [], conditions.ref_name.include: ["~DEFAULT_BRANCH"], enforcement: "active".
gh pr view <n> --repo <repo> --json mergeStateStatus returns CLEAN on a real open PR — the only check that catches a surviving require_last_push_approval
live test below passes post-merge
AFK / HITL
HITL.
Not a preference — a mechanical fact. Step 1's PR parks at pipeline-config-park, which nothing lifts (see What to build). Step 2 is a GitHub settings mutation (branch ruleset + deletion of the classic rule): it is not a tree artifact, CI cannot gate it, and a revert of the merge commit does not reach it. Flipping this issue to afk does not make an unattended run able to finish it; it makes the run open a PR and then park.
Live test
set -uo pipefail
R=developerz-ai/ui-debugger-mcp
command -v jq >/dev/null || { echo"LIVE_TEST_INCONCLUSIVE: jq is missing on this runner";exit 1; }
cfg="$(gh api "repos/$R/contents/.agent-pipeline.json" -H "Accept: application/vnd.github.raw"2>&1)"; rc=$?if [ "$rc"-ne 0 ];thencase"$cfg"in*"Not Found"*|*404*)
# A contents 404 is ambiguous on a private repo: absent file, or a token that cannot# read CONTENTS at all (GitHub answers 404, not 403). Discriminate with a SECOND# contents read of a path that pre-exists this change - never the bare repos/$R# metadata route, which a contents-blind token passes forever.
ctl="$(gh api "repos/$R/contents/README.md" -H "Accept: application/vnd.github.raw"2>&1)"; ctlrc=$?if [ "$ctlrc"-ne 0 ];thenprintf'%s\n'"$ctl"| tail -c 200
echo"LIVE_TEST_INCONCLUSIVE: 404 for .agent-pipeline.json and the README.md control read failed too - a contents-blind token and an absent file are the same 404"exit 1
fi# Readable repo: a just-merged path can still 404 while the contents view serves the# pre-merge default branch. Budget is a floor, not a pick - 1333-8537 ms of positive-path# settle lag measured on agent-pipeline. Expiry is INCONCLUSIVE, never FAIL: it is# indistinguishable from a budget sized too small, and FAIL arms revert.
deadline=$((SECONDS +9))while [ "$SECONDS"-lt"$deadline" ];do
sleep 2
cfg="$(gh api "repos/$R/contents/.agent-pipeline.json" -H "Accept: application/vnd.github.raw"2>&1)"; rc=$?
[ "$rc"-eq 0 ] &&breakdoneif [ "$rc"-ne 0 ];thenecho"LIVE_TEST_INCONCLUSIVE: .agent-pipeline.json still 404s after a 9s settle budget with a readable repo - a human must tell a missing file from an unsettled view"exit 1
fi ;;
*) printf'%s\n'"$cfg"| tail -c 200
echo"LIVE_TEST_INCONCLUSIVE: could not read $R contents from this runner"exit 1 ;;
esacfi
gate="$(printf '%s'"$cfg"| jq -r '.merge.reviewGate // "absent"')"if [ "$gate"!="ci-only" ];thenecho"FAIL: .agent-pipeline.json declares merge.reviewGate=$gate, expected ci-only"exit 1
fi
slug="$(printf '%s'"$cfg"| jq -r 'if has("repo") then (.repo|tostring) else "absent" end')"if [ "$slug"!="$R" ];thenecho"FAIL: .agent-pipeline.json declares repo=$slug, expected $R - governsRepo (config.ts:880) is an exact case-insensitive full-slug match, so a mismatch ships an INERT config: merge.reviewGate is never read (merge.ts:1893) and the hard cr-approved gate silently stands"exit 1
fi
rs="$(gh api "repos/$R/rules/branches/main"2>&1)"; rsrc=$?if [ "$rsrc"-ne 0 ];thenprintf'%s\n'"$rs"| tail -c 300
echo"LIVE_TEST_INCONCLUSIVE: could not read $R branch rules from this runner"exit 1
fi
params="$(printf '%s'"$rs"| jq -c '[.[] | select(.type=="pull_request") | .parameters][0] // empty')"if [ -z"$params" ];thenecho"FAIL: $R main carries no pull_request ruleset after the migration"exit 1
fi# jq's `//` substitutes its RHS for `false` as well as `null`, and `false` is the TARGET# value for three of these four - so the `// "absent"` form can never yield the string# "false" the guard below tests for, and a flawless migration scores a hard FAIL. Use# has()/tostring: it yields "false" on the target, "absent" on a missing key and "true" on# a wrong value - both failure directions closed. A bare `| tostring` is NOT a substitute:# on an absent key it yields the string "null" on some jq builds, which is luck, not a guard.
count="$(printf '%s'"$params"| jq -r 'if has("required_approving_review_count") then (.required_approving_review_count|tostring) else "absent" end')"
owner="$(printf '%s'"$params"| jq -r 'if has("require_code_owner_review") then (.require_code_owner_review|tostring) else "absent" end')"
lastpush="$(printf '%s'"$params"| jq -r 'if has("require_last_push_approval") then (.require_last_push_approval|tostring) else "absent" end')"
dismiss="$(printf '%s'"$params"| jq -r 'if has("dismiss_stale_reviews_on_push") then (.dismiss_stale_reviews_on_push|tostring) else "absent" end')"if [ "$count"!="0" ] || [ "$owner"!="false" ] || [ "$lastpush"!="false" ] || [ "$dismiss"!="false" ];thenecho"FAIL: $R ruleset is not merge-ready (count=$count code_owner=$owner last_push=$lastpush dismiss_stale=$dismiss)"exit 1
fi
types="$(printf '%s'"$rs"| jq -r '[.[].type]|unique|sort|join(",")')"fortin deletion non_fast_forward pull_request;docase",$types,"in*",$t,"*) ;;
*) echo"FAIL: $R main ruleset is missing the $t rule (types=$types) - classic protection's force-push/deletion guarantees would be lost"exit 1 ;;
esacdoneforidin$(printf '%s'"$rs"| jq -r '[.[].ruleset_id]|unique|.[]');do
ba="$(gh api "repos/$R/rulesets/$id" --jq '[.bypass_actors[]?]|length'2>&1)"; barc=$?if [ "$barc"-ne 0 ];thenprintf'%s\n'"$ba"| tail -c 200
echo"LIVE_TEST_INCONCLUSIVE: could not read ruleset $id bypass actors from this runner (needs repo admin)"exit 1
fiif [ "$ba"!="0" ];thenecho"FAIL: $R ruleset $id carries $ba bypass actor(s) - every gate above is reopened for them"exit 1
fidone
prot="$(gh api "repos/$R/branches/main/protection"2>&1)"; prc=$?if [ "$prc"-eq 0 ];thenecho"FAIL: $R still carries legacy classic branch protection alongside the ruleset"exit 1
ficase"$prot"in*"Branch not protected"*) echo"PASS: $R is on the ci-only posture with a ruleset at count 0 and no classic protection" ;;
*) printf'%s\n'"$prot"| tail -c 300
echo"LIVE_TEST_INCONCLUSIVE: could not determine $R classic protection state"exit 1 ;;
esac
Rollback
Revert the .agent-pipeline.json commit and restore the classic protection rule from the values recorded above. No automation reverts this: the plugin's revert.armOnLiveTestFail is not set on any repo here, so a live-test FAIL escalates rather than reverting.
Blocked by
ivndev001/agent-pipeline#525 — the owner ruling that authorizes the fleet-wide ci-only flip.
This is a fail-open change (it relaxes required_approving_review_count 1 → 0, drops the required status-check context that classic protection enforces today, and makes a null reviewDecision count as an accept). The only committed ruling in range is a0c4b28 (2026-07-27), which by its own text says "The grant removes the human YES and nothing else" — so it does not authorize this. ivndev001/agent-pipeline#525 is the artifact that would, and it is still open. Per the parent epic's own discipline: the authorizing artifact precedes the code it authorizes. (That epic is referenced under ## Context below, deliberately not here — this section's refs are parsed as blockers, and blocking on a parent epic that stays open until its children close would be circular.)
Context
Part of ivndev001/agent-pipeline#524 — peer-PR merge lane and fleet-wide ci-only enablement.
What to build
Bring this repo onto the agent-pipeline
ci-onlyposture, so/merge-ci-onlyand/execute-prd-ci-onlycan actually reach a merge here. One PR plus one protection change, both scoped to this repo.Filed here rather than in the plugin repo deliberately: the deliverable is a commit in this tree, so the executor produces a real, non-empty PR — it clears the pipeline's
empty-diffblocker, which a zero-file PR filed elsewhere would not.The merge is an operator click, not pipeline work.
decideMergecarries an unconditionalpipeline-config-parkblocker keyed on/(^|\/)\.agent-pipeline\.json$/(gate.ts:1824-1833; class defined atgate-paths.ts:120-122). Its own text: "UNCONDITIONAL: no posture, standing authorization, reviewGate or merge.classificationParks lifts this park … EXIT: a human reviews the config change deliberately and merges it themselves — it is not pipeline work." The rationale (#485) is that this file is where asupplyblock names which issues the pipeline may treat as its own work scope — a config that grants scope, reachable by what it grants scope to — so it must never merge unread.An agent can therefore draft the config, open the PR and drive it to green CI. A human merges it. That is why this issue is HITL, and no label or config change makes it AFK.
1. Commit
.agent-pipeline.json(the PR)Minimum shape:
{ "repo": "developerz-ai/ui-debugger-mcp", "merge": { "reviewGate": "ci-only" } }For this repo, omit
ci.requiredChecksentirely — the scope-guard acceptance criterion below pins the committed config to exactly two scalar paths (repo,merge.reviewGate), and aci.requiredChecksarray adds a third. The general rule, for reference elsewhere: add it only if every context named runs on every PR — verify against a live PR head first. When unsure, omit it: an absentrequiredChecksfalls through toevaluateCiAllGreen, where every observed check must be completed and non-failing and a zero-check head fails closed. A named context that does not run scoresabsent (fail-closed)and parks every PR forever.2. Migrate branch protection to a
main-protectionrulesetdeletion+non_fast_forward+pull_request, with thepull_requestparameters at:required_approving_review_count: 0require_code_owner_review: falserequire_last_push_approval: falsedismiss_stale_reviews_on_push: falseThen delete the classic rule. Do not add
required_status_checksunless every listed context runs on every PR.Ordering matters and is not optional: the ruleset migration must land before the step-1 PR can merge, because the live classic rule still requires an approving review. This step is operator-run — it mutates repo settings out-of-band, so no PR contains it and no revert undoes it. Record the pre-change values (below) before touching anything.
All four parameters matter, not just the count — each is an independent merge blocker in GitHub's own semantics, so a repo migrated to count 0 with
require_last_push_approvalor stale-review dismissal left on passes a count-only check and stays blocked. That is the reason to set all four; it is derived from the API contract, not from a sample.Corrected fleet survey (verified live 2026-08-07 via
gh api repos/<r>/rules/branches/mainand.../branches/main/protection). An earlier revision of this issue claimed eight repos "all verify at exactly these values". That is false for seven of the eight:pull_requestruleivndev001/agent-pipeline0 / false / false / falseai-designer0 / false / false /truedb-mcp-gatewayrequired_status_checks)0 / false / false /truedeveloperz.aiminime-app0 / false / false /truemulti-ai-routervenom-astrology0 / false / false /truevenomtechmedellin.comOnly
ivndev001/agent-pipelinematches all four target values. Four repos sit at count 0 withdismiss_stale_reviews_on_push: true— so that parameter is not empirically load-bearing for reaching a merge, and the target shape here is the strict one by choice, not by precedent. Three repos are not migrated at all. Do not cite the fleet as evidence that this shape is already proven.What this trades, stated plainly
required_approving_review_count: 0does not delete review — it transfers it from a GitHub click to the plugin's two-lens deep-review gate plus fail-closed CI. On a repo at count 1 an approving bot review already satisfies the requirement, which is the specific hazarddeveloperz-ai/wurk's committed.maintainer.ymlrecords as its reason for pinning auto-merge off on 2026-08-01.This repo's current state
Verified live 2026-08-07. Public repo. No rulesets (
rules/branches/mainreturns[]). Classic protection:required_approving_review_count: 1,require_code_owner_reviews: false,require_last_push_approval: false,dismiss_stale_reviews: false,enforce_admins: false, one contextbun (lint + typecheck + test). No CODEOWNERS (404).This is the repo behind the whole spec — Din's #57 sits here, and its classic count of 1 is what the gate cannot currently see (#526).
Acceptance criteria
.agent-pipeline.jsonon the default branch, parsing cleanly, declaringrepoandmerge.reviewGatejq -r 'paths(scalars)|join(".")' .agent-pipeline.json | sortemits exactlymerge.reviewGateandrepo— nothing else. Goes red if the committed posture lands more permissive than specified (a straymerge.standingAuthorization,merge.classificationParks,policy.module, or aci.requiredChecksnaming a context that does not run on every PR). Every other criterion here only catches an incomplete migration; this one catches an over-broad one.repovalue equals this repo's full slug.governsRepo(config.ts:880) is an exact case-insensitive full-slug match, so a typo'd slug ships an inert config that passes every other criterion here whilemerge.reviewGateis never read (merge.ts:1893) and thecr-approvedgate silently stands. Key presence is not enough.main-protectionruleset with all fourpull_requestparameters at their target valuesbranches/main/protectionreturns the literal messageBranch not protected— not merely "a 404". That endpoint requires repo admin, and a caller without it also gets a 404:{"message":"Not Found", … ,"status":"404"}. The two share the status code but differ in the message body (verified live 2026-08-07 againstanthropics/claude-codefor the non-admin case andivndev001/agent-pipelinefor the genuinely-unprotected case), which is exactly what makes matching the literal string sound and matching the bare status code unsound. Assert the message; route any other 404 toLIVE_TEST_INCONCLUSIVE, never PASS.gh pr view <n> --json reviewDecision,mergeStateStatusreturns an empty/nullreviewDecision. Null is the ci-only premise, not a failure (config.ts:106: the posture exists precisely because "reviewDecision is structurally null there"). A non-null value means the migration did not take.gh api repos/<r>/rules/branches/main --jq '[.[].type]|unique|sort|join(",")'contains all three ofdeletion,non_fast_forward,pull_request. Classic protection carriesallow_force_pushes: falseandallow_deletions: falsetoday; apull_request-only ruleset that replaces it passes every review-related criterion here while leavingmainforce-pushable and deletable. That is the single most damaging way this migration can go wrong, and nothing else on this list detects it.ruleset_idreturned above,gh api repos/<r>/rulesets/<id> --jq '[.bypass_actors[]?]|length'returns0. A bypass actor silently reopens every gate the ruleset appears to close. Reference shape, live onivndev001/agent-pipeline:rule_types: [deletion, non_fast_forward, pull_request],bypass_actors: [],conditions.ref_name.include: ["~DEFAULT_BRANCH"],enforcement: "active".gh pr view <n> --repo <repo> --json mergeStateStatusreturnsCLEANon a real open PR — the only check that catches a survivingrequire_last_push_approvalAFK / HITL
HITL.
Not a preference — a mechanical fact. Step 1's PR parks at
pipeline-config-park, which nothing lifts (see What to build). Step 2 is a GitHub settings mutation (branch ruleset + deletion of the classic rule): it is not a tree artifact, CI cannot gate it, and a revert of the merge commit does not reach it. Flipping this issue toafkdoes not make an unattended run able to finish it; it makes the run open a PR and then park.Live test
Rollback
Revert the
.agent-pipeline.jsoncommit and restore the classic protection rule from the values recorded above. No automation reverts this: the plugin'srevert.armOnLiveTestFailis not set on any repo here, so a live-test FAIL escalates rather than reverting.Blocked by
ivndev001/agent-pipeline#525— the owner ruling that authorizes the fleet-wideci-onlyflip.This is a fail-open change (it relaxes
required_approving_review_count1 → 0, drops the required status-check context that classic protection enforces today, and makes a nullreviewDecisioncount as an accept). The only committed ruling in range isa0c4b28(2026-07-27), which by its own text says "The grant removes the human YES and nothing else" — so it does not authorize this.ivndev001/agent-pipeline#525is the artifact that would, and it is still open. Per the parent epic's own discipline: the authorizing artifact precedes the code it authorizes. (That epic is referenced under## Contextbelow, deliberately not here — this section's refs are parsed as blockers, and blocking on a parent epic that stays open until its children close would be circular.)Context
Part of
ivndev001/agent-pipeline#524— peer-PR merge lane and fleet-wide ci-only enablement.