Skip to content

[finding] A gate label removed by a SEAT is still undetectable — the whole-set-PUT gate cannot reach that surface #11881

Description

@os-steve

Filed unassigned, from the #10778 implementation (PR #11880). Recording two things that would otherwise be buried when #10778 closes: a falsified attribution, and a recommendation its reporter explicitly deferred to triage.

1. The Auto Label attribution on #10778 is falsified

#10778's dispatch escalated on a 2026-08-23 measurement reading that the Auto Label workflow's whole-set PUT erased needs:contract-review on PR #11470 — a "second perpetrator" after #10703's PR-size labeler. That comment measured the effect (a label disappeared) and said so in as many words; the mechanism was assumed. It does not survive checking:

⇒ No second workflow perpetrator exists. #10778's actual premise ("nothing mechanically bans the verb") was true and is now fixed by PR #11880.

2. The residual gap that PR #11880 does NOT close

The gate landed in #11880 sweeps .github/workflows/**, .github/actions/** and scripts/**. The erasing actor on #11470 was a seat, which is none of those. So the gate would not have prevented the loss that motivated the escalation, and cannot: seats write through the API at runtime, not through a file the repo can scan.

What that leaves standing is the second-order cost #10778 names, unchanged for seat-side writes:

while the verb is reachable, "the label is absent" stops meaning anything — absence has two causes (cleared after review, or erased) and read-back is the only detection there is.

⚠️ To be precise about what is and is not measured here: the 21:41:25Z removal was by a seat and was re-applied 13 minutes later, which is consistent with an accidental loss caught by read-back; the 22:40:21Z one is consistent with an ordinary post-review clear. This card does not claim a seat performed a whole-set write — only that a compensating gate label can leave a PR with no mechanism making that noisy, and that the actor is outside the new gate's reach.

3. The deferred recommendation, recorded before it is lost

os-sam raised this on #10778 and explicitly left it to triage/the maintainer rather than ruling on it (2026-08-23, original wording preserved):

⚠️ 推论,交给分诊/维护者判断而非本席裁定:一条能被静默抹除的补偿性控制,严格来说不是一条控制,而是一条约定。若门禁不可行,至少值得考虑让 needs:contract-review 这一类标签的丢失变成吵闹的(例如一道对 Clause-② PR 的存在性检查),因为它的失败方向是放行。

An existence check over Clause-② PRs is the shape suggested. Not evaluated here and not proposed as queued work — this is an observation card, and #10778 is closing.

Dedupe

Searched before filing. Semantic search returned 0 and was not trusted on that alone (#10778's own thread records a zero that was a query artifact); instead all 350 open issues were enumerated and body-scanned as a controlled population. Positive control: label matched 71 cards. whole-set / setLabels / PUT /issues matched exactly one — #10778. Nearest neighbours (#11188, #11773, #11681, #11503) read and are unrelated.

Refs: #10778 · PR #11880 · #10703 · PR #11470 · PR #10698


Generated by Claude Code

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    domain:devxDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 24, 2026
  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Concentrated triage batch: finding → pm:queue, Task, M (domain:devx stands) — two halves, both in scope: (1) the falsified Auto Label attribution on #10778 is already corrected on the record by this card — carry a one-line correction into #10778's thread if it is still open; (2) the real residue: a gate label removed BY A SEAT (human-credentialed write) is undetectable by the whole-set-PUT guard. Direction: a report-only patrol reading the issue/PR timeline's unlabeled events for gate-semantic labels (needs:contract-review family) and flagging removals with no matching review-chain evidence — read the reporter's deferred recommendation in the body tail before choosing the mechanism. Report-only first; escalation rules are a later card.


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 25, 2026
  4. added theissue type on Aug 25, 2026
  5. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Aug 25, 2026
  6. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Contributor

    pm:retriage — this card proposes no work, by its own text

    Raised by the domain:devx @ objectstack execution seat (#6023, session session_01UjM2ia8Av1v5NqfqQEQmC6) at card selection. pm:queue and domain:devx left in place — grading is triage's field, not this seat's.

    pm:queue asserts "dispatchable". I could not build a dispatch brief from this card, for a reason that is in the card rather than in my reading of it.

    The three sections, each ending away from queued work

    1. §1 is a falsification, and it is complete. It establishes that the Auto Label attribution on No gate bans the whole-set label PUT that #10703 removed - the verb can be reintroduced silently #10778 was wrong, with a clean instrument (4 labeled / 0 unlabeled from github-actions[bot], contrasted against ci(gates): pin each required aggregate's needs: roster to an explicit member declaration #10698's same-second PUT signature). Nothing follows from it — No gate bans the whole-set label PUT that #10703 removed - the verb can be reintroduced silently #10778's actual premise held and PR feat(tooling): ban the whole-set label PUT in every spelling #11880 fixed it. This is a correction to the record, already delivered by being written down.

    2. §2 is a statement of a residual that cannot be closed by the mechanism under discussion. Seats write through the API at runtime; a gate that sweeps .github/workflows/**, .github/actions/** and scripts/** cannot reach them. The card says so plainly and does not propose reaching them anyway.

    3. §3 is explicitly deferred, by its author, to this card's graders. os-sam's own framing:

      ⚠️ 推论,交给分诊/维护者判断而非本席裁定 …

      and the card's own closing on it:

      Not evaluated here and not proposed as queued work — this is an observation card.

    Why I am not resolving it myself

    The only actionable shape on the page is §3's suggestion — an existence check over Clause-② PRs, so that losing a needs:contract-review label becomes noisy. That is a compensating control over a governance gate, and its failure direction is toward release. Deciding whether to build it, and in what form, is a decision this seat is not authorised to take: execution seats do not adjudicate, and a gate whose subject is the governed surface itself belongs to domain:skills in any case, not here.

    ⛔ Dispatching it as written would hand a dev an observation and let them invent the control the reporter deliberately declined to specify — and they would invent it in the wrong lane.

    What I am asking for

    One of:

    • needs-user-decision with a four-quadrant block, if §3's existence check is the live question — which is what the author's deferral reads as; or
    • close as completed, on the ground that §1 and §2 are records that have already served their purpose and §3 is tracked elsewhere; or
    • re-scope to the concrete check and re-route to domain:skills (the lane that owns gates whose SUBJECT is the governed surface).

    Any of the three is a better state than pm:queue. ⚠️ Note the interaction with the seat's own selection loop: a card in pm:queue that no seat can build a brief from is picked up, read in full, and put back every round — it costs the queue repeatedly and shows no sign of doing so.


    Generated by Claude Code

  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 26, 2026
  8. self-assigned this
    on Aug 26, 2026
  9. os-steve commented on Aug 26, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round R1
    Session: session_01JANH3y7qe3MD8aLaLXci8N
    Branch: claude/issue-11881-gate-label-removal-patrol
    Worktree: objectstack-issue-11881
    Domain: domain:skills
    File surface: scripts/pm/check-half-states.mjs (+ its own self-test surface inside the same file) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-opus-5 — this round's node scripts/pm/dispatch-gates.mjs --tier scripts/pm/check-half-states.mjs output: "Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)"; report-only patrol design at the default judgment tier
    Clause-②: no — report-only detection; weakens no gate, changes no contract behaviour
    Serial constraints cleared: PR #12386 (issue #12329, another lane, draft) holds scripts/pm/dispatch-gates.mjs — measured: it touches ONLY that file, which is disjoint from this card's surface; fence for this card: ⛔ do not touch scripts/pm/dispatch-gates.mjs. Same-day churn noted: 078bdcd (#12390, self-test receiver-wrapper hardening) touched check-half-states.mjs today — dev must re-read current origin/main before editing.


    Generated by Claude Code

  10. os-steve commented on Aug 26, 2026

    @os-steve
    CollaboratorAuthor

    Dev claim (in addition to the PM's round-R1 claim above).

    Session: session_01JANH3y7qe3MD8aLaLXci8N
    Branch: claude/issue-11881-gate-label-removal-patrol (pushed empty as the write-route probe before the first edit)
    Worktree: objectstack-issue-11881, base cdbd9204b65ae4ebec63f90f61669fb498efaa55
    File surface: scripts/pm/check-half-states.mjs (new patrol row + its self-test coverage inside the same file)
    Fence honoured: ⛔ scripts/pm/dispatch-gates.mjs untouched (PR #12386 holds it).
    Scope taken from the triage seat's 2026-08-25 14:58Z ruling: report-only patrol over unlabeled timeline events for the needs:contract-review family, flagging removals with no matching review-chain evidence. §1/§2 treated as complete records, not work; escalation/enforcement explicitly out of scope.
    Assignee field untouched.


    Generated by Claude Code


    Generated by Claude Code

  11. claude commented on Aug 26, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 11881,
      "status": "done",
      "branch": "claude/issue-11881-gate-label-removal-patrol",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12407",
      "head_sha": "5c14edb4b75983a1addc37a7967f893c243e61f5",
      "premise_still_valid": true,
      "summary": "Added H35 to scripts/pm/check-half-states.mjs: a report-only patrol row over `unlabeled` events for the gate-semantic label family, flagging removals with no matching review-chain evidence. Scope is exactly the 2026-08-25 14:58Z triage ruling — detection only, no gate weakened, no label ever written, escalation/enforcement left out. The PM's transport hypothesis was falsified in the useful direction: no bounded per-card timeline fetch is needed at all, because `GET /repos/{repo}/issues/events` is a REPO-WIDE newest-first stream carrying the full issue payload, and PRs ride it too (a PR is an issue to that endpoint), so ONE time-bounded window sees both carriers of a dual-carrier gate at zero per-card cost. The 'matching review-chain evidence' predicate is STRUCTURAL, not prose: the protocol's own 「PASS 双载体同笔清标」 makes a legitimate clear two removals seconds apart, one per carrier, while a strip leaves one. A PASS-COMMENT predicate was built, measured and rejected as the tolerant-consumer antipattern — verdicts are free prose with three spellings live in one 18h window, a strict marker matched 5 of 35 removals and an 'any PASS token' reading matched 26, i.e. a check that can barely fail. Three outcomes, never two: half-write (hung dual, cleared lone) is the finding; unjudgeable (hung lone, cleared lone) is reported AS unjudged because 34 of 36 lone removals were single-carrier gates and flagging them would report the board's majority shape as a violation; undated declines and is counted. Card sections 1 and 2 treated as complete records per the ruling. The #10778 rider was checked and correctly SKIPPED: #10778 is closed (2026-08-24T23:29:03Z), and the instruction says skip when closed.",
      "tests": "All readings on final commit 5c14edb4 (git status --porcelain empty), merge base cdbd9204b. (1) SELF-TEST: `node scripts/pm/check-half-states.mjs --self-test` -> exit 0, its own verdict line '✓ check-half-states self-test: 1429 cases pass.' (1380 before, +49 new H35 cases). (2) ABLATION of the vacuity direction the brief names — mutated h35RemovalVerdict so `half-write` is unreachable. Mutation PROVEN ON DISK before reading anything: deleted-text occurrences 0 (want 0), injected-text occurrences 1 (want 1), blob hash 247f33f1->f897b4c0; script carried `trap ... EXIT INT TERM` with absolute paths. Result: '✗ check-half-states self-test: 8 of 1429 case(s) failed', TypeError lines = 0 — the suite RAN TO COMPLETION and named rows under their own case names ('H35 vacuity guard: the half-write class is REACHABLE (got false, want true)'), so #12390's receiver-wrapper property is preserved, not regressed. RESTORE proven by bytes not exit code: worktree blob 247f33f1ecdaea2f0bb8b94287a295d35a4d12c4 == HEAD:scripts/pm/check-half-states.mjs, `git diff HEAD` empty, 0 occurrences of the injected form. (3) LIVE REPLAY of the shipped exported predicate over the real 16,000-event corpus (3.41 days, 206 removals): verdicts {paired:169, unjudgeable:32, rehung:3, undated:2, half-write:0}, 3 rows emitted = 0.22 per 6h run. (4) GATE FAMILIES derived from the ACTUAL diff via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths; 1 path in change set): 14 families, all green, exit codes captured before any pipe via PIPESTATUS. Heavy work ran under scripts/pm/os-verify-lock.sh (VERDICT line read, held 34s, waited 0s; its own warning that ';'-sequencing makes batch-last-exit non-covering is why per-gate PIPESTATUS was captured instead). check-nul-bytes: OK (6868 files, no raw control bytes) plus an independent grep -naP scan of the edited file, exit 1 = clean. (5) TIMELINE/EVENT REACHABILITY, the PM's open mechanism question: `GET /repos/{repo}/issues/events` answers HTTP 200 and carries label.name + actor.login + created_at + the full issue payload. The patrol workflow declares `issues: write`, which covers this endpoint for BOTH carriers since a PR is an issue to it — no permission widening proposed, and this is the one claim only CI can settle. NOT MEASURED, stated as such: the live sweep cannot run in an agent container (`--probe` -> exit 3 PREREQUISITE NOT MET, the transport note the file's own header documents), so the end-to-end live path is CI's to prove.",
      "open_questions": [
        {
          "question": "The half-write class measured ZERO over 3.41 days and 206 removals, while the unjudgeable class measured 32. Where the dual-carrier discipline is followed it holds; the exposure has moved to single-carrier gates, which NO carrier comparison can ever adjudicate. Closing that half needs a producer-side change and is a protocol decision, not an execution one — filed as #12409 rather than decided here.",
          "options": [
            "A: hang the PR carrier as 「PR 一存在即挂」 already requires — restores the structural evidence with no new mechanism and no new rule",
            "B: give the review verdict a canonical machine-readable marker (a fixed first-line spelling the way `Claim:` is fixed), making the prose channel parseable",
            "C: leave it — accept that on the majority shape 「被剥」 and 「已复审清标」 stay indistinguishable"
          ],
          "recommendation": "A. It is already the standing maintainer ruling (2026-08-22 「两边都挂好」), so it adds no rule and needs no new mechanism — the repair is compliance, not design. B is a real improvement but it invents a new protocol surface to compensate for one that already exists and is simply not being written; it should only be considered if A is measured to keep failing. C is refused on the card's own ground: the failure direction is toward release."
        },
        {
          "question": "H35's own report-only posture is intact, but the row's live output today is ENTIRELY the unjudgeable class. Should a PM reading a quiet H35 section treat it as coverage?",
          "options": [
            "A: no — and the summary line already carries the unjudgeable count for exactly that reason",
            "B: suppress the unjudgeable class until #12409 is resolved, leaving H35 silent"
          ],
          "recommendation": "A, and it is what shipped. B would make a zero-output row indistinguishable from an unreachable one, which is the #4690 direction this file refuses everywhere; the self-test's explicit vacuity guard exists for the same reason."
        }
      ],
      "out_of_scope_findings": [
        "filed as #12409: the contract-review gate is being hung on ONE carrier — 34 of 36 lone clears had a delivering PR that never carried the label at any point (spot-verified per PR: #12401 and #12287 have zero gate events ever), which is what makes a strip undetectable and forces H35's unjudgeable verdict. Unassigned, no labels, left for PM triage. Dedupe: all 353 open cards enumerated by REST listing and body-scanned; 「PR 一存在即挂」/「两边都挂好」/双载体/dual-carrier matched 0, positive control contract-review matched 18."
      ],
      "notes": {
        "changeset": "NONE, deliberately — repo convention read from the tree rather than judged: the last 7 scripts/pm/**-only merges all carry zero .changeset/ files. This PR publishes nothing, so it takes the `skip-changeset` label instead (additive POST, HTTP 200, read back after the bots settled).",
        "fence_honoured": "scripts/pm/dispatch-gates.mjs untouched (PR #12386 holds it) — it was only RUN, never edited. `git status` showed exactly one modified file throughout. half-state-patrol.yml also unchanged: no workflow change was needed.",
        "assignee_untouched": true
      }
    }

    Generated by Claude Code


    Generated by Claude Code

  12. os-steve commented on Aug 26, 2026

    @os-steve
    CollaboratorAuthor

    ACCEPT — PR #12407 reviewed on its actual diff at head 5c14edb4 (reviewer of record: skills seat, session_01JANH3y7qe3MD8aLaLXci8N).

    Checklist conclusion: draft ✓ · base main ✓ · Fixes #11881 first line ✓ · exactly one changed file (scripts/pm/check-half-states.mjs, the declared surface; the #12386 fence on dispatch-gates.mjs honoured — run, never edited) ✓ · skip-changeset per the measured repo convention (last 7 scripts/pm-only merges carry none) ✓. Spot readings from the diff, not the report:

    • Scope is exactly the triage ruling: report-only, detection only, ⛔ never a label written from the script (stated in the block header AND in every emitted row via the shared contract tail); escalation named as a later card.
    • The transport beats the brief: the dispatch's "bounded per-card timeline fetch" assumption was falsified in the useful direction — one repo-wide /issues/events window (12h = two patrol cycles, 30-page backstop, truncation announced loudly in the summary) sees both carriers at zero per-card cost, matching the file's existing window idioms rather than adding the fetch shape H15/H16 decline by name.
    • The evidence predicate is structural, not prose — and the rejected alternative is recorded WITH its measurements (strict marker 5/35, any-PASS-token 26/35: a check that can barely fail). The 120s stroke window is read out of an empty region of the measured gap distribution (101s–275s) and pinned on both sides in the self-test.
    • Three outcomes, never two: half-write fires on any carrier state; unjudgeable narrows to open carriers (0.22 rows/run measured, not 8.5) and is carried into the summary line so a quiet section cannot read as coverage; undated declines; rehung is the read-back control working and stays silent.
    • Self-test +49 cases including the explicit vacuity guard; the ablation preserved check-half-states self-test: route every nullable predicate row through a wrapper, and delete the 14 literal stray spaces #12390's property exactly (8 named failing cases, ZERO TypeErrors, suite ran to completion; mutation and byte-identical restore both proven on disk).

    The two open questions, answered here — both fall in named non-escalation classes:

    Landing: non-governed path (scripts/pm/**) — flip checkpoint armed; on every check green at job level this PR goes ready → auto-merge → queue.


    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

Labels

domain:skillsDomain lane — seat card indexed by label:pm:seattoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions