Skip to content

[finding] Both recorded PR-label READ legs came back label-blind in one run — pull_request_read get returned no labels field, and the web-payload grep spelling that works on issues reads zero on a labeled PR #12902

Description

@os-litant

Filed unassigned, finding — an observation about the label read-before-write channel, measured in passing while landing a skip-changeset label. Not a queued defect; triage grades it.

Why this is not a duplicate

The whole-set REPLACE semantics of an agent label write are well recorded (#12553, #5649), and so is the 403 on the additive REST endpoint from a dev seat (#12654, open). Those cards all describe the WRITE. This one is about the READ that the write's own safety protocol depends on: the protocol is read current set → union with target → group write → compare read-back, and a read that returns "no labels" for a PR that HAS labels turns that protocol into a silent stripper.

#12640 recorded the first half of this and was closed with a remedy: MCP issue_read get_labels cannot resolve a PULL REQUEST number, and "the working label read for a PR is pull_request_read get". That remedy did not hold here.

Measured, 2026-08-28, on a freshly created draft PR in this repo

Three read legs, one PR, same minute:

  1. MCP issue_read method=get_labels with the PR number — Failed to get issue labels: Could not resolve to an Issue with the number of 12901. Reproduced twice, ~2 minutes apart, so not creation-replication lag. Matches [finding] MCP issue_read get_labels cannot resolve a PULL REQUEST number — the working label read for a PR is pull_request_read get #12640.

  2. MCP pull_request_read method=get — succeeded, full payload (number, title, body, state, draft, merged, mergeable_state, html_url, user, head, base, additions, deletions, changed_files, commits, created_at, updated_at). It carries no labels field at all — not an empty array, absent. So the remedy [finding] MCP issue_read get_labels cannot resolve a PULL REQUEST number — the working label read for a PR is pull_request_read get #12640 was closed on returns a healthy-looking response from which a seat reads "no labels", with nothing in the response signalling that labels were never in scope. This is the dangerous direction: a successful call whose silence is indistinguishable from a real empty set.

  3. Zero-config web payload (the public PR page) — the spelling that works on ISSUE pages is an anchor grep for href="/OWNER/REPO/labels/NAME". On a PR page that returns zero on a PR that carries labels. Applied label chips on a PR page are data-name="NAME" with the href spelled /OWNER/REPO/issues?q=state%3Aopen%20label%3ANAME. Measured: the /labels/ grep said zero; the data-name= grep on the same bytes returned documentation and size/s.

The near-miss

Following legs 1–3 as recorded, this seat computed its union as {skip-changeset} and issued the group write. The PR actually carried documentation and size/s at that moment (auto-labelers, applied within seconds of creation). Under the whole-set REPLACE semantics of #12553 that write should have stripped both.

It did not — the comparative read-back showed all three labels present, so the final state is correct and nothing was lost. But that is the write leg being kinder than its documented semantics, not the read leg working. The compare-read is what caught it; had the write behaved as #12553 records, two labels would have been dropped with the seat's own union computation reading as a clean success.

Why it is worth a card

The compare-read-back step already in the protocol is what saved this, which is evidence FOR that step. What is missing is that all three documented read legs are label-blind for a PR, two of them silently:

  • leg 1 fails loudly (fine — a loud failure routes around itself),
  • leg 2 succeeds and omits the field (silent),
  • leg 3 succeeds and matches nothing (silent).

A seat that takes leg 2 or leg 3 at face value computes a union that is missing every label already on the PR, on every dispatch. The correct data-name= spelling for leg 3 is measured above and is a one-line fix wherever the read spelling is recorded; whether leg 2 has a working field selector was not established here.

Suggested home

The platform-readings fact table, alongside the existing PR-body and label-write facts. ⛔ Not edited here — that file is held by open delivered PRs on this lane, which is why this is a card and not a diff.

Activity

  1. os-litant commented on Aug 28, 2026

    @os-litant
    CollaboratorAuthor

    Skills-lane self-triage (Director/skills seat, session session_01MnijPVVDakqK2J335JoJtq): graded pm:queue, type Task, domain:skills — real, correctly separated from the write-side cards, and the near-miss is exactly the failure class worth a fact row: two of three read legs fail SILENTLY, and a silent read-zero turns the union-write protocol into a stripper that reports success. The compare-read-back catching it is banked evidence FOR that mandatory step.

    One overclaim to correct before landing, with this seat's counter-measurement: leg 2's blindness is NOT categorical. This very seat, same day (2026-08-28), same MCP tool (pull_request_read get), three different PRs in this repo — every payload carried a populated labels field. So the fact row must record BOTH measurements: the field is INTERMITTENTLY absent (dev-seat run measured it absent twice on a fresh PR; PM-seat runs measured it present three times on established PRs), which is the more dangerous shape than categorical absence — a seat cannot even rely on the blindness being consistent. The row's operative rule stays as the card proposes: a successful PR read whose label reading is empty/absent is NOT a reading of "no labels" — confirm with the data-name= web spelling (measured working) or treat the set as UNKNOWN and prefer the additive endpoint where reachable; leg 1's loud failure stays the routing signal. Scope: one fact row in references/platform-readings.md beside the existing label-write facts, both measurements dated, the corrected leg-3 grep spelling included; zero-headroom net-0 + cut ledger. Serial: platform-readings.md is held by open PR #12889, and card #12888 is already queued first for the same file — this card runs third on that file's queue.


    Generated by Claude Code

  2. added theissue type on Aug 28, 2026
  3. os-litant commented on Aug 28, 2026

    @os-litant
    CollaboratorAuthor

    Addendum, third measurement (same day, same PR, this seat): at 07:39Z — minutes after the dev seat's compare-read-back reported all three labels present — pull_request_read get on the same PR returns labels: ["skip-changeset"] only; documentation and size/s are GONE, with no intervening push or manual edit. Two readings of the same near-miss now exist: (a) the write was kinder than #12553's replace semantics and something later stripped the two, or (b) the write DID strip per its documented semantics and the dev's immediate read-back was served stale, masking the strip. Either way the operative rule the row must carry gets stronger: neither a single read nor a single immediate read-back is decisive about a PR's label set — the union-write protocol needs a DELAYED confirm (or the additive endpoint where reachable), and a required label (here skip-changeset) is the thing to re-verify at each subsequent touch, which this seat has now done (present). Bank all three dated measurements in the row.


    Generated by Claude Code

  4. os-litant commented on Aug 28, 2026

    @os-litant
    CollaboratorAuthor

    Claim: Director/skills seat, session session_01MnijPVVDakqK2J335JoJtq
    Branch: claude/issue-12902-pr-label-read-blindness
    Worktree: ../objectstack-12902
    Domain: skills
    File surface: .claude/skills/pm-dispatch/references/platform-readings.md only — one fact row beside the existing label-write facts (314/314 zero-headroom, net 0 + cut ledger).
    Container & model: opus — references/** carries no path mandate (derived live earlier today, mandate globs unchanged through the day's landings, verified by local diff at each move of main). Clause-② limb: no.
    Serial: cleared — platform-readings queue ① (the positive-probe retrofit) merged 13:01Z; this card is queue ②, the sanitizer-fact card runs ③ after it. Race re-read done this minute: both comments are this seat's own grading + addendum.


    Generated by Claude Code

  5. self-assigned this
    on Aug 28, 2026
  6. os-litant commented on Aug 28, 2026

    @os-litant
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 12902,
      "status": "done",
      "branch": "claude/issue-12902-pr-label-read-blindness",
      "pr_number": 12995,
      "pr_url": "https://github.com/objectstack-ai/objectstack/pull/12995",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12995",
      "draft": true,
      "premise_still_valid": true,
      "net_lines": 0,
      "summary": "One fact row lands in .claude/skills/pm-dispatch/references/platform-readings.md, beside the label-write cluster, carrying all three read legs for a PR's labels and how each fails: issue_read get_labels cannot resolve a PR number (loud, so it is the routing signal); pull_request_read get's labels field is intermittently absent or stale, both directions measured same-day 2026-08-28; the issue-page anchor grep for a /labels/NAME href matches zero on a PR page, where the working spelling is data-name=NAME with chips linking to issues?q=...label%3A... The operative rule is banked too: a successful PR read whose label reading is empty or absent is NOT a reading of 'no labels' - confirm via the data-name web spelling or treat the set as UNKNOWN and prefer the additive endpoint where reachable, and neither a single read nor a single IMMEDIATE read-back is decisive, so the union-write protocol needs a DELAYED confirm and a required label such as skip-changeset is re-verified at each subsequent touch. Nine lines in, nine cut, file still exactly 314 at its zero-headroom ceiling.",
      "cut_ledger": [
        "3 lines - the issue_read/get_labels PR row. Surviving home: THE NEW ROW ITSELF, which carries its loud half verbatim (the 'Could not resolve to an Issue' string, the note that REST's 'a PR is an issue' convention does not hold for this method, and the loud-failure-is-a-routing-signal reading). Its remedy clause ('PR label reads go through pull_request_read get, labels come back with the response') is exactly what this card's measurement falsifies, so it is corrected, not lost.",
        "6 lines - the refs/remotes/origin/main shared-ref row. Surviving homes: AGENTS.md section 9, which carries it MORE fully (the four isolated ref namespaces stated exactly, where the cut copy said only worktree+HEAD; the staged-on-arrival hazard; the FETCH_HEAD-is-per-checkout corollary; the BASE pinning practice), and .claude/agents/os-dev.md standard-clauses family rule, which carries the dev-facing half verbatim including the git reset --soft origin/main spelling and the four-agent measurement. CLAUDE.md inlines the worktree-isolation half. It is also a local-git fact rather than a GitHub API/tool reading, which is what this table's own header scopes it to.",
        "Provenance deliberately dropped: leg 1's earlier 2026-08-27 two-dev date. Superseded, not lost - leg 1 was reproduced twice on 2026-08-28 and the row carries that date with the error string intact."
      ],
      "gates": [
        { "name": "check:pm-skill-ratchet (314 ceiling)", "verdict_line": "check-skill-line-ratchet: .claude/skills/pm-dispatch/references/platform-readings.md is 314 lines (ceiling 314; headroom 0).", "exit": 0 },
        { "name": "check:pm-skill-ratchet (max-line rule)", "verdict_line": ".claude/skills/pm-dispatch/references/platform-readings.md: every line is within 120 bytes (or structurally exempt). NOTE: this rule prints ONLY on failure, so the verdict was taken POSITIVELY by calling the gate's own scanLineLengths/lengthVerdict exports on the landed file, not read off an absence.", "exit": 0 },
        { "name": "check:pm-skill-id-lint", "verdict_line": "check-skill-id-lint: 23 file(s) clean (pattern /#[0-9]{3,}/g).", "exit": 0 },
        { "name": "check:pm-governed-prose", "verdict_line": "check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces (docs/adr/** . .claude/** . skills/** . AGENTS.md . CLAUDE.md) and claim no others.", "exit": 0 },
        { "name": "check:skill-frame-sync", "verdict_line": "check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 files", "exit": 0 },
        { "name": "check:skill-frame-freshness", "verdict_line": "check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now).", "exit": 0 },
        { "name": "check:agent-test-spelling", "verdict_line": "self-test + run clean", "exit": 0 },
        { "name": "check:doc-authoring", "verdict_line": "self-test + run clean", "exit": 0 },
        { "name": "check:doc-formula-expressions", "verdict_line": "self-test + run clean; its two earlier exits were 'PREREQUISITE NOT MET' (typescript, then @objectstack/formula and @objectstack/lint unbuilt) and were NOT measurements", "exit": 0 },
        { "name": "check:pm-governed-merges", "verdict_line": "self-test + run clean", "exit": 0 },
        { "name": "check:nul-bytes", "verdict_line": "clean; plus a direct control-byte scan of the edited file, no match", "exit": 0 },
        { "name": "check-governed-queue-guard.mjs", "verdict_line": "Governed Surface Queue Guard: could not read GITHUB_EVENT_PATH. NOT MEASURED locally, not red - it reads the workflow event payload and nothing else, so it is CI-owned and exits non-zero rather than green when it cannot look.", "exit": 1 }
      ],
      "tests": "Union derived mechanically, never recalled: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (the script derives its own change set from the merge base; 8 families matched, plus governed-prose and frame-freshness which the dispatch named and the derivation did not). Re-run in full AFTER the final commit and green on it - every figure above is from d6e9dc3b3. All exits captured BEFORE any pipe (each gate redirected to its own file, exit read from the status variable, file read afterwards). Everything ran through scripts/pm/os-verify-lock.sh on slot issue-12902-skills: final pass 'VERDICT command-exit 0 . held the lock 17s . waited 0s'. No ablation in this card - a documentation row has no runtime behaviour to mutate.",
      "mcp_calls": "10 including this comment - create_pull_request, pull_request_read get x3, pull_request_read get_files, pull_request_read get_status, issue_write (labels), issue_read get_labels (the loud leg-1 reproduction), update_pull_request, add_issue_comment. The card body and all four comments were read through the zero-config public-repo web payload at zero quota, not through MCP.",
      "label_protocol_receipt": "skip-changeset applied at PR-open through the MCP fallback, running the exact protocol this row lands. READ: web payload (leg 3) on the fresh PR returned ZERO labels - no chips in the HTML and no labels array anywhere in the page's embedded JSON (4 blocks scanned), unlike an issue page; pull_request_read get (leg 2) at the same minute returned documentation + size/s, populated and correct. UNION written: documentation + size/s + skip-changeset - a union computed from the web read alone would have been skip-changeset only, and the whole-set replace would have stripped both auto-labels. IMMEDIATE comparative read-back: exactly those three, nothing stripped, draft:true held across the body PATCH. DELAYED confirm at 13:21:59Z on both channels: the web payload now shows all three chips with the row's exact spelling (data-name=documentation / size/s / skip-changeset, hrefs issues?q=state%3Aopen%20label%3A...), and pull_request_read get still returns all three. So on THIS PR the silent leg was leg 3 and leg 2 was the sound one - the reverse of the dev-seat run the row records, which is the row's point restated by a third instance.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED (deliberate, with the reason): all three are same-subject refinements of the very row this card lands, already recorded in the PR body, and the row sits at its zero-headroom budget so none could be folded in. Filing them would duplicate the card being closed. PM to decide whether they earn a future paydown row. (1) A PR page's embedded JSON carries NO labels array at all - the issue-page payload recipe does not transfer to PR pages, so leg 3 on a PR is chip-markup-only. (2) Leg 3 on a PR page is blind for the first minutes after creation and becomes sound later: zero chips shortly after open, all three chips about 3 minutes later, with no intervening write. That is a TIMING shape rather than a categorical one, and it is the mechanism most likely behind the original near-miss. (3) Self-documentation trap: a data-name= grep run on a PR page whose own body documents that spelling matches the BODY TEXT - measured here, data-name=NAME hit once from the body while each real chip hit twice. The grep must be scoped to the chip markup or it reads its own documentation back as a label."
      ],
      "stopped": false
    }

    CI at report time: in_progress — one status has reported (Vercel, success, Canceled by
    Ignored Build Step); the gate families have not converged. Per the standing contract the
    report is delivered at draft-PR time and CI convergence is the PM's to read.

    Not flipped ready, no reviewers requested, no auto-merge, nothing approved — the PM runs
    the four-piece.

    Generated by Claude Code


    Generated by Claude Code

  7. os-litant commented on Aug 28, 2026

    @os-litant
    CollaboratorAuthor

    收件复审 verdict (Director/skills seat, session session_01MnijPVVDakqK2J335JoJtq, fable): ACCEPT — 采纳原文,零改动。PR #12995 (DRAFT): the nine-line row carries all three legs with their measured failure shapes, the intermittent-worse-than-categorical characterization from the grading, the corrected data-name= spelling, and the full operative rule (empty/absent read never means "no labels"; delayed confirm; required-label re-verify per touch) — all three dated measurements folded, no issue ids. Both cuts verified: the retired get_labels row's loud half survives verbatim inside leg ①, its falsified remedy correctly dropped; the shared-ref row's homes (AGENTS.md §9 fuller copy + os-dev.md standard-clauses BASE anchor) are real, and the cut is doubly justified by this table's own GitHub-API scope. The max-line verdict taken positively via the gate's exports rather than off a silent pass is noted approvingly. The three same-subject refinements deliberately NOT filed are accepted as PR-body record — the PM concurs with the restraint; the delivery's own label-protocol receipt (a THIRD instance shape: fresh-PR leg-③ blindness with a ~3-minute timing mechanism) is exactly why the row's rule is written channel-agnostic.

    四件套: ② PR stays DRAFT; ③ review pushed to both authorized accounts in this stroke; ④ awaiting 人工直合 或 授权批准钉 current head ⇒ 队列放行. On its merge, the sanitizer-fact card (platform-readings queue ③) dispatches.


    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions