tooling(pm): sweep the pm-loop state labels off cards closed on a merged delivery - #16021
Conversation
A card GitHub closes on a merged `Fixes` pull request keeps every label, so the pm-loop state labels stay behind as claims that work is in flight. The seat has been removing them by hand, once per landing. `scripts/pm/sweep-closed-cards.mjs` takes that stroke: it lists the closed cards carrying H22's residue set (imported, never restated), establishes that the close really was a merged delivery by one of two routes -- a closing commit contained in the default branch, or a pull request whose body closes the card and merged into it -- and removes exactly the residue labels, leaving ownership, priority and outcome untouched. Each stripped card gets one comment naming the readings taken. The action set is bounded to the fresh edge by a window, because the 2026-08-31 maintainer ruling refused a backfill over the archived stock while keeping close-time hygiene as the convention. `--all-time` exists so a seat can measure that stock in a dry run; the workflow never passes it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…se unknown flags `check-self-test-wired` is the gate that settles where the self-test belongs: CI runs this script, so CI must run its cases. The patrol step now runs them first and skips the sweep entirely if they fail -- this step writes to other people's cards, and a broken predicate that still runs is the one failure the next run cannot undo. The CLI gains `--dry-run` as a real flag so the workflow can name the mode it is in, and refuses any argument it does not know: a caller who believes it passed a mode flag and did not must not get a run that looks ordinary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
|
ACCEPT — lands #16005 as ruled, with both declared deviations accepted. Pure code (a What the seat verified, at head
Open questions answered: Q1 → A, keep the 72-hour window — the 2026-08-31 ruling refused the backfill by name and the measured cost was the fresh-edge round trip, which this covers. Q2 → A for this PR; the adopt-list sentence is owed at the workflow's next touch (the objectui anchor flight, objectui#5986) and is recorded on #16005's thread. Implemented-by: os-dev executor, flight #16005, branch Generated by Claude Code |
Fixes #16005
GitHub closes a card when a pull request carrying
Fixes #Nmerges, and it leaves every label in place. The pm-loop state labels are CLAIMS that work is in flight, so on such a card they are stale claims — and the seat has been removing them by hand, once per landing (eighteen identical round trips in one measured shift). This takes that stroke.What lands
scripts/pm/sweep-closed-cards.mjs— dry run by default,--writeacts,--self-testis offline (87 cases, 9 batteries with per-battery floors). It IMPORTS H22's residue label set fromcheck-half-states.mjsand never restates it.check-half-states.mjsitself is untouched (check-half-states: H8, H19 and the H24 inventory quote 「同笔摘 assignee」 — a phrasing that is in no SKILL.md, and that now prescribes half of the landed release act #15899 is in flight on it)..github/workflows/half-state-patrol.yml, afterRun the live sweepand before the anchor write, gated ongithub.repositoryso a verbatim copy in a sibling repo skips it (objectui has no patrol anchor yet — objectui#5986).package.jsonalias,check:pm-closed-card-sweep, running the self-test.pull_request:paths:list, naming the new script. Declared here because it is beyond the ruled "one step": without it, a PR touching only the sweep script never fires the PR-time proof this trigger exists to give — the defect the file's own comments argue at length one file along (scripts/invoked-as.mjs).The condition to act
A card is stripped only when its
state_reasoniscompletedAND its closing is a merged pull request, established by one of two routes:closedevent carriescommit_id, and that commit is confirmed contained in the repo's default branch.cross-referenced/connectedpull request whose BODY carries a closing keyword bound to this card's number, read throughclosingKeywordTargets(so a backticked mention does not deliver), confirmedmergedinto the default branch.Route B is deliberately narrower than "a merged pull request that mentions this card": a cross-reference is created by any mention, and stripping on that relation would de-label cards a merged pull request merely discussed.
Part of #Ncloses nothing, so it is not accepted as a closing either.⛔ A card with a delivering pull request still OPEN is LEFT, ahead of both routes. That is H8's refusal one layer down, for H8's reason: a card whose remaining half is in flight must not read as un-dispatched.
A closed card that neither route attributes to a merged pull request — closed by hand,
not_planned,duplicate— is OUT: the seat that closed it owes the strip, and the sweep says so in its report rather than acting.The window, and the ruling it obeys
The action set is bounded to the fresh edge (
--since-hours, default 72h — twelve runs of slack over the six-hourly patrol). This is a declared narrowing of the ruled population, and the reason is a maintainer ruling recorded incheck-half-states.mjs's H22 section, 2026-08-31, verbatim and untranslated: 「13605 已关卡 为什么要清理。普查时不应该只看open的卡片吗,其他同意」 — a BACKFILL over the archived stock is the write that ruling refused by name, while the same ruling's item ③ kept close-time hygiene as the convention. This step is that convention, mechanized; it is not the backfill.The size of the difference, measured today in dry run: the archive stock is at least 1,907 closed cards carrying residue (1,837 of them
completed; thepm:dispatchedlisting hit its page ceiling, so that is a floor and the report says so). The 72h window is 78. Without the window the first scheduled run would post roughly two thousand comments onto archived cards, four times a day.--all-timeremoves the floor so a seat can MEASURE that stock in a dry run; the workflow never passes it, and--write --all-timeprints the ruling at the caller before proceeding.Live proof — dry run, read-only, 2026-09-05, from a dev container
Five of the 74, with the closing evidence each strip rests on:
The four left: #15813 (
duplicate), #15279 (not_planned), #15865 and #14796 (closed by hand, no merged pull request declaring the close). ⛔ No--writerun was made from this container.Two readings the tree corrected
compare/{sha}...mainwith statusesbehind/identical; those two halves disagree. Measured against this repo:compare/main...{an older commit on main}isbehindwithahead_by 0, whilecompare/{that commit}...mainisahead. This file therefore askscompare/{defaultBranch}...{sha}— the brief's statuses, the other orientation — and requiresahead_by === 0as well, since that count is the invariant the statuses summarise. Getting this backwards inverts the verdict silently, so the measurement is in the file's header and both directions are pinned in the self-test.package.jsonalias alone is NOT derived bydispatch-gates, which enumerates families from workflow invocations.check-self-test-wiredsettled it mechanically (exit 1: "CI runs this script and its code carries--self-test, but no workflow ever executes it with that flag"), and prescribed the remedy: wire it into the step that runs it. The patrol step now runs the self-test FIRST and skips the sweep entirely if it fails — this step writes to other people's cards, and a broken predicate that still runs is the one failure the next run cannot undo. The alias stays as the dev-facing spelling of the same command.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived the families off the merge base (no path list of my own: the script reads the change set from git). Reconciled with--ranat the final commit:51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASURED. All green, except three that answered PREREQUISITE NOT MET (exit 3, NOT a pass and NOT a failure):check:dts-closure,check:dual-build-cjs-loadsandcheck:published-readme-exportsall read a built tree and no package here has adist/. This diff adds no package source and noexportsentry, so it cannot move a declaration closure; CI runs them on the built tree.check-single-claim-paths.mjsanswered NOT WIRED without aPR_NUMBER(its own words, and its self-test is green through the alias).Also run, because the derivation flags their silence as evidence in neither direction when a path of yours sits in their roster's directory:
check-ci-filter-parity --self-test,check-published-list-mirrors(both spellings),pr-labels --self-test,check:console-injection,check:engine-double-contract,check:i18n-stale-fill,check:pm-label-desc-cap,check:single-claim-paths,check:pm-governed-prose,check:pm-half-states,check:pm-clause2-carriers— all green.node scripts/pm/check-governed-merges.mjs --teston the final file list:0 of 3 path(s) hit the register— NOT governed, ordinary queue landing applies.Whole-repo
pnpm lint(eslint . --no-inline-config) throughscripts/pm/os-verify-lock.sh(slotissue-16005): VERDICT command-exit 0, held 107s. No narrowing.There is no
actionlintin this repo — no binary on PATH, and no reference to it inpackage.json,.github/workflows/**orscripts/**. The repo's workflow lint is the gate family, and the members of it that this diff derives all ran green:check:workflow-status-functions,check:ci-filter-parity,check:required-contexts,check-step-collectors,check-self-test-workflow-commands,check-self-test-wired. The edited workflow also parses under the workspace'syamlpackage, with the new step in the ruled position.Ablation, to show the self-test can fail — no build is involved (a plain
.mjsnode runs directly; no bundle, nodist, noexportsresolution). Two legs, each mutating one load-bearing conjunct, the mutation confirmed on disk by anchor and injected-text grep counts (1 to 0, and 1 injected) rather than by an editor's exit code, restored withgit checkout HEAD -- ABSOLUTE_PATHunder a trap and proved restored by an emptygit diff HEADplus an identical blob hashcb3459eba4:Both legs green again after the restore.
mainis GREEN on the merge-driver gate as off50c394da5— 57 check runs, zero non-success — so #15992's red no longer applies to this PR.pnpm check:merge-driveris also green locally on this branch.skip-changeset: this publishes nothing from any package.Gate results are quoted from each gate's own verdict line; every exit code was captured before any pipe.
🤖 Generated with Claude Code
https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Generated by Claude Code