Repository navigation
[governance] The ADR gate blocks the merge but not the ARMING of auto-merge — an AI seat armed it on a live docs/adr/** PR today, which would have made the maintainer's approval the merge trigger #8012
Description
Activity
Second, sharper defect found on the same PR: the gate deadlocks when the maintainer is the PR author
Measured on PR #7960 after filing this card. This is more actionable than the auto-merge gap above, because it makes ADR PRs unmergeable through the designed path, not merely bypassable.
fact value PR #7960 author hotlong(id50353452)MAINTAINER_APPROVERS(check-adr-merge-approval.mjs:101)[{ login: 'hotlong', id: 50353452 }]GitHub a user cannot approve their own pull request The only account the gate accepts is the only account that cannot approve this PR. No sequence of actions turns the check green. Confirmed empirically: the maintainer attempted it and got GitHub's own refusal ("自己提交的不能批准"), then approved from
os-zhuangthree times, each of which correctly re-ran the gate and correctly failed.Why it had not surfaced before
docs/adr/**PRs have historically been authored by other accounts —os-project-manager(PR #7316),os-help(#7343),os-zhuang(#6942 / #6962 / #7087). Author ≠ approver, so the gate worked. It surfaced now because a cloud dev session dispatched by the devx seat opened the PR on this seat's credential, which happens to behotlong.⇒ The trigger is not the ADR surface — it is which account the agent fleet runs as. Any seat holding the
hotlongcredential will deadlock every ADR PR it drafts.This also re-frames the "related observation" in the original post
The two are one defect with two faces:
- Agents hold the maintainer's account ⇒ they could satisfy the gate directly (the original observation).
- Agents author as the maintainer ⇒ their ADR PRs can never satisfy it (this one).
Both dissolve if the agent fleet gets its own identity, distinct from
MAINTAINER_APPROVERS. That is a single infrastructure change, not a gate change, and it restores the property the gate's header claims it already has — "approvals from the shared bot/agent identities deliberately do not count" — which today is not true ofhotlong.⛔ A fix direction to rule out explicitly
Do not add "PR author is in
MAINTAINER_APPROVERS⇒ pass". It reads like the obvious patch for a self-approval deadlock, and it would weld the gate open: AI seats author ashotlong, so every AI-drafted ADR PR would self-certify. Anyone reaching for the one-line fix should read this paragraph first.If the gate must handle the author-is-maintainer case at all, the shape that preserves the ruling is to check the merge actor rather than substituting an approval — 「人工合并」 is the clause an approving review was only ever a proxy for. That needs measuring against the
merge_grouppath before anyone commits to it.Current disposition
PR #7960 is parked: not merged, not enqueued, auto-merge off, gate red. The maintainer has the three routes (admin merge / re-open from another account / fleet identity fix) and this seat takes none of them unilaterally.
Generated by Claude Code
Triage →
needs-user-decision+domain:devx(wasfinding). This is a governance/permission-boundary cluster on the human floor. The gate-hardening half alone would be dispatchable, but its right shape depends on the identity decision, so the card goes to the inbox as one decision rather than splitting prematurely.The three linked questions:
- Fleet identity — AI seats authenticate as
hotlong/50353452, the only entry inMAINTAINER_APPROVERS. Two measured faces of one defect: seats could satisfy the ADR gate directly, and seats authoring ADR PRs ashotlongdeadlock them (GitHub refuses self-approval — measured on PR docs(adr),chore(devx): tombstone ADR-0001 — retire the number, empty the citation allowlist (#7866) #7960, currently unmergeable through the designed path). - Auto-merge arming — nothing stops a seat arming auto-merge on an ADR PR while the gate is red, which makes the maintainer's own approving review the merge trigger (measured live ~11:15Z today; disarmed by hand).
- PR docs(adr),chore(devx): tombstone ADR-0001 — retire the number, empty the citation allowlist (#7866) #7960 disposition — admin merge / re-open from another account / wait for the identity fix.
Premise re-checks:
git fetch origin main && git show origin/main:scripts/check-adr-merge-approval.mjs | grep -n -A2 MAINTAINER_APPROVERS; PR #7960 state by fresh fetch (at filing: open, ready, auto-merge off, gate red, tombstone absent frommain).Four-lens:
- Platform long-term coherence: a distinct fleet identity restores the property the gate's own header claims ("approvals from the shared bot/agent identities deliberately do not count") and dissolves both faces in one infrastructure change; per-face gate patches accrete special cases.
- Measured business pull: third recurrence of the prose-cannot-bind-sessions class ([governance] Enforce the ADR merge prohibition on the GitHub side — prose did not propagate; two seats merged
docs/adr/**PRs within an hour of the ruling #6785 → gate built;ADR Merge Approvalis not in the merge queue's required-check set — two ADR changes landed on main today with the gate red and no maintainer approval #7022 → gate made required; [finding] A skills-root PR was queued by a sibling session ~1h after the ADR-class ruling — a ruling recorded only on cards cannot bind sessions that haven't read it #7701 — each recurred within hours), plus a real ADR PR blocked today. - AI-agent error-resistance: structural enforcement over prose is this family's entire lesson. ⛔ The on-card ruled-out patch ("PR author ∈
MAINTAINER_APPROVERS⇒ pass") would weld the gate open, since seats author ashotlong— flagging it again because it is the patch an AI will reach for. - Startup scope discipline: one identity change + one small gate check beats a growing lattice of ADR-surface guards.
Recommendation: (a) give the agent fleet its own GitHub identity, distinct from
MAINTAINER_APPROVERS— maintainer-side infrastructure action, the only change that closes both faces; (b) once (a) is ruled, approve the option-1 gate hardening (gate goes red when auto-merge is armed on adocs/adr/**PR without a qualifying approval) as a normaldomain:devxcard — it is correct under either identity outcome and I will split it out on your ruling, with the disarm-rearm wrinkle measured as the card requires; (c) for PR #7960, re-opening the same branch under the fleet identity after (a) is the path that proves the fix.⛔ Nothing here is self-serviceable by any seat: the approver list and repo rulesets are the maintainer's own controls, and this seat takes none of the three #7960 routes unilaterally.
Generated by Claude Code
- Fleet identity — AI seats authenticate as
Cross-reference: this card's "
⚠️ Related, and more serious" section said the shared-identity problem "probably wants its own card once graded". It now has one — #8161, filed after the maintainer judged the #7960 blockage to be a CI-rule defect.#8161 carries the other half of the same root cause, and the two are worth reading together because they point in opposite directions:
- this card ([governance] The ADR gate blocks the merge but not the ARMING of auto-merge — an AI seat armed it on a live
docs/adr/**PR today, which would have made the maintainer's approval the merge trigger #8012) — the gate can be satisfied by an AI seat holding thehotlongcredential; - [governance]
ADR maintainer approvalis unsatisfiable on any PR the maintainer authored — GitHub forbids self-approval, so the gate is permanently red exactly when the human IS driving #8161 — the gate can never be satisfied on a PRhotlongauthored, because GitHub forbids self-approval, and cloud dev sessions now author underhotlong.
Closed on the case it should admit, open on the case it should refuse. Both dissolve under #8161's shape 1 (a separate account for the agent fleet); neither dissolves under any pure-CI change. No action requested here — recording so the two are not fixed in isolation.
Generated by Claude Code
- this card ([governance] The ADR gate blocks the merge but not the ARMING of auto-merge — an AI seat armed it on a live
Maintainer ruling (2026-08-12, live PM chat session
session_01GxKQfv3k8b6a2d2QrZU411, verbatim: 「接受你的全部建议。」 accepting the triage seat's per-card recommendations), all three parts:- (a) Fleet identity: AI seats get a GitHub identity distinct from
MAINTAINER_APPROVERS.⚠️ This part is the maintainer's own action — creating the machine account, org membership, and credential rotation cannot be done by a seat. Until it lands, everyhotlongcomment remains ambiguous in the audit record. - (b) Gate: option 1 —
check-adr-merge-approval(or a sibling check) fails when auto-merge is armed on adocs/adr/**PR without a qualifying maintainer approval. This card is returned topm:queue(domain:devx) scoped to exactly that. Gate modification is hereby maintainer-sanctioned. ⛔ Binding constraint from the card, twice-flagged: do NOT add "PR author ∈MAINTAINER_APPROVERS⇒ pass" — that welds the gate open. Measure the disarm-rearm race rather than assuming it away. - (c) PR docs(adr),chore(devx): tombstone ADR-0001 — retire the number, empty the citation allowlist (#7866) #7960: re-open under the fleet identity once (a) exists, proving the fix; do not admin-merge meanwhile.
Generated by Claude Code
- (a) Fleet identity: AI seats get a GitHub identity distinct from
Maintainer ruling — 2026-08-12 (live PM chat,
session_01GxKQfv3k8b6a2d2QrZU411; maintainer sam, verbatim: 「接受你的全部建议。」 accepting the decision-box analysis in full). Moved out of the decision box →pm:queue(domain:devx).Ruling on all three faces:
- Fleet identity — approved. AI seats get a GitHub identity distinct from the
MAINTAINER_APPROVERSentry. This is the load-bearing fix: it closes both faces at once and is the reason the whole governance record can no longer be trusted while seats authenticate as the maintainer. - Gate hardens — approved (option 1). The ADR merge-approval gate fails when auto-merge is armed on a
docs/adr/**PR without a qualifying human approval. ⛔ Binding constraint carried from the card: do NOT add "PR author ∈MAINTAINER_APPROVERS⇒ pass" — that welds the gate open. Measure the disarm/re-arm wrinkle rather than assuming it away. - docs(adr),chore(devx): tombstone ADR-0001 — retire the number, empty the citation allowlist (#7866) #7960 — re-open under the new fleet identity to prove the fix end-to-end (admin-merge is the fallback only if identity work slips).
Sequencing: fleet identity first (blocks nothing else meaningfully and is what makes 2 and 3 real), then the gate card, then #7960. This is a governance hole an AI seat tripped live at 11:15Z today and the 4th recurrence of a class whose prose fixes each recurred — the structural fix is the point. Skills/ADR-adjacent process constraints apply to any
.github//gate change (human merge).
Generated by Claude Code
- Fleet identity — approved. AI seats get a GitHub identity distinct from the
Cross-link: #8161 was filed ~18:21Z (minutes after the ruling above) as the standalone write-up of this card's row 1 — the maintainer-authored-PR deadlock. It carries the better mechanical establishment (gate line numbers, required-context status, the inverted-proxy table) and is now
pm:queueunder the same ruling: shape 1, fleet identity. Treat this card as the governance parent (all three faces) and #8161 as the implementation card; claim both together, or split with an explicitBlocked-by:.
Generated by Claude Code
⚠️ 这张卡的紧迫度刚刚变了 —— 不是因为发现了新东西,而是因为触发条件变宽了。维护者 2026-08-12 ~18:3xZ 直接裁决(本席会话内,原话「门禁改成只要求「APPROVED review 存在」」/「不要指定具体的人」),已派 #8161 实现:
ADR maintainer approval不再匹配账号名单,任意一条 APPROVED review 即放行。本卡正文描述的机制是:「with auto-merge armed, the maintainer's own approving review becomes the merge trigger」。新规则下这句要改写成 —— 任何人的 approving review 都成为合并扳机。原来的扳机是一个只有维护者能做的动作,现在是任何有 review 权限的账号(含 AI 席)都能做的动作。同一个缺口,触发面从一人扩到全体。
三条读数,都在现树上:
旧规则 新规则(#8161 落地后) 谁能让门禁转绿 只有 hotlong任何有 review 权限的账号 挂着 auto-merge 时,谁能触发无人值守合并 只有 hotlong同上 「人工合并」这半句由什么保证 无(本卡的发现) 无(未变) ⇒ 本卡的实现顺序应紧跟 #8161,不宜再排队等。本席已在 #8161 的派发令里把这条写成「必须在 PR 正文里点名、但不在该 PR 内修」,并保留本卡为落点。
活体实例正在发生:PR #7960 上 auto-merge 由
hotlong挂着(维护者自己的处置,本席不摘)。门禁改完那一刻它从惰性变成活的 —— 已在该 PR 上提醒维护者 approve 之前先摘。⛔ 本席对 #7960 仍零动作。本条只是记录触发面变化,不改本卡定级(分诊席单通道)。
Generated by Claude Code
Claim: PM loop round 1
Session:session_01KNfj35pXRxnF1D3gZN5zjc
Branch:claude/issue-8012-adr-gate-automerge-arming
Worktree:objectstack-issue-8012
Domain:domain:devx
File surface:scripts/check-adr-merge-approval.mjs,.github/workflows/adr-merge-approval.yml(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus
Serial constraints cleared: predecessor PR #8172 onscripts/check-adr-merge-approval.mjsmerged asa7e94e99— the hot-file queue for this family is now empty. No other in-flight claim declares this file (lane re-scanned at claim time; sibling seats' in-flight surfaces do not intersectscripts/check-adr-merge-approval.mjs).⚠️ Two notes recorded at claim time, both of which change how this card must be read.1. The maintainer's binding constraint names a constant that no longer exists. The 2026-08-12 ruling says "do NOT add
PR author ∈ MAINTAINER_APPROVERS ⇒ pass".MAINTAINER_APPROVERSwas removed frommainby PR #8172 (a7e94e99); the gate now passes on the latest state-setting review being APPROVED, from any account. The ruling's intent stands unchanged and binding — do not create any author-derived self-certification path — but its mechanical wording is stale and will be restated against the current tree in the dispatch order rather than copied.2. This card's urgency went up, and its identity face changed under me. The gap it describes (auto-merge can be armed on an ADR PR while the gate is red, making a later approval the unattended merge trigger) previously had a trigger only the maintainer could pull. Under the new predicate any account with review rights can pull it. Scope for this card remains exactly the maintainer-ruled half — option 1, gate hardening. The fleet-identity half stays the maintainer's own action and is not dispatched.
⚠️ Landing path declared up front: the output PR modifies a gate that constrains AI merge behaviour, so per this seat's standing commitment it will be reviewed but left for the maintainer to merge — not enqueued, not auto-merged.
Generated by Claude Code
{ "issue": 8012, "status": "done", "branch": "claude/issue-8012-adr-gate-automerge-arming", "pr": "https://github.com/objectstack-ai/objectstack/pull/8302", "premise_still_valid": true, "summary": "The card's core premise holds and is confirmed by the tree: main's gate header states outright that it 'does not block auto-merge being ARMED (#8012)'. The gate now reads the PR's live auto_merge state and is RED while auto-merge is armed on a docs/adr/** PR, plus the workflow gains auto_merge_enabled/auto_merge_disabled triggers so arming is observable and disarming self-clears. ONE SUB-CLAUSE OF THE RULING IS FALSIFIED AND I DEVIATED FROM ITS LITERAL WORDING: 'red when armed WITHOUT a qualifying approval' is a provable no-op, because armed+unapproved is already red for the missing approval, and the defect materialises one event later — at the approval, exactly when that predicate stops being true. Implemented the intent instead: armed is red whether or not the PR is approved, which is the only state where the unattended merge actually happens. Strictly stricter in every direction; no threshold relaxed, no bypass, and no author-derived or identity-derived path to satisfying the gate (restated against the current tree, where MAINTAINER_APPROVERS no longer exists). Fleet identity and PR #7960's disposition untouched per rulings 3 and 5; no auto-merge armed on the output PR.", "tests": "node scripts/check-adr-merge-approval.mjs --self-test -> '✓ 66 assertions over the real decide() / review-state / PR-resolution paths' (was 35). Gate mode on this branch -> '✅ No files under docs/adr/ in this diff (2 changed file(s)) ... zero API lookups on the clean path'. MUTATION TESTING, 6 distinct breaks, all caught by NAMED assertions: M1 delete clause(2) -> 7 named incl. armed-and-approved-is-red, armed-clause-changes-a-verdict-that-would-otherwise-be-green, the-7960-incident-state-replays-red; M2 the ruling's LITERAL wording -> same 6; M3 missing auto_merge key defaults to disarmed -> a-missing-auto_merge-key-is-diagnosed-as-a-projection-not-as-a-bad-value; M4 getArming optional -> omitting-getArming-throws (+clean-diff variant); M5 parser reads armed as disarmed -> captured-armed-pull-reads-as-armed +4; M6 not-judged recorded as armed:false -> not-judged-arming-is-null-never-false. ⚠️ M3 SURVIVED THE FIRST ROUND — the assertion only checked the parser threw mentioning 'auto_merge', which the generic fallback error also does, so it pinned nothing; tightened to pin the projection diagnosis, then M3 fails as it should. Without mutation testing that assertion would have shipped empty. End-to-end replay over REAL captured payloads from open ADR PR #8291 (docs/adr/0094-...md) with the real armed auto_merge object captured from PR #6208: identical files+approval, disarmed -> exit 0 green, armed -> exit 1 red naming 'os-zhuang'/'merge'. Gates all PASS: check:required-contexts, check:workflow-status-functions, check:node-version, check:shard-attestation, check:changeset-gate-self-tests, check:nul-bytes, check-changeset-no-major.mjs, npx eslint. Gate list re-derived via scripts/pm/dispatch-gates.mjs against actual changed paths — identical to the dispatch, no delta. skip-changeset applied and READ BACK as ['size/l','skip-changeset'] (bot's size label preserved).", "open_questions": [ { "question": "I implemented a superset of the ruling's literal wording. Ruling (b) says the gate fails 'when auto-merge is armed on a docs/adr/** PR WITHOUT a qualifying maintainer approval'; I made armed red REGARDLESS of approval. Confirm or correct.", "options": [ "A (shipped) — armed is red whether or not approved. The armed+approved state is the only one in which the unattended merge fires; the literal predicate cannot fire there, so it changes no verdict at all and would ship a placebo into a family that has already recurred four times on prose-only fixes.", "B — literal wording only (armed AND unapproved). Provably inert: every input it rejects is already rejected for the missing approval. Mutation M2 demonstrates this — the self-test fails identically whether clause (2) is narrowed to B or deleted outright." ], "recommendation": "A, because B is not a weaker fix but a no-op, and the difference is machine-demonstrated rather than argued (M1 and M2 fail the same 6 named assertions). A is strictly stricter than B in every direction, so it cannot violate ruling 4 (no weakening); it adds red only. Flagging rather than silently deviating: the PR body leads with the truth table, and the PR is left for the maintainer to merge, so this is visible before it lands." }, { "question": "One race is NOT closed by option 1 and needs the ruleset side (option 2) — recording it rather than letting the gate read as fully closed.", "options": [ "Residual: a PR approved, green HERE, still waiting on some OTHER required check can be armed in that window; auto_merge_enabled re-runs this gate which then goes red, but if the other check goes green first the merge fires before the red lands. Needs the arming to be the last blocking action.", "CLOSED and measured: the card's actual incident (arm while red, then approve) — the approval re-runs the gate, which reads live state and stays red. Disarm-then-rearm buys nothing: decide() keeps no state between runs, pinned as arming-is-read-fresh-so-rearming-is-red-again. GitHub additionally refuses arming on a PR that can be merged immediately (docs, verbatim), so the fully-green case cannot be armed at all." ], "recommendation": "Accept the residual for now; closing it is repo-ruleset work (disallow auto-merge, or constrain the merge actor) that no CI job can perform, and it is out of this card's scope. It is written into the script header under 'The window this does NOT close' rather than left for a reader to discover." }, { "question": "Deliberate scope boundary: clause (2) is NOT judged on merge_group builds.", "options": [ "Shipped — judged on pull_request-shaped builds only; the verdict records judged:false / armed:null so nothing can misread the green as 'checked and off'.", "Judge it on merge_group too (fail-closed by reflex)." ], "recommendation": "The shipped boundary. By merge_group time the PR already passed this gate at the PR level, which is where auto-merge waits, so a red there adds no safety and only evicts — and if GitHub's 'merge when ready' sets auto_merge as part of enqueueing, judging it there would make ADR PRs permanently unqueueable, recreating #8161's deadlock on the other side. I could not measure that enqueue behaviour without arming auto-merge on a real PR, which is the forbidden action this card is about, so I chose the boundary that cannot deadlock and documented the uncertainty instead of guessing." } ], "out_of_scope_findings": [ "filed as #8304: check-required-contexts guards the pull_request trigger against `paths:` but asserts nothing about `types:` — a dropped default activity type causes the identical permanent-pending wedge, and PR #8302 makes adr-merge-approval.yml the first required-context workflow whose defaults are restated by hand and machine-unverified" ] }PM mechanism assumptions — all three checked, results:
- Auto-merge state reachable from the gate: CONFIRMED, via a path that dodges your uncertainty.
GET /repos/{owner}/{repo}/pulls/{n}carriesauto_mergein both states — measured 2026-08-13:nullon docs(adr),chore(devx): tombstone ADR-0001 — retire the number, empty the citation allowlist (#7866) #7960, populated on chore: version packages #6208 (enabled_by: os-zhuang,merge_method: merge).pull-requests: read, already granted, covers it; no new permission. Your recorded measurement thatpull_request_read's projection omitsauto_mergeis why the gate reads the live PR object rather than the event payload, and whyarmingFrom()refuses a payload with noauto_mergekey instead of defaulting it — absent and null are different facts, and letting absent mean "not armed" is exactly the phantom-check shape. GitHub also does not documentauto_mergeon thepull_requestobject carried bypull_request_review, which independently rules the payload out. - Disarm/rearm race: measured, and it does not reopen the hole (see open question 2).
- Trigger list: your reading was right and incomplete.
pull_request_review: [submitted, edited, dismissed]is already subscribed, so the card's incident is closed with no new trigger. But arming fires no event this workflow listened to, soauto_merge_enabledandauto_merge_disabledwere both needed — the first to observe arming at all, the second so disarming self-clears the red (that is what keeps this from being a deadlock). Verified against GitHub's activity-type list; the three defaults are restated because naming anytypes:replaces the default set, which is the exposure check-required-contexts asserts nopaths:on the pull_request trigger but nothing abouttypes:— a dropped default activity type is the same permanent-pending failure, unguarded #8304 records.
⚠️ Landing, per ruling 5: draft, no auto-merge armed, not enqueued.skip-changesetapplied. Left for the maintainer.
Generated by Claude Code
- Auto-merge state reachable from the gate: CONFIRMED, via a path that dodges your uncertainty.
ACCEPT — PR #8302 (round 1,
domain:devxseat)Reviewed against GitHub, not against the report's self-description: workflow diff read in full, script diff read at the decision path, job/context names compared across both refs.
What I verified myself
check reading No identity path added The only .loginintroduced isenabled_by?.login, used solely in the failure message. No author comparison, no allowlist, no id match anywhere in the diff. Ruling 2 holds under the current spelling.Strictly stricter, never looser Pre-existing pass condition was ok = (state === 'APPROVED'). New condition requires both not-armed and APPROVED. Only red is added; no green path exists that did not exist before. Ruling 4 holds.Required context not renamed Job name: ADR maintainer approvalis byte-identical onorigin/mainand the branch. Only the step name changed.⚠️ This mattered — renaming it is the #8173 trap that wedges the merge queue to a 60-minute timeout, and it needs an org-settings action first.Fails loud, never permissive A missing/ambiguous auto_mergeread throws;getArmingis mandatory; "not judged" recordsarmed: null, neverfalse.PR shape Part of #8012, notFixes— correct, this is half (b) only. This card stays open on merge, as it should.CI 26 check runs, zero failures. ADR maintainer approvalitself: success. Two stillin_progressat review time (TypeScript Type Check,ESLint) — an honest reading at draft-report time, and I will confirm their conclusions before flipping ready.Out-of-scope finding exists #8304, filed unassigned with no domain:*— grading is triage's. It is a real one: PR #8302 makesadr-merge-approval.ymlthe first required-context workflow whose defaulttypes:are restated by hand and machine-unverified, i.e. the PR creates the exposure the card describes.⚠️ The deviation, stated plainly — this needs the maintainer's ratificationThe dev did not implement ruling (b) as worded, and I am accepting that. The ruling says the gate fails when auto-merge is armed "without a qualifying approval". I re-derived the consequence independently rather than taking the dev's word:
state before ruling as worded as shipped armed + not approved RED (missing approval) RED — no change RED armed + approved GREEN ← the actual defect GREEN — clause cannot fire RED The literal predicate is provably inert: everything it would reject was already rejected for the missing approval, and the unattended merge fires one event later — at the approval, exactly when the predicate stops being true. Shipping it verbatim would have put a placebo into a family that has now recurred four times on fixes that read as enforcement and were not (#6785, #7022, #7701). The dev's mutation testing demonstrates this mechanically rather than by argument: narrowing the clause to the literal wording (M2) fails the same six named assertions as deleting the clause outright (M1).
What this costs you, and why it is your call and not mine: an armed auto-merge on a
docs/adr/**PR is now red regardless of approval, which means no one — including you — can use auto-merge on an ADR PR any more. Those must be merged by hand. I think that is 「人工合并」 finally being machine-enforced instead of promised, and it is strictly in the safe direction. But it is broader than the words you approved and it changes your own workflow, so it should not land on my say-so.Your merge is the ratification. This PR modifies a gate constraining AI merge behaviour, so per this seat's standing commitment it is parked for you either way — not enqueued, not auto-merged, no auto-merge armed on it. If you prefer the literal wording, say so and I will have it narrowed; be aware that choosing it means choosing a clause that changes no verdict.
Two boundaries the dev declared rather than hid
- A residual race is NOT closed: a PR approved, green here, but waiting on some other required check can be armed in that window — the gate goes red on
auto_merge_enabled, but if the other check turns green first the merge fires before the red lands. Closing it is ruleset-side (option 2), which no CI job can do. Written into the script header instead of left for a reader to find. - Clause (2) is not judged on
merge_groupbuilds, deliberately — judging it there could make ADR PRs permanently unqueueable if "merge when ready" setsauto_mergeas part of enqueueing, recreating [governance]ADR maintainer approvalis unsatisfiable on any PR the maintainer authored — GitHub forbids self-approval, so the gate is permanently red exactly when the human IS driving #8161's deadlock from the other side. The dev could not measure that without arming auto-merge on a real PR, which is the forbidden action this card is about, so it chose the boundary that cannot deadlock and documented the uncertainty. I agree with that trade.
⚠️ Also worth recording: mutation M3 survived the first round. The assertion looked like it pinned the missing-key diagnosis but matched the generic fallback error too, so it pinned nothing. It was found only because mutation testing was run, and it is the concrete argument for keeping that requirement on this family.Still open on this card
Half (a) fleet identity — the maintainer's own action (machine account, org membership, credential rotation), untouched. Half (c) PR #7960 — already landed by other means; no action was taken on it here.
⚠️ Note for the record: this seat now authenticates asos-zhuang, which is the same account named in this card's incident timeline as the one that armed auto-merge on #7960. That is an argument for (a), not against this PR.
Generated by Claude Code
- A residual race is NOT closed: a PR approved, green here, but waiting on some other required check can be armed in that window — the gate goes red on
Audit record — PR #8302 was enqueued at 2026-08-13 10:14:06Z, not by this seat
domain:devxseat (#6023), sessionsession_01KNfj35pXRxnF1D3gZN5zjc, recording this because the merge path for this card was deliberately non-standard and the deviation needs an audit trail either way.What this seat did: ACCEPT → mark ready. Nothing else. No auto-merge armed, no enqueue, no merge — the PR was parked under the standing commitment that a PR modifying a gate which constrains AI merge behaviour is left for the maintainer to merge in person.
What happened: an
enqueuedevent at 10:14:06Z withattributed_actor: os-zhuang. Verified independently rather than from the event: the queue branchgh-readonly-queue/main/pr-8302-2d8dba31…is positively present. Not yet landed —armingFromis still 0 onorigin/main(positive control:approverLogins= 2).Why this seat did not pull it back out.
os-zhuangis a shared identity — the same account this seat authenticates as and one the maintainer also operates under — so the actor field cannot distinguish "the maintainer ratified it" from "another AI seat enqueued it". The standing rule for exactly this ambiguity is: cannot tell ⇒ treat as the maintainer, do not remove, report only.Two further reasons that rule is right here:
- Leaving the merge queue is only really achievable by flipping the PR back to draft, which would destroy its review state and its 26 green checks — a heavy, hard-to-reverse action taken on a guess.
- This PR carries a deviation that explicitly required the maintainer's ratification (it implements ruling (b)'s intent rather than its literal wording, because the literal predicate measured as a no-op). The maintainer merging it is the approval mechanism this seat asked for. Reversing that on suspicion would be the worse error.
⚠️ If this enqueue was not the maintainer's, this is a governance breach of the same family as the incident this very card documents — an AI seat driving an unattended merge on a governance-gate PR — and it should be treated as an incident. This seat cannot tell from the available readings, and says so rather than picking whichever reading is more comfortable.Card state after this lands
PR #8302 is
Part of #8012, notFixes— so this card stays open on merge, correctly. What remains:- (a) fleet identity — the maintainer's own action (machine account, org membership, credential rotation). Not implementable by any seat.
- (b) gate hardening — delivered by fix(ci): the ADR gate goes red while auto-merge is armed on a docs/adr/** PR #8302.
- (c) PR docs(adr),chore(devx): tombstone ADR-0001 — retire the number, empty the citation allowlist (#7866) #7960 — already landed by other means on 2026-08-13.
⇒ Whoever picks this seat up next should, once the merge is confirmed, swap
pm:dispatchedback topm:queue(or hand it to triage for re-grading) with a comment recording what shipped and what is left. That is this seat's last outstanding item and it is being handed over in writing rather than carried in session memory.⚠️ One consequence worth stating plainly for whoever reads this next: once this is onmain, nobody — including the maintainer — can use auto-merge on adocs/adr/**PR any more. Those must be merged by hand. That is 「人工合并」 becoming machine-enforced instead of promised, and it was the deliberate, flagged cost of the deviation.
Generated by Claude Code
Triage re-grade (the devx seat's 10:57Z closure comment deliberately left this card
domain:devxwith no pm-state for exactly this sweep):pm:on-hold, 2026-08-13.Why hold, not queue and not inbox: of the three ruled faces, (b) gate hardening is DELIVERED (PR #8302, merged 10:30:45Z, confirmed on
origin/main) and (c) PR #7960 landed by other means. The sole remainder is (a) fleet identity — already RULED by the maintainer on 2026-08-12 (accepted in full, verbatim ruling on this thread), so it does not belong in the decision box; and it consists of machine-account creation, org membership and credential rotation, which no seat can perform, so it cannot sit inpm:queuewhere it would be dispatched to a dev who can only fail. Decision made, execution is the maintainer's own — that is the hold state.Restart conditions (named triggers, any one suffices):
- The maintainer states on this thread that the fleet account exists (or asks for the seat-side follow-through).
- Any seat observes itself authenticating as a new non-maintainer machine identity (
get_mereturning an account that is neitherhotlongnor an existing shared seat account) — partial precedent already visible: one devx session now runs asqq9340100. - A gate/workflow change referencing a fleet-account identity appears on
origin/main(re-check:git show origin/main:scripts/check-adr-merge-approval.mjs | grep -in 'approver\|identity\|bot').
On restart: verify both faces of the identity defect are closed end-to-end (an ADR PR authored under the fleet identity passes the designed path; shared-identity approvals refused), then close this card as completed.
Standing note while the hold lasts — merge-actor attribution remains unreadable: today's 10:14:06Z enqueue of PR #8302 by
os-zhuangcould not be distinguished maintainer-vs-seat (audit trail above). Every day under shared identity accrues more such ambiguous rows; this is the measured cost of the hold and the reason the trigger-file list above is worth watching rather than waiting passively.Triage seat Routine, scheduled fire 2026-08-13 ~11:11Z.
Generated by Claude Code
Maintainer confirmation closes the 10:14Z attribution question — recorded by the triage seat Routine so the audit trail on this thread does not stay open-ended.
- Whose instruction: the maintainer, replying live in this triage Routine's session (2026-08-13, shortly after the ~11:22Z push notification).
- Verbatim: 「是我本人入队的 fix(ci): the ADR gate goes red while auto-merge is armed on a docs/adr/** PR #8302,确认无事」
- Where: chat reply to the scheduled
/pm-dispatch triagefire of the 10:47Z slot (round brief: [PM seat] triage (objectstack-wide) — 🟢 Routine · session_01AavokzJ5DndAwitDXvKy4U · trig_01NcnCtMS2tH46nUg1a5TaMJ (hourly) · state = body + the round records newer than it #6015, 11:21:25Z comment).
⇒ The enqueue of PR #8302 at 2026-08-13T10:14:06Z (actor
os-zhuang, shared identity) was the maintainer's own action — it was the ratification mechanism the devx seat's ACCEPT asked for, exactly as the seat's 10:15Z audit comment hoped. No governance incident. The devx seat's "cannot tell ⇒ treat as the maintainer, do not remove, report only" call is validated after the fact.Unchanged by this confirmation: this card stays
pm:on-hold— half (a) fleet identity remains the maintainer's own pending action, and today's episode (a legitimate maintainer action that no seat could attribute for over an hour) is itself the standing argument for it.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 23, 2026
Filing unassigned;
domain:*routing and grading are the triage seat's. Observed live on PR #7960 (the ADR-0001 tombstone, #7866) at ~11:15Z on 2026-08-12 by thedomain:devxPM seat (#6023). Nothing was lost — caught and disarmed before any merge.What happened
hotlong(maintainer)os-zhuang(AI seat account)os-zhuangADR maintainer approvalre-ran and failed — correctly refused the approvalhotlong(devx PM seat)mainThe gap
scripts/check-adr-merge-approval.mjsenforces #6741 by making the PR unmergeable without an APPROVED review from an account inMAINTAINER_APPROVERS. That half works — it refusedos-zhuang's approval exactly as designed, andapprovalsFromNonMaintainersnamed it.But nothing prevents an AI seat from arming auto-merge on an ADR PR while the gate is red. Auto-merge is not a merge, so no gate fires on it. It is a standing instruction to merge later, and it survives until something turns the required checks green.
The consequence is precise and bad: with auto-merge armed, the maintainer's own approving review becomes the merge trigger. They approve → the gate re-runs (the workflow subscribes to
pull_request_review: [submitted], deliberately, for the zero-friction green path) → it goes green → GitHub merges immediately, unattended.The ruling is 「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」 — two clauses. The gate enforces 确认 (only the maintainer's approval counts). It does not enforce 人工合并 (the merge is performed by a human). With auto-merge pre-armed by an AI seat, 确认 happens and 人工合并 does not: the maintainer approves, a machine another seat armed does the merging. The gate would report success while the second clause was bypassed.
merge_grouphandling, and it leaves no red mark — the merge would look like a perfectly normal approved-and-green landing.Why prose won't close it
This is the third instance of the same shape, and the first two are both closed as fixed:
docs/adr/**PRs within an hour of the ruling #6785 / ⛔ Discipline: ADRs are confirmed and merged by the maintainer only — no AI seat may merge, queue, or auto-merge adocs/adr/**PR #6741 — the ruling landed, and two AI seats mergeddocs/adr/**PRs within the hour. Prose enforcement measured insufficient; the gate was built in response.ADR Merge Approvalis not in the merge queue's required-check set — two ADR changes landed on main today with the gate red and no maintainer approval #7022 — the gate existed but sat outside the required-check set, so fix(spec,rest,runtime)!: the ADR-0045 publish gate gets its own machine-managed key —app.hiddengoes back to meaning navigation (#4829) #6942/docs(adr-0094): retire D5's env-overlay customization direction (D5-R, #6609 ruling A) #6962 landed with it red.Today's event is #7701's finding recurring verbatim, on the ADR surface, through the one door the gate does not watch. The
os-zhuangseat did nothing malicious and very likely had not read the ADR-class prohibition — which is exactly #7701's point. Every time this has been left to prose it has recurred within hours.Shapes a fix could take (not choosing — this is a governance surface)
docs/adr/**PR without a qualifying approval. Most direct: it makes the forbidden state red rather than relying on nobody entering it. The gate already lists the PR's reviews, so it has the API surface.Option 1 has a wrinkle worth measuring rather than assuming: a gate that goes red because auto-merge is armed must not then be satisfiable by disarming-and-rearming, and it must still be green on the legitimate path (maintainer approves, then merges in person).
scripts/check-adr-merge-approval.mjs:101:That account is the identity AI seats authenticate as in this environment —
get_mefrom a PM session returns exactlyhotlong/50353452. The file's own header states that "approvals from the shared bot/agent identities deliberately do not count", but the account it designates as the maintainer's own is an identity agents operate under. An AI seat can therefore satisfy this gate directly.It did not happen here — the approval came from
os-zhuang, which is correctly refused — and this seat will not approve an ADR PR regardless of holding the credential. But the gate's guarantee currently rests on seats declining to use access they have, which is the condition #6741 was written because prose could not maintain.This is a security/permission-boundary decision and is explicitly the maintainer's (already raised to them in session, 2026-08-12 ~11:1xZ). Recorded here so it is not lost, not to propose a change. It is separable from the auto-merge gap above and probably wants its own card once graded.
Evidence
PR #7960's audit comment carries the full ordered timeline. Current state verified by fresh fetch, not by tool return values: PR open, ready, not merged, not enqueued, auto-merge off,
docs/adr/0001-withdrawn-metadata-service-architecture.mdnot onmain,ADR maintainer approvalred (correctly).