Skip to content

tooling(pm): sweep the pm-loop state labels off cards closed on a merged delivery - #16021

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-16005-closed-card-sweep
Sep 5, 2026
Merged

tooling(pm): sweep the pm-loop state labels off cards closed on a merged delivery#16021
os-steve merged 2 commits into
mainfrom
claude/issue-16005-closed-card-sweep

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #16005

GitHub closes a card when a pull request carrying Fixes #N merges, 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

  • NEW scripts/pm/sweep-closed-cards.mjs — dry run by default, --write acts, --self-test is offline (87 cases, 9 batteries with per-battery floors). It IMPORTS H22's residue label set from check-half-states.mjs and never restates it. check-half-states.mjs itself 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).
  • ONE new step in .github/workflows/half-state-patrol.yml, after Run the live sweep and before the anchor write, gated on github.repository so a verbatim copy in a sibling repo skips it (objectui has no patrol anchor yet — objectui#5986).
  • ONE package.json alias, check:pm-closed-card-sweep, running the self-test.
  • ONE row in that workflow's 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_reason is completed AND its closing is a merged pull request, established by one of two routes:

  • ROUTE A, the closing COMMIT: the timeline's last closed event carries commit_id, and that commit is confirmed contained in the repo's default branch.
  • ROUTE B, the closing PULL REQUEST: a cross-referenced / connected pull request whose BODY carries a closing keyword bound to this card's number, read through closingKeywordTargets (so a backticked mention does not deliver), confirmed merged into 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 #N closes 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 in check-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; the pm:dispatched listing 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-time removes the floor so a seat can MEASURE that stock in a dry run; the workflow never passes it, and --write --all-time prints the ruling at the caller before proceeding.

Live proof — dry run, read-only, 2026-09-05, from a dev container

listed: 78 closed card(s) carrying residue · candidates: 76
requests: 7 listing · 150 evidence · 0 write
verdicts: 74 strip · 4 leave · 0 unjudged · 0 not judged
left, by reason: 2 not-completed · 2 no-closing-delivery

Five of the 74, with the closing evidence each strip rests on:

#14552  [pm:dispatched] — closing PR #15962, commit f7db8f4fd2, merged into main
#15835  [pm:dispatched] — closing PR #15940, commit 904e70780b, merged into main
#15746  [pm:dispatched] — closing PR #15925, commit 0467941823, merged into main
#15852  [pm:queue]      — closing PR #15914, commit f1e91595f7, merged into main
#14946  [pm:dispatched] — closing PR #15912, commit ea03c7c16f, merged into main

The four left: #15813 (duplicate), #15279 (not_planned), #15865 and #14796 (closed by hand, no merged pull request declaring the close). ⛔ No --write run was made from this container.

Two readings the tree corrected

  1. The compare orientation. The dispatch brief named compare/{sha}...main with statuses behind/identical; those two halves disagree. Measured against this repo: compare/main...{an older commit on main} is behind with ahead_by 0, while compare/{that commit}...main is ahead. This file therefore asks compare/{defaultBranch}...{sha} — the brief's statuses, the other orientation — and requires ahead_by === 0 as 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.
  2. Where the self-test had to be wired. A package.json alias alone is NOT derived by dispatch-gates, which enumerates families from workflow invocations. check-self-test-wired settled 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/objectstack derived the families off the merge base (no path list of my own: the script reads the change set from git). Reconciled with --ran at 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-loads and check:published-readme-exports all read a built tree and no package here has a dist/. This diff adds no package source and no exports entry, so it cannot move a declaration closure; CI runs them on the built tree. check-single-claim-paths.mjs answered NOT WIRED without a PR_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 --test on 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) through scripts/pm/os-verify-lock.sh (slot issue-16005): VERDICT command-exit 0, held 107s. No narrowing.

There is no actionlint in this repo — no binary on PATH, and no reference to it in package.json, .github/workflows/** or scripts/**. 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's yaml package, with the new step in the ruled position.

Ablation, to show the self-test can fail — no build is involved (a plain .mjs node runs directly; no bundle, no dist, no exports resolution). 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 with git checkout HEAD -- ABSOLUTE_PATH under a trap and proved restored by an empty git diff HEAD plus an identical blob hash cb3459eba4:

ahead_by === 0 dropped from commitIsContained yields EXIT 1, 1 of 87 failed:
  "⛔ nor is a `behind` that still carries commits the branch lacks"
pr.fetched === true dropped from route B      yields EXIT 1, 1 of 87 failed:
  "⛔ an UNFETCHED base is never accepted as the default branch"

Both legs green again after the restore.

main is GREEN on the merge-driver gate as of f50c394da5 — 57 check runs, zero non-success — so #15992's red no longer applies to this PR. pnpm check:merge-driver is 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

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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added size/xl ci/cd dependencies Pull requests that update a dependency file labels Sep 5, 2026

os-steve commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

ACCEPT — lands #16005 as ruled, with both declared deviations accepted. Pure code (a scripts/pm/ script, one workflow step, one alias; governed-surface predicate exit 0), so ready + SQUASH auto-merge follow in this same act.

What the seat verified, at head 51bc005b9c, in its own compare worktree (merge base f7db8f4fd2)

  • Three files, +1,368/−0. The residue set is PM_RESIDUE_LABELS IMPORTED from check-half-states.mjs, never restated; needs-user-decision, finding, domain:*, type, pm:seat and the rest are outside it by construction. The action set is bounded by DEFAULT_WINDOW_HOURS = 72 with --all-time as the measuring knob the workflow never passes.
  • The workflow step is gated on github.repository == 'objectstack-ai/objectstack', runs --dry-run on pull_request and --write on the schedule and dispatch, never fails the job, and the script's path was added to the workflow's pull_request paths list so a change to the sweep fires its own PR-time proof.
  • Self-test: 87 cases across 9 batteries, exit 0 in the seat's run (the compare-orientation correction pinned both ways); check-governed-merges --test exit 0.
  • CI, seat's read 20:0xZ: 38 check runs, 31 success, 7 skipped, none failing — including Live half-state sweep, whose job ran the new step on a real runner in dry-run.
  • Live proof accepted as reported: 78 closed cards in the 72-hour window, 74 STRIP, 4 LEFT with the right reasons (a duplicate, a not-planned, two closed by hand), 0 unjudged; the archive stock (≥ 1,907) stays untouched.
  • Body line 1 Fixes #16005; skip-changeset present.

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 claude/issue-16005-closed-card-sweep
Reviewed-by: pm-dispatch skills seat, https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 5, 2026 20:00
@os-steve
os-steve enabled auto-merge September 5, 2026 20:01
@os-steve
os-steve added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit c223028 Sep 5, 2026
40 checks passed
@os-steve
os-steve deleted the claude/issue-16005-closed-card-sweep branch September 5, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants