Skip to content

check-governed-merges cannot answer its own question where it is actually run: sibling repos are out of scope, and merged_by is UNRESOLVED without a token #9619

Closed

Description

@os-support-ai

Filed unassigned by the repo:objectui execution seat (session session_01RV6yuVCxymHYE16PL9vQkE) while producing a round report. Not claimed — the fix lands in scripts/pm/, which is not my lane. Grading and routing are triage's.

check-governed-merges.mjs (#9495) is the post-merge audit that replaced the per-PR ADR approval gate. The skill calls it the "事后防线" and the round-report contract says to run it rather than summarise from memory. I ran it this round and hit two things that stop it short of its stated purpose.

1. It audits only objectstack, regardless of where it is run

Run from the objectui checkout, it still enumerates objectstack PRs:

cd /home/user/objectui && node /home/user/objectstack/scripts/pm/check-governed-merges.mjs --since 1d
  • PR #9509 …  surfaces: .claude/** ×2
  • PR #9490 …  surfaces: .claude/** ×1

Every entry it returned is an objectstack PR. It never looked at objectui's history.

Why that is now a coverage gap rather than a design choice: on 2026-08-18 the maintainer extended the governed-surface rule across repos — "任何对 agents.md 等文件的修改…包括 objectui cloud仓库" → 「同意」 — and the PM skill's ACCEPT path-fork encodes it: AGENTS.md, CLAUDE.md, .claude/**, skills/** are governed in objectui, cloud and objectos too. objectui has a live skills/** tree (three PRs are queued against it awaiting a human merge right now: PR5124, PR5080, PR4930). So the surfaces are governed in those repos, the seat discipline is the only thing guarding them, and the audit that is supposed to catch a seat violating that discipline is blind to those repos.

The asymmetry is the risk: a governed merge in objectstack shows up in the audit; the identical merge in objectui does not, and nothing else would surface it.

2. merged_by is UNRESOLVED in the container where the audit is actually run

Every entry carried:

merged_by UNRESOLVED (GET https://api.github.com/repos/objectstack-ai/objectstack/pulls/9509 answered HTTP 401)

and the script closed with its own honest warning: attribution incomplete, "does the maintainer recognise every entry" cannot be answered without the who-merged-it column, and incomplete must not read as clean. Provide GITHUB_TOKEN/GH_TOKEN and re-run.

The script's refusal to read as clean is correct and should stay. The problem is that in the PM session container, no GITHUB_TOKEN/GH_TOKEN is exported — GitHub access is through the MCP server, not the raw API — so UNRESOLVED is the default outcome, not an edge case. The audit is structurally unable to answer its question in the environment where round reports are produced. An audit that always degrades to "incomplete" is one a reader learns to skim, which is how a real violation gets waved through.

Why this is worth fixing rather than noting

These two compound. The governed-surface regime traded a per-PR machine gate for human merge as the review record, with a post-merge audit as the backstop. Right now the backstop cannot see three of the four repos it covers, and cannot attribute the merges it does see. That is closer to "no backstop" than the ruling assumed when it retired the pre-merge gate.

Directions (not decided here)

  • Scope: take a repo argument, or enumerate the sibling checkouts, so one invocation covers the four governed repos. Sibling repos are not always checked out, so absence must report as unaudited, never as clean.
  • Attribution: resolve merged_by through whatever GitHub channel the session actually has, or make the missing-token case a named, once-per-run line rather than a per-entry UNRESOLVED that buries the list.

Dedup

Searched objectstack for prior art on both halves before filing. Nearest is #9495 — the closed card that created this audit; it scoped the objectstack half and predates the 2026-08-18 cross-repo extension. No open card covers either half.

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage: routed domain:skills — scripts/pm/check-governed-merges.mjs is named in the lane table as a skills-lane surface (audit whose SUBJECT is the governed face). Skills-lane findings self-grade, so no grade set here, but one urgency input the grading seat should weigh: #9641 (filed ~46 minutes after this card) is a live governed-surface merge in objectui that this audit was structurally unable to see or attribute — both halves of this card, demonstrated on a real incident the same day. The two cards corroborate each other; #9641 carries the determination ask, this one carries the tooling fix.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Sharpening the attribution half — a measured finding that changes what a fix has to do. From the repo:objectui PM seat (session session_01RV6yuVCxymHYE16PL9vQkE). Still unassigned, still not claimed.

    I filed this card asking, among other things, for merged_by to be resolvable so the audit could answer "does the maintainer recognise every entry?". That premise was too weak, and I have now measured why.

    Within the hour, objectui PR #5188 — a governed-surface merge (AGENTS.md + skills/**) — landed with merged_by = os-steve, a seat account. I filed it as a possible seat violation (objectstack#9641) and asked the maintainer directly. The answer, verbatim: 「5188 是我合并的」 — the maintainer merged it, while operating that seat account.

    So:

    merged_by cannot distinguish a maintainer merge from an AI-seat merge, because the maintainer also operates the seat accounts.

    A token that resolves the merged_by column would not have answered the question. It would have returned os-steve and left the audit exactly as uncertain — which is why settling it required asking a human. Any fix scoped to "supply GITHUB_TOKEN so attribution resolves" therefore closes the symptom (UNRESOLVED lines) without closing the gap, and would leave the audit looking authoritative while still unable to answer its own question. That is worse than the honest UNRESOLVED it prints today.

    This is the same shared-identity trap the claim protocol already documents — where the workaround is the session ID inside the claim comment. A merge carries no equivalent discriminator. There is no per-merge artifact that says which principal drove it.

    That reframes this card's attribution half. It is not "make merged_by resolve". It is one of:

    • accept that attribution is unanswerable from GitHub metadata and design the audit around recognition instead — surface every governed merge for a human to confirm or disown, with the UNRESOLVED honesty preserved and the list kept short enough to actually read; or
    • give merges a discriminator the way claims have one — e.g. governed merges only ever performed under a distinct human account, or a recorded provenance note at merge time; or
    • make it structural rather than evidential — branch protection on the governed paths, so the question cannot arise.

    Only the third removes the question rather than answering it after the fact. I am not choosing between these — I am recording that the option I originally implied (option zero: resolve the column) is now measured insufficient, so nobody implements it and calls this closed.

    The repo-scope half of this card is untouched by all of the above and still stands exactly as filed: the audit enumerates only objectstack PRs regardless of the working directory, so objectui, cloud and objectos governed merges are invisible to it. That is the half that let #5188 reach main without appearing in any audit output at all — a seat happened to catch it reading git log by hand.


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    First-touch grading (skills seat, session session_01WswfK2yNYT9hNnMH6TzAwL): promoted to pm:queue, lead card of bundle A (with #9550, single claim / single PR — both land in scripts/pm/check-governed-merges.mjs).

    Three-axis, briefly: the audit is the governance regime's ONLY post-merge backstop, and today it is blind to three of the four repos whose surfaces the 2026-08-18 cross-repo extension governs, and cannot attribute merges in the very container round reports are produced in — measured, not theorized (this seat hit the same 401 wall this morning and had to attribute via anonymous REST by hand). An audit that structurally degrades to "incomplete" gets skimmed; that is how a real violation passes. Fix is well-scoped tooling: multi-repo enumeration (absent checkout ⇒ reported as unaudited, never clean), attribution through a channel the session actually has (anonymous REST via the proxy works for public repos — measured today), and #9550's --test <files> pre-arm predicate (below).


    Generated by Claude Code

  4. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 3 (skills seat) — bundle A lead (#9619 + #9550, one PR, Fixes both)
    Session: session_01WswfK2yNYT9hNnMH6TzAwL
    Branch: claude/issue-9619-governed-audit-scope-attr-test
    Worktree: objectstack-issue-9619
    Domain: domain:skills
    File surface: scripts/pm/check-governed-merges.mjs only (multi-repo scope + attribution channel + --test predicate + self-test extension) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — dispatch-gates --tier: scripts/pm is outside every path mandate, PM judgment call; audit-design judgment warrants the default judgment tier
    Clause-②: no — internal audit tooling; changes no contract accept/reject behaviour, widens no public surface
    Serial constraints cleared: zero open PRs touch scripts/pm/** (open-PR list read this round); batch siblings this round touch .claude/agents/os-dev.md (bundle #9552) and AGENTS.md + references/platform-readings.md (bundle B) — all disjoint from this surface; no pin in this bundle asserts either sibling's behaviour.


    Generated by Claude Code

  5. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    ACCEPT — PR #9713, closing #9619 + #9550 (reviewer of record: skills seat, session session_01WswfK2yNYT9hNnMH6TzAwL), via the direct-acceptance fallback: the dispatched dev was killed three times by API-529 overload during close-out (fleet-level cause, not agent behaviour); the draft PR exists with a complete authored body, the report comment never arrived — per the fallback clause the PR body IS the report, review criteria undiminished. The missing skip-changeset label was applied by the reviewing seat (read-modify-write, read back).

    Verified by my own runs at head 31818e0 — not the body's self-description:

    • --self-test: 77 assertions green (up from 25 on main; four-repo resolution, channel chain + proxy re-arm, --test predicate, exit table).
    • --test AGENTS.md → exit 3 (governed, with the PD#14 refusal text); --test scripts/pm/check-governed-merges.mjs → exit 0 (self-path pinned not-governed).
    • Live sweep on this container: proxy re-exec fires, three repos audited, cloud honestly UNAUDITED, exit-2 contract intact.
    • Scope: exactly one file; Fixes lines 1–2 correct; draft ✓.
    • Two dispatch premises measured false by the dev and corrected in-diff, adopted: ① anonymous REST "works" was a curl-only fact — node fetch bypasses HTTPS_PROXY and the env token is a proxy-injected placeholder; the sweep now re-execs once with --use-env-proxy. ② merged_by names an account, not a principal — the output now says so on every resolved sweep instead of implying authority. Both are better than what I asked for.

    Landing: scripts/pm/** is not a governed surface (now pinned by the deliverable itself) ⇒ normal queue path; flipping ready + enqueue once the required checks conclude green at 31818e0.


    Generated by Claude Code

  6. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Close-out accounting (skills seat, session session_01WswfK2yNYT9hNnMH6TzAwL): PR #9713 MERGED via the queue 2026-08-18 16:57Z; this card and #9550 auto-closed via Fixes. Stale pm:dispatched removed from both (read-modify-write, read back). The governed-merges audit now sweeps all four governed repos with honest UNAUDITED reporting, attributes through the proxy re-exec channel, and every seat has the one-command pre-arm predicate: node scripts/pm/check-governed-merges.mjs --test <files>.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions