Repository navigation
No gate bans the whole-set label PUT that #10703 removed - the verb can be reintroduced silently #10778
Description
Activity
huangyiirene commented
on Aug 21, 2026 CollaboratorMore actionsTriage (finding round): promoted →
pm:queue, type Task — a measured loss class (#10703's erasedskip-changeset) guarded today only by prose plus one script's self-test is exactly the "live defect tracked only by prose" shape this repo keeps re-paying for. Gate-strengthening only, no maintainer floor involved. Dispatch note: the root-package.jsoncheck:*registration is fenced by the @changesets/cli v3 migration lane — land via directnode scripts/...invocation inlint.yml(existing precedent, anddispatch-gates.mjsreads either spelling), or after the fence lifts; the card's three assertions (no PUT in any spelling, no known whole-set-writing actions, reasoned allowlist) are the spec.
Generated by Claude Code
huangyiirene commented
on Aug 22, 2026 CollaboratorMore actionsQueued per maintainer ruling (2026-08-22)
The maintainer's decision-inbox ruling (recorded on decision card #10703) directs this follow-up into the queue.
State:
pm:queueadded.
Generated by Claude Code
Claim:
domain:devxPM seat, sessionsession_015ahemw8RcTgqtxrj15PEZx, branchclaude/issue-10778-whole-set-label-put-gate.Dispatching at a higher priority than triage graded, because the card's own title is now understating it. The title says the verb "can be reintroduced silently".
os-sam's 2026-08-23 measurement on PR #11470 shows it is not hypothetical — the verb is live, with a second perpetrator: the Auto Label workflow's whole-set PUT erased a label that had just been added through the additive endpoint.What it erased is the part that changes the grading:
#10703 (fixed, closed) the new measurement perpetrator PR-size labeler Auto Label workflow label erased skip-changeset(an exemption)needs:contract-review(a compensating safety control)failure direction a changeset gate falsely red — noisy, gets noticed a Clause-② card silently loses its pre-merge gate ⇒ #10703 fixed one labeler. The verb survived, and its second instance fails toward silent pass-through rather than a false red.
⭐ And the second-order cost
os-samnames is the sharper one: while this verb lives, "the label is absent" stops meaning anything — absence now has two causes (cleared after review, or erased by a PUT), so every seat must do extra forensics before acting on a missing gate label. Read-back is the only detection, so a write without one is silent.I checked whether this reaches my own escalation channel. The maintainer polls
needs-user-decision, and an erased label there would silently drop a decision from their queue. Enumerated just now: 20 openneeds-user-decisioncards, all present and correctly labelled. No evidence of loss today — but that is a spot check, not a guarantee, which is precisely the card's point.Pre-dispatch collision check
New gate script: free.
⚠️ The wiring step lands in.github/workflows/lint.yml, which PR #11864 (#11050, armed and currently in the merge queue) also touches at line ~988, and PR #11716's insertion at 3594 has already landed. A third co-tenancy in one shift. Their step and yours belong in different regions, so git should merge cleanly; if the branch goesdirty, mergeorigin/mainin and keep both steps — ⛔ never drop theirs, ⛔ never rebase or force-push.Model tier: opus. Live
--tierrun reports no path-derived mandate. Not fable despite the severity: the deliverable is well-specified (triage: "the card's three assertions are the spec") and the failure mode of a wrong gate is a false red on a legitimateuses:— loud, not silent.Zone 1 — rulings
- Triage's registration route stands: the root
package.jsoncheck:*entry is fenced by the @changesets/cli v3 lane, so wire it as a directnode scripts/...step inlint.yml. There is a landed precedent from today — PR feat(scripts): corpus-wide route-spelling detector for published prose, wired advisory #11864 did exactly this and recorded why in its workflow comment.dispatch-gates.mjsreads either spelling. - The three assertions are the spec, from the card: (a) no
PUTagainst/issues/{n}/labelsin any spelling —curl -X PUT,octokit/actions/github-scriptcallingissues.setLabels,gh api -X PUT …/labels; (b) nouses:of an action known to write the whole set (codelytv/pr-size-labelerandactions/labelerwere both verified to do so at their pinned versions, read out of source in The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703); (c) an allowlist entry requires a stated reason. - ⭐ Measure the population BEFORE building, because this gate may not be able to land green. If
Auto Labelor any other workflow currently violates, you cannot ship an enforcing gate without either fixing the violator or allowlisting it. Ruling on that fork: a small, local fix to the violating writer inpr-automation.ymlis in scope (that file was already made additive for two writers by The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703, so the shape exists). Anything larger — or a violation in a third-party action you cannot edit — is an allowlist entry with its reason stated, plus a separate card naming the violator. ⛔ Do not ship the gate red, and ⛔ do not quietly widen the allowlist to make a red go away.
Zone 2 — assumptions to falsify
- That
codelytv/pr-size-labelerandactions/labelerstill write the whole set at the versions pinned today. The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703 read that out of source; pins may have moved. - The exact spelling
Auto Labeluses.os-sammeasured the effect (a label disappeared); confirm the mechanism in the workflow source before writing a rule keyed on it.
⚠️ Stop-and-report if the violating population is large, or if the only way to green is an allowlist wide enough that the gate asserts nothing.
Generated by Claude Code
- Triage's registration route stands: the root
- added a commit that references this issue
on Aug 24, 2026 { "issue": 10778, "status": "done", "branch": "claude/issue-10778-whole-set-label-put-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/11880", "premise_still_valid": true, "summary": "Landed scripts/check-whole-set-label-write.mjs, wired as a direct `node scripts/...` step in lint.yml (root package.json is inside the @changesets/cli v3 fence), asserting all three of the card's assertions: no PUT against /issues/{n}/labels in any spelling (curl -X PUT, gh api -X PUT/--method PUT, octokit.request('PUT /repos/...'), a method:'PUT' fetch, and issues.setLabels), no `uses:` of an action measured to write the whole set, and an allowlist entry that REQUIRES a stated reason (run() refuses, exit 2, rather than passing). Population measured BEFORE building per Zone-1 ruling 3: 0 violations in executable content, 0 known whole-set labelers pinned, so no violator needed fixing and the allowlist ships EMPTY. The card's premise (no gate bans the verb) held and is now closed. TWO ASSUMPTIONS FALSIFIED. (a) The dispatch's severity escalation does not hold: `Auto Label` is NOT a second perpetrator. In source it runs `node scripts/pr-labels.mjs --paths`, POST-only with no DELETE; in PR #11470's timeline github-actions[bot] emitted 4 labeled and 0 unlabeled events, while both `unlabeled needs:contract-review` events came from claude[bot] (a seat) at 21:41:25Z and 22:40:21Z, 33 and 92 minutes AFTER the Auto Label job ran at 21:08:46Z. Contrast #10698, the real PUT: `unlabeled skip-changeset | github-actions[bot]` in the SAME SECOND as that bot's own `labeled size/l`. os-sam measured the effect and said so; the mechanism was assumed. (b) Zone-2's pinned-version assumption is moot, not confirmed: neither codelytv/pr-size-labeler nor actions/labeler is pinned anywhere any more, so there is no version to re-read; the roster is forward-looking. Consequence recorded, not encoded: the gate could not have prevented the #11470 loss, because the actor was a seat, outside .github/workflows/** and scripts/**.", "tests": "All against final HEAD 89f74f2d4. (1) The 20 gate families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` from the real changeset (it discovers this PR's own gate), plus check:nul-bytes, run under scripts/pm/os-verify-lock.sh: 21 PASS, 1 FAIL. Exit codes captured before any pipe (per-gate log files, never `| tail`). The single red is check:type-check-debt and it is environmental, proven not asserted: its --re-measure leg REFUSES in a fresh worktree because the workspace closure is unbuilt (packages/{core,spec,runtime,lint}/dist all ABSENT), lint.yml builds that closure at line 3574 immediately before calling it at 3577, and this diff adds ZERO TypeScript and zero package source (2 files: a workflow comment+step and a new .mjs). Its sibling limb check:type-check-coverage PASSES, as does that gate's own self-test (47 semantic + 59 observation + 29 re-measure + 28 built-closure + 19 auto-lowering cases). DECLARED NARROWING: the --re-measure leg was not run locally. (2) eslint . --no-inline-config --format json over the WHOLE repo, not a narrowing: 5037 files selected by eslint's own config, 0 errors, 0 warnings. lint.yml is not in eslint's population; its YAML was parsed and the step confirmed to land in the `lint` job, and check:workflow-status-functions / check:required-contexts / check-step-collectors / check-aggregator-roster all pass. (3) NON-VACUITY, both directions, every mutation proven on disk and every leg under `trap <restore> EXIT INT TERM`; every mutant node --check'ed so a red cannot be a parse error wearing proof. FAIL-BEFORE/PASS-AFTER on the real tree: injected `gh api -X PUT \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels\"` into pr-automation.yml, anchor asserted to occur exactly once, injected-text count on disk 0->1, sha 9abb4721bb828d14 -> 57e811b11a7fda6b; gate exit 1 naming `.github/workflows/pr-automation.yml:152` and the spelling; the comment-cleared count STAYED at 9 while judged went 0->1, so the executable limb fired, not the prose; restored byte-identically (sha back to 9abb4721bb828d14, injected count 0), gate exit 0. FOUR ABLATIONS, each reddening --self-test on exactly the cases it owns: SET_LABELS_RE neutered -> 1 failure (the setLabels case); WHOLE_SET_ACTIONS emptied -> 3 failures (all three uses: cases); allowlist reason requirement removed -> `REFUSE allowlist entry without a reason: expected 2, got 0`, i.e. an unreasoned exemption would have silently PASSED; comment blanking disabled -> 14 failures in the over-red direction, the gate reddening on the documentation of its own rule. All four restored byte-identically (gate sha back to 28fe58215c14f5f6). No build/dist is involved: the gate is plain node run from source, so there is no dist preflight to do. (4) The gate's own --self-test: 24 fixture trees on disk + 5 refusals + 1 allowlist hatch. (5) Live verdict: `0 violations - 194 file(s) over 3 root(s) - 11 raw mention(s) - 11 in comments/prose (cleared) - 0 in EXECUTABLE content (judged) - 147 uses: pin(s) over 18 distinct action(s) judged - 0 allowlist entr(ies)`. The judged zero is a MEASUREMENT: the same matcher runs over raw and comment-blanked text, both counts print, and PROSE_PROBES declares the live prose that must keep matching or the gate refuses instead of passing. The uses: limb is not vacuous at all (147 pins judged and cleared each run).", "open_questions": [], "out_of_scope_findings": [ "filed as #11881: [finding] a gate label removed by a SEAT is still undetectable - the new gate cannot reach that surface; also records the falsified Auto Label attribution and preserves os-sam's explicitly-deferred 'make Clause-2 label loss noisy' recommendation, which would otherwise be buried when this card closes. Dedupe was controlled: semantic search returned 0 and was NOT trusted on that alone (this thread records a zero that was a query artifact), so all 350 open issues were enumerated and body-scanned; positive control 'label' matched 71, while whole-set/setLabels/'PUT /issues' matched only #10778." ] }Notes for the PM, outside the JSON.
- Draft PR feat(tooling): ban the whole-set label PUT in every spelling #11880, not flipped ready, auto-merge not armed.
skip-changesetapplied through the additive endpoint (HTTP 200) and read back:['ci/cd', 'size/l', 'skip-changeset']— the bots added their two labels without erasing mine, which is exactly the post-The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703 behaviour this card is about. - Co-tenancy is clean. The wiring step landed at
lint.yml:1840-1877, beside the existingAdditive label-write self-teststep it complements — clear of PR feat(scripts): corpus-wide route-spelling detector for published prose, wired advisory #11864's insertion at ~988 and PR fix(spec): re-export the three types the root entry's own inferred types mention (#11350) #11716's at 3594.origin/mainmoved387e23138→e75e34381while this was in flight;git merge-treereports 0 conflict markers, so no merge was needed and nothing of theirs was touched. Never rebased, never force-pushed. - The one red is environmental and CI will run it against a built tree — see
testsabove for the proof rather than the assertion. ⚠️ The card's own title is now the accurate one, and the dispatch's escalation is not. "The verb can be reintroduced silently" was true; "the verb is live with a second perpetrator" was not. The gate is still worth exactly what the original card said it was worth — it just closes a future hole rather than an active one, and the perf(plugin-security): batch capability + overlay boot seeds, skip no-op writes #11470 loss it was escalated on came from a surface no repo-scanning gate can reach.
Generated by Claude Code
Generated by Claude Code
- Draft PR feat(tooling): ban the whole-set label PUT in every spelling #11880, not flipped ready, auto-merge not armed.
⚠️ Correction: my severity escalation was wrong.Auto Labelis NOT a second perpetrator.domain:devxPM seat, sessionsession_015ahemw8RcTgqtxrj15PEZx. Retracting a claim I made in the claim comment above, and repeated to the maintainer as established fact.I wrote that the verb is "live, with a second perpetrator", on the strength of
os-sam's 2026-08-23 measurement. The measurement was sound; my reading of its mechanism was not.The dispatched dev tested it, and I verified independently rather than taking the correction on trust:
scripts/pr-labels.mjs— whatAuto Labelactually runs — emits onlyPOSTand a targetedDELETE:CODE 117: const FORBIDDEN_VERB = 'PUT'; ← a constant used to FORBID it CODE 338: method: 'POST' CODE 348: method: 'DELETE' ← one label, BY NAME CODE 364: method: 'POST'Every other
PUT/setLabelsoccurrence in that file is a comment — its own documentation of the hazard it exists to avoid. The file states the distinction at:54-57:DELETE /issues/{n}/labels/{name}removes ONE label, BY NAME.PUT /issues/{n}/labelsreplaces the whole set. Destructive.
Neither POST nor DELETE carries a label this writer does not name, so neither can destroy a concurrent write.⚠️ My first pass nearly reproduced the same error in reverse: a raw grep reportedPUT13 /DELETE13 in that file and looked like it contradicted the dev. A grep count is not a reading until you look at what it counted.The timeline settles it, and it is what an effect-level reading cannot reach. On PR #11470:
github-actions[bot]emitted 4labeled, 0unlabeled; bothunlabeled needs:contract-reviewevents came fromclaude[bot]— a seat — at 21:41:25Z and 22:40:21Z, 33 and 92 minutes after the Auto Label job ran at 21:08:46Z. Against the genuine PUT in #10698:unlabeled skip-changeset | github-actions[bot]in the same second as that bot's ownlabeled size/l.⛔ Nothing here reflects on
os-sam. Their comment says plainly that they measured the effect, scoped what they had not established, and left the mechanism to this lane. I am the one who turned an assumed mechanism into a severity claim and shipped it into a dispatch.⭐ The real exposure is elsewhere, and it is not smaller
the gate could not have prevented the #11470 loss, because the actor was a seat, outside
.github/workflows/**andscripts/**.So a compensating control on a Clause-② card was removed by a seat, 33 minutes after any bot touched the PR, and nothing detects that — the new gate included. Filed as #11881, which also preserves
os-sam's explicitly-deferred recommendation to make Clause-② label loss noisy, so it does not die when this card closes.This card's own premise held and is closed by PR #11880
Nothing banned the verb; now something does. Measured population before building: 0 violations, 0 pinned whole-set labelers, so the allowlist ships empty and no violator needed fixing. Zone 2 item (b) came back moot rather than confirmed — neither
codelytv/pr-size-labelernoractions/labeleris pinned anywhere any more (every remaining mention is a comment marking them retired), so the roster is forward-looking. The gate is worth having on that basis; it is just not the thing that would have saved #11470.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 17, 2026
Split out of #10703, which made both label writers in
.github/workflows/pr-automation.ymladditive. That card removed the two whole-setPUT /issues/{n}/labelswrites; it did not make the verb unavailable.The gap
Nothing mechanically stops a future workflow, action or agent from reintroducing a whole-set label write. The failure is silent and recurring: a PUT destroys any label that lands between the writer's read and its write, and #10703 records a measured loss on PR #10698 where a seat's
skip-changesetwas erased one second after an additive POST returned HTTP 200 — which turns a PR that publishes nothing into a falsechangeset-checkred.Today the whole guard is (a) a prose paragraph in that workflow's header and (b)
scripts/pr-labels.mjs --self-test, which only constrains that one script. Neither notices a newly added third-party labeler, nor a second workflow that calls the endpoint directly. That is the same shape the original card complained about — a live defect tracked only by prose — moved up one level.What a gate would assert
Over
.github/workflows/**(and plausiblyscripts/**):PUTagainst/issues/{n}/labels, in any spelling —curl -X PUT,octokit/actions/github-scriptcallingissues.setLabels, orgh api -X PUT .../labels;uses:of an action known to write the whole set.codelytv/pr-size-labelerandactions/labelerwere both verified to do so at their pinned versions, read out of source, in The PR-size labeler's whole-set PUT erases a seat-appliedskip-changesetone second after an additive POST — measured loss, and the only tracker is prose in a closed card #10703;Why it was not done in that PR
A
check:*gate needs an entry in the rootpackage.json, which is fenced by the @changesets/cli v3 migration lane, so that PR could not add one. It wired the existing self-test intolint.ymlby directnode scripts/...invocation instead, which covers the one script and nothing else. A real gate wants the normalcheck:*shape and should land once that fence lifts, or with the lane owner's agreement.Filed unassigned, PM triage.
Generated by Claude Code
Generated by Claude Code