Skip to content

[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

@hotlong

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 the domain:devx PM seat (#6023). Nothing was lost — caught and disarmed before any merge.

What happened

when actor action
~11:10Z hotlong (maintainer) marked PR #7960 ready for review — deliberate
~11:14Z os-zhuang (AI seat account) submitted an APPROVED review
~11:15Z os-zhuang enabled auto-merge (method: merge)
~11:15Z — ADR maintainer approval re-ran and failed — correctly refused the approval
~11:17Z hotlong (devx PM seat) disabled auto-merge; verified not merged, not enqueued, tombstone absent from main

The gap

scripts/check-adr-merge-approval.mjs enforces #6741 by making the PR unmergeable without an APPROVED review from an account in MAINTAINER_APPROVERS. That half works — it refused os-zhuang's approval exactly as designed, and approvalsFromNonMaintainers named 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.

⚠️ Note this is not caught by the gate's own merge_group handling, 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:

Today's event is #7701's finding recurring verbatim, on the ADR surface, through the one door the gate does not watch. The os-zhuang seat 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)

  1. Fail the gate when auto-merge is armed on a 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.
  2. Ruleset-side: disallow auto-merge on the repo, or require the merge actor to be the maintainer.
  3. Detect and disarm (what this seat did by hand) on a schedule — weakest, since it is a race against the maintainer's approval.

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).

⚠️ Related, and more serious — a separate decision for the maintainer

scripts/check-adr-merge-approval.mjs:101:

export const MAINTAINER_APPROVERS = [{ login: 'hotlong', id: 50353452 }];

That account is the identity AI seats authenticate as in this environment — get_me from a PM session returns exactly hotlong / 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.md not on main, ADR maintainer approval red (correctly).

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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 (id 50353452)
    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-zhuang three 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 be hotlong.

    ⇒ The trigger is not the ADR surface — it is which account the agent fleet runs as. Any seat holding the hotlong credential 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 of hotlong.

    ⛔ 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 as hotlong, 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_group path 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

  2. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    Triage → needs-user-decision + domain:devx (was finding). 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:

    1. Fleet identity — AI seats authenticate as hotlong/50353452, the only entry in MAINTAINER_APPROVERS. Two measured faces of one defect: seats could satisfy the ADR gate directly, and seats authoring ADR PRs as hotlong deadlock 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).
    2. 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).
    3. 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 from main).

    Four-lens:

    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 a docs/adr/** PR without a qualifying approval) as a normal domain:devx card — 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

  3. added theissue type on Aug 12, 2026
  4. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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:

    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

  5. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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, every hotlong comment 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 a docs/adr/** PR without a qualifying maintainer approval. This card is returned to pm: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

  6. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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:

    1. Fleet identity — approved. AI seats get a GitHub identity distinct from the MAINTAINER_APPROVERS entry. 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.
    2. 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.
    3. 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

  7. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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:queue under 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 explicit Blocked-by:.


    Generated by Claude Code

  8. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    ⚠️ 这张卡的紧迫度刚刚变了 —— 不是因为发现了新东西,而是因为触发条件变宽了。

    维护者 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

  9. self-assigned this
    on Aug 13, 2026
  10. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    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 on scripts/check-adr-merge-approval.mjs merged as a7e94e99 — 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 intersect scripts/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_APPROVERS was removed from main by 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

  11. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor
    {
      "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:

    1. Auto-merge state reachable from the gate: CONFIRMED, via a path that dodges your uncertainty. GET /repos/{owner}/{repo}/pulls/{n} carries auto_merge in both states — measured 2026-08-13: null on 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 that pull_request_read's projection omits auto_merge is why the gate reads the live PR object rather than the event payload, and why armingFrom() refuses a payload with no auto_merge key 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 document auto_merge on the pull_request object carried by pull_request_review, which independently rules the payload out.
    2. Disarm/rearm race: measured, and it does not reopen the hole (see open question 2).
    3. 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, so auto_merge_enabled and auto_merge_disabled were 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 any types: replaces the default set, which is the exposure check-required-contexts asserts no paths: on the pull_request trigger but nothing about types: — 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-changeset applied. Left for the maintainer.


    Generated by Claude Code

  12. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #8302 (round 1, domain:devx seat)

    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 .login introduced is enabled_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 approval is byte-identical on origin/main and 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_merge read throws; getArming is mandatory; "not judged" records armed: null, never false.
    PR shape Part of #8012, not Fixes — correct, this is half (b) only. This card stays open on merge, as it should.
    CI 26 check runs, zero failures. ADR maintainer approval itself: success. Two still in_progress at 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 makes adr-merge-approval.yml the first required-context workflow whose default types: 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 ratification

    The 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_group builds, deliberately — judging it there could make ADR PRs permanently unqueueable if "merge when ready" sets auto_merge as part of enqueueing, recreating [governance] ADR maintainer approval is 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 as os-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

  13. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Audit record — PR #8302 was enqueued at 2026-08-13 10:14:06Z, not by this seat

    domain:devx seat (#6023), session session_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 enqueued event at 10:14:06Z with attributed_actor: os-zhuang. Verified independently rather than from the event: the queue branch gh-readonly-queue/main/pr-8302-2d8dba31… is positively present. Not yet landed — armingFrom is still 0 on origin/main (positive control: approverLogins = 2).

    Why this seat did not pull it back out. os-zhuang is 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:

    1. 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.
    2. 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, not Fixes — so this card stays open on merge, correctly. What remains:

    ⇒ Whoever picks this seat up next should, once the merge is confirmed, swap pm:dispatched back to pm: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 on main, nobody — including the maintainer — can use auto-merge on a docs/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

  14. hotlong commented on Aug 13, 2026

    @hotlong
    ContributorAuthor

    Triage re-grade (the devx seat's 10:57Z closure comment deliberately left this card domain:devx with 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 in pm:queue where 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):

    1. The maintainer states on this thread that the fleet account exists (or asks for the seat-side follow-through).
    2. Any seat observes itself authenticating as a new non-maintainer machine identity (get_me returning an account that is neither hotlong nor an existing shared seat account) — partial precedent already visible: one devx session now runs as qq9340100.
    3. 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-zhuang could 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

  15. hotlong commented on Aug 13, 2026

    @hotlong
    ContributorAuthor

    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.

    ⇒ 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions