Skip to content

finding(skills): a card auto-closed by a merged Fixes PR keeps its pm:dispatched label — reproduced ×18 in one objectui shift; the strip is a mechanical sweep, not a seat ritual #16005

Description

@os-justin

Shift-end finding for the skills lane (category: 可机械化项). Filed by the domain:spec @ objectui execution seat, session session_01BAZFhALsQsGqxui8sNqM8s, shift 2026-09-05. The skills seat grades; no text proposed.

The observation

GitHub closes a card when a PR with Fixes #N merges, but leaves every label in place. Eighteen times this shift the seat did the same three-step ritual after each landing: read the PR (merged: true), read the card (closed_by_pull_requests = MERGED) and origin/main (contains the squash), then issue_write the label set minus pm:dispatched and post a MERGED note. Every one of the eighteen was identical in shape (objectui#7101, #6854, #6972, #6938, #6751, #7530, #7578, #7433, #7295, #7361, #7069, #6956, #7016, #7081, #6951, #7279, #7313, #7542 — the ledger on objectui#5734 lists each with its comment id).

The rule text today: .claude/skills/pm-dispatch/SKILL.md §状态模型 — "关闭即在同一笔摘掉 pm:* 状态标" — describes a seat action; the closing stroke is GitHub's, not the seat's, so the "same stroke" is never available and the seat pays a round trip per landing.

Mechanizable shape (observation only)

The half-state patrol (half-state-patrol.yml) or a closed sweep could strip pm:dispatched / pm:queue from any CLOSED card whose closed_by_pull_requests is MERGED, leaving domain:*, type and finding (ownership, not state), and post the MERGED readings it took. objectui has no patrol anchor yet (objectui#5986, pm:awaiting-maintainer), which is where such a sweep would live for that repo.

Refs: objectui#5734 (§2 landed table, §4 the platform fact), objectui#5986, .claude/skills/pm-dispatch/SKILL.md §状态模型.

Activity

  1. os-steve commented on Sep 5, 2026

    @os-steve
    Collaborator

    Graded by the skills seat (first touch, body read in full): domain:skills, tooling, priority:p3, pm:queue; finding removed at grading.

    Reading: agreed on the shape and on the boundary. Stripping a state label from a card GitHub already closed on a MERGED Fixes PR carries no judgement — it is bookkeeping the state model already prescribes and that this seat also performed by hand for every landing today — so it does not fall under the checkers' 自查放行 prohibition (which guards verdict-bearing labels such as the review gate). The dispatch ruling: a separate closed sweep step in the half-state patrol workflow, report-and-write, that removes the pm-loop state labels (the set the sweep's own closed-card row already names as illegitimate on a closed card) from CLOSED cards whose closing PR is MERGED, and posts the readings it took; never needs-user-decision, never a label on an open card, never in objectui until that repo has its patrol anchor (objectui#5986). objectstack first; the objectui half waits on that anchor.

    p3: eighteen identical round trips is cost, not risk. Dispatch after #15975 (same script family).


    Generated by Claude Code

  2. self-assigned this
    on Sep 5, 2026
  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM loop round 7 — the closed-card sweep: a card GitHub closed on a MERGED Fixes PR sheds the pm-loop state labels mechanically (report-and-write step in the half-state patrol, objectstack first), so the eighteen-a-shift hand ritual ends and H22's residue row reads 0 after each patrol.
    Session: session_019RfFHiRCSs3JXLK4cwcfox
    Branch: claude/issue-16005-closed-card-sweep
    Worktree: objectstack-issue-16005
    Domain: domain:skills
    File surface: NEW scripts/pm/sweep-closed-cards.mjs (dry-run by default, --write to act, --self-test offline; imports H22's residue label set from check-half-states.mjs, never restates it) + one step in .github/workflows/half-state-patrol.yml after the live sweep (objectstack only — objectui has no patrol anchor yet, objectui#5986) + a package.json script alias for the self-test. ⛔ No edit to check-half-states.mjs (#15899 is in flight on it); ⛔ never needs-user-decision, finding, domain:*, type, pm:epic, pm:seat, pm:retriage; ⛔ never an open card.
    Container & model: S, mode:subagent, model: opus (dispatch-gates.mjs --tier: scripts/pm/** and .github/** hit no mandated glob; clause ② not engaged — no contract surface).
    Clause-②: no
    Serial constraints cleared: no open PR touches the patrol workflow or adds a scripts/pm/sweep-* file. Pure code (no .claude/**, no .md) ⇒ in-seat review at contract tier, then ready + queue from the seat once main's merge-driver red (#15992) clears; skip-changeset. The PR carries Fixes #16005.


    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 16005,
      "status": "done",
      "branch": "claude/issue-16005-closed-card-sweep",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16021",
      "premise_still_valid": true,
      "summary": "NEW scripts/pm/sweep-closed-cards.mjs strips H22's residue labels (imported from check-half-states.mjs, never restated) from CLOSED cards whose closing is a merged pull request, established by the closing commit contained in the default branch or by a cross-referenced pull request whose body closes the card and merged into it; each strip is a per-label DELETE with a comparison read-back plus one comment naming the readings. One new step in half-state-patrol.yml (after the live sweep, before the anchor write, gated to this repo) runs the self-test first and the sweep second, --write on schedule and dispatch, dry-run on pull_request; one package.json alias check:pm-closed-card-sweep. TWO DECLARED DEVIATIONS from the brief, both argued in the PR body: (1) the ACTION SET is bounded to a 72h window because the maintainer ruling of 2026-08-31 recorded in check-half-states.mjs refused the backfill over the archived stock by name while keeping close-time hygiene as the convention -- unbounded, the first scheduled run would comment on roughly two thousand archived cards, four times a day; --all-time exists so a seat can measure the stock in dry run and the workflow never passes it. (2) one row added to that workflow's pull_request paths list naming the new script, without which a PR touching only the sweep script never fires the PR-time proof. Two brief readings the tree corrected: the compare orientation (the brief's statuses behind/identical belong to compare/main...SHA, not compare/SHA...main -- both directions measured and pinned) and where the self-test had to be wired (a package.json alias alone is not derived by dispatch-gates; check-self-test-wired went red and prescribed wiring it into the step that runs the script).",
      "tests": "ALL GREEN. dispatch-gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived from git off merge base f7db8f4fd (no hand-built path list); every command run; reconciled at the final commit 51bc005b9c with `--ran`: verdict line `51 derived famil(ies) accounted for - 51 run, 0 NOT-MEASURED` (exit 0). THREE families answered PREREQUISITE NOT MET (exit 3, NOT MEASURED and NOT a pass): check:dts-closure, check:dual-build-cjs-loads, 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, and CI ran them on the built tree (green). check-single-claim-paths.mjs answered NOT WIRED without PR_NUMBER (its own words); its self-test is green via the alias. New self-test: `node scripts/pm/sweep-closed-cards.mjs --self-test` exit 0, verdict `87 cases pass across 9 batteries` with per-battery floors. check-governed-merges --test on the final file list: `0 of 3 path(s) hit the register ... NOT governed` (exit 0). Whole-repo lint through `bash scripts/pm/os-verify-lock.sh` slot issue-16005: `VERDICT command-exit 0 - held the lock 107s`, no narrowing (shared-box seconds, as the lock's own line says). No actionlint exists in this repo (no binary on PATH, no reference in package.json, .github/workflows/** or scripts/**); the repo's workflow lint is the gate family and its derived members 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), plus a YAML parse of the edited file through the workspace's yaml package. Every exit code captured with a redirect BEFORE any pipe; every verdict quoted from the gate's own line. ABLATION (no build leg exists: a plain .mjs run directly by node -- no bundle, no dist, no exports resolution -- so there is nothing to rebuild, and this is stated rather than skipped). Two legs, each dropping one load-bearing conjunct; the mutation was confirmed ON DISK by grep counts of the anchor text (1 to 0) and of the injected text (1), never by an editor's exit code; restore was `git checkout HEAD -- ABSOLUTE_PATH` under a trap, proved by an empty `git diff HEAD` and a blob hash identical to HEAD's (cb3459eba4). Leg 1, `ahead_by === 0` dropped from commitIsContained: self-test EXIT 1, 1 of 87 failed, the case named `nor is a behind that still carries commits the branch lacks`. Leg 2, `pr.fetched === true` dropped from route B: self-test EXIT 1, 1 of 87 failed, the case named `an UNFETCHED base is never accepted as the default branch`. Both green again after restore. CI on the PR head 51bc005b9c: polled in the foreground to completion, 38 of 38 check runs complete, ZERO not-success -- including Lint and Repo Gates, Test Core, and Live half-state sweep, whose job shows the new step `Sweep the closed cards` as success on a real runner with the anchor write skipped (pull_request), and ZERO annotations on that check run, so the sweep exited 0 there: its self-test passed and its dry run judged every card. LIVE PROOF (dry run, read-only, no --write from this container): 78 closed cards listed in the 72h window, 76 candidates, 74 STRIP, 4 LEFT (2 not-completed: #15813 duplicate, #15279 not_planned; 2 closed by hand with no merged closing pull request: #15865, #14796), 0 unjudged, 157 requests. Sample of five with the closing evidence each rests on: #14552 closing PR #15962 commit f7db8f4fd2; #15835 PR #15940 commit 904e70780b; #15746 PR #15925 commit 0467941823; #15852 (pm:queue) PR #15914 commit f1e91595f7; #14946 PR #15912 commit ea03c7c16f -- all merged into main. Separately, `--all-time --max-cards=1` measured the archive stock the window excludes: at least 1907 closed cards carrying residue, 1837 of them completed, and the run announced that the pm:dispatched listing hit its page ceiling so that figure is a floor, not a count.",
      "mcp_calls": "2 - both reads of card #16005 (get, get_comments) at the start. Every other GitHub read and write went over REST from this container through the session proxy, which is reachable here.",
      "open_questions": [
        {
          "question": "The ruled population is every CLOSED card carrying residue; I bounded the ACTION SET to a 72h window (--since-hours) because the 2026-08-31 maintainer ruling recorded in check-half-states.mjs refused a backfill over the archived stock by name while keeping close-time hygiene. Measured today: the window is 78 cards, the stock is at least 1907. Which default does the seat want?",
          "options": [
            "A. Keep the 72h window as shipped: the patrol mechanizes close-time hygiene only, the archive stays an archive, and a seat who wants the stock measured runs --all-time in dry run. One constant, DEFAULT_WINDOW_HOURS, is the whole knob.",
            "B. Ship unbounded (--all-time in the workflow): the sweep also clears the stock, at roughly 2000 comments and several thousand label writes on archived cards, against a ruling that refused exactly that write.",
            "C. Keep the window but widen it once (a one-off dispatch run at a larger --since-hours) if the seat judges some recent tail is owed."
          ],
          "recommendation": "A, on all four axes. Real business need: the measured cost in the filing card is the per-landing hand round trip on FRESH cards, and 74 of the 78 in-window cards are exactly that population; nobody reads a closed card's labels, which is the ruling's own operative reason. Long-term soundness: the window is a stated boundary with a named constant, not a workaround, and it keeps one mechanism honouring both halves of one ruling. AI-error resistance: a bounded, announced action set is the contract-tight option -- an unbounded machine write onto 2000 archived cards is precisely the kind of bulk mistake nobody would notice card by card, and the sweep refuses rather than guesses on every card it cannot attribute. Startup-stage focus: B spends the fleet's quota and two thousand comments to buy tidiness the ruling says is not owed."
        },
        {
          "question": "The workflow's adopt list (its header) says a sibling repo copies THREE files. The new step adds a fourth dependency, gated off by repository name so a verbatim copy still works. Should the adopt list gain one sentence naming it?",
          "options": [
            "A. Leave it: the step is gated off for siblings, so a verbatim copy is correct today, and the script's own header carries the adoption note.",
            "B. Add one sentence to the adopt list in this PR (prose in the same file, beyond the ruled `one step`).",
            "C. File it as a follow-up card."
          ],
          "recommendation": "B if the seat is willing to take the extra sentence, since that comment's own text says the list must not be shortened from memory and an adopter who drops the gate would install a step with no script. I did not write it, because the ruling named exactly one step in that file and widening a governed-by-ruling diff on my own judgement is not mine to do."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-steve commented on Sep 5, 2026

    @os-steve
    Collaborator

    Follow-up owed, recorded by the skills seat (not blocking PR #16021): the half-state patrol workflow's adopt list (its header) names three files a sibling repo copies; the closed-card sweep adds a fourth, gated off by repository name so a verbatim copy still works today. One sentence naming the fourth file and the gate belongs in that list at the workflow's next touch — the objectui patrol-anchor flight (objectui#5986) is the natural carrier, since that is where the sibling copy happens.


    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

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions