Skip to content

Commit d00692f

Browse files
chore(pm): close-cards.mjs — one named script for the three-step card closure, and the allow rules that let seats run it (ruling 「B(治本)」) (#19479)
Fixes #19469 Clause-②: no ## The ruling this lands under, verbatim 「B(治本):给具名脚本加一条 Bash 允许规则进 .claude/settings.json(受管面,走 PR 您合),此后席位的批量关闭不再靠分类器掷硬币。分诊席的 #19292 加了 4 条规则但没覆盖这个形状。你负责派发」 A second ruled source touches the same file: batch #204 item 4, letter A (「204 同意」, recorded on #19362) authorises the objectui spellings of the two landing-endpoint allow rules. Those two entries are **not in this diff** — see *Scope addition, refused mid-round* below. #19362 is not addressed here and remains open. ## The measured defect Two seats, one wall, one day. A 90-card closing sweep spelled as a bash loop over `post-stamped.mjs` → `label-write.mjs` → `PATCH /issues/{n}` was refused by the session runtime's write classifier before any request. The second seat took the same order (#19458, execution log 5753693136): 90 of 90 cards passed its live gate, 77 were actionable, **one** closed, and then a batch script, an inline three-card loop and a *single* `post-stamped --comment=19243` were each refused — the identical command shape that had just succeeded twice. It stopped rather than grind a coin-flip channel across 76 three-step acts, because a comment that lands without its label write is a half-state on the board. Why the existing rules did not cover it: the two seat-write rules carry `--use-env-proxy` **inside the prefix**, and seats invoke `node scripts/pm/post-stamped.mjs …` (the tool re-execs itself with that flag), usually behind a `cd … &&` compound. An allow rule is a prefix match against the command *as typed*, so neither matched and every call fell to the classifier. No rule named a batch shape at all. ## What lands **1. `scripts/pm/close-cards.mjs`** — the three-step closure (comment · label · close) as one named command, ⛔ no new gate. Per card, re-read live first, then SKIP and log on: not open · has an assignee · carries `pm:retriage` · pm-state is not *exactly* the expected label (default `pm:queue`; no state, another state and two states all skip) · an open PR references it (`--skip-pr-referenced`, default on). Otherwise: post the comment, remove the state label, close with the `state_reason` — and read the close back, because a 200 whose body does not say `closed`, or that records another reason, is not the close that was asked for. Stamping and the four-step label write are **reused, never re-implemented**: post-stamped's write path is module-private (`writeArtefact`/`main`), so it is driven as a child process with its documented flags (`--repo=`, `--comment=N`, `--file=`, `--json`) and its exit code read before any pipe; its exported pure half (`renderBody`, `claimKeyedLineRefusals`) runs the comment pre-flight **once**, before card one, rather than ninety times. The label step calls label-write's exported `runLabelWrite` in-process with options built by label-write's own `parseOptions`. The pm-state vocabulary is imported from `check-half-states.mjs`. A card whose comment landed and whose label write or close did not is a HALF-WRITE: the run **stops at that card** and exits 4 naming it and exactly which of the three writes landed. It does not continue and it does not retry — continuing turns one half-state into a page of them. Exits: `0` every non-skipped card landed all three writes · `2` usage · `3` PREREQUISITE NOT MET · `4` HALF-WRITE, the card is named · `5` the platform refused a write. **Which PR reading** — `GET /repos/{o}/{r}/issues/{n}/timeline`, `cross-referenced` events whose `source.issue` carries a `pull_request` and whose `state` is `open`. That endpoint is the one `references/rest-channel.md` already declares reachable for cross-references. ⛔ Not `/search/issues` (the egress proxy refuses `/search/*` by design, so the default skip would be unavailable on exactly the seats this tool is for) and ⛔ not a `closed_by_pull_requests`-style signal, which answers "which PR would close this" — narrower than "an open PR references it", and it would pass a card an open PR merely mentions. **2. `.claude/settings.json`** — four `permissions.allow` entries: `Bash(node scripts/pm/close-cards.mjs *)`, `Bash(node --use-env-proxy scripts/pm/close-cards.mjs *)`, and the no-flag spellings of the two existing seat-write rules, `Bash(node scripts/pm/post-stamped.mjs *)` and `Bash(node scripts/pm/label-write.mjs *)`. `deny` is untouched; key order and formatting unchanged. **3. Usage** — in the script header, with the reason: invoke it **from the repo root with nothing in front of `node`**, no `cd … &&` compound, because the rule matches the command as typed. `references/rest-channel.md` gets **no** line: `pnpm check:pm-skill-ratchet` reports that file at 82 lines against a ceiling of 82 — headroom 0 — so the header carries it alone, exactly as the card's item 3 provides for. **Minimal registration**, stated as the card asks: `check:pm-close-cards` in the root `package.json` and one step in `lint.yml`, beside the identical pair for post-stamped and label-write. Without it the new self-test would ship unrun by CI, which is the state `check:self-test-wired` exists to prevent — it now counts 220 scripts and this one is in the population. ## A defect this found in its own first reading The first dry run over the 90 cards exposed a truncation in the script's own timeline read. Measured: of those 90 cards, **#13799 carries more than 100 timeline events**, so a single `?per_page=100` request returned a truncated history at HTTP 200 with nothing saying so — and a cross-reference on page 2 reads exactly like no cross-reference at all, i.e. the open-PR skip answering "no" for a card that has one. Fixed in the second commit: `readTimeline` walks by **page number** until a short page (the spelling `references/rest-channel.md` prescribes, cursor exhaustion having been measured on this platform to stop early), and a card still returning full pages at the 30-page cap **stops the run** rather than deciding on what it managed to read. The fake board pages for real, so the truncation case is driven rather than modelled. ## Verification **`--self-test`** — `node scripts/pm/close-cards.mjs --self-test`, exit 0: `OK close-cards self-test: 102 cases pass across 11 batteries — offline, no network, no token.` Battery roster, per-battery floor and the verdict handshake all copied from the landed shape in `label-write.mjs`. **Three ablations, each with the mutation proved on disk and the restore proved byte-identical** (`scripts/ablation-replace.mjs`, blob `a04f59d49673` before and after every leg, `git diff HEAD` empty): | leg | mutation | reading | |---|---|---| | A — a rule | delete the `pm:retriage` skip | blob `a04f59d49673` → `f455f723528c`; self-test RED, 1 of 102, naming that case | | B — the handshake | `return 0` before the verdict | blob → `44141e535b3c`; dispatch refuses, exit 1, "selfTest() returned without reaching its verdict" | | C — the floor | delete one assertion | blob → `d0d6a6169a2b`; RED with 0 case failures and 1 floor problem, naming the battery that fell 8 → 7 | **`--dry-run` over #19458's 90 numbers** (a READ; it wrote nothing, on any card), re-run at `901b26ea` after the pagination fix: ``` close-cards: DRY RUN — nothing will be written. objectstack-ai/objectstack · 90 card(s) · reason `not_planned` · expect-state `pm:queue` · open-PR skip ON #19408 SKIP an open PR references it (#19445) #19325 SKIP not open (state closed/not_planned) #19146 SKIP has an assignee (`os-steve`) — somebody owns it #19240 SKIP an open PR references it (#19335) close-cards: DRY RUN — nothing was written. 90 read · 86 actionable · 4 skipped ``` **86 actionable / 4 skipped**, against the card's 77/13 read at 00:01Z. The card provides for the move; the move is measured rather than assumed. Probing every cross-referenced PR on the 86: **11 of those cards had their referencing PR close after 2026-09-21T00:00Z** — #19440, #19404, #19396, #19395, #19390, #19343, #19336, #19319, #19309, #19179 and #18375, ten of them on PR #19456 alone, closed 00:34:28Z. Of the remaining two, #19325 is the one card the triage seat closed before it stopped, and #19146 has since gained an assignee. The matrices agree; the board moved. **Gates** — derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against the diff (68 commands, the list identical before and after the second commit), each exit code captured before any pipe. Green includes `check:pm-settings-deny-roster`, `check:pm-widening-tells` (and the real diff judged with `--declaration no --diff`: no tell, "no declared surface covers it (4)"), `check:pm-skill-ratchet`, `check:self-test-wired`, `check:self-test-workflow-commands`, `check:pm-dispatch-gates`, `check:nul-bytes` and `check:pm-close-cards`. **`pnpm lint` (`eslint . --no-inline-config`) is green over the whole repo at exit 0 — no narrowing, so no narrowing to justify.** Six derived families answer **PREREQUISITE NOT MET — a built tree is required** and are NOT MEASURED locally: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt` and the lint package's `check:doc-formula-expressions`. Each reads `dist/`; this diff contains no package source and produces no `dist/` byte, so it cannot move any of them, and CI runs them on a built tree. ⛔ Recorded as not measured, not as green. ## Scope addition, refused mid-round A mid-round scope addition asked for two further `permissions.allow` entries — the objectui spellings of the two landing-endpoint rules that already exist for objectstack (`.../objectui/pulls/*/ccr/ready_for_review` and `.../objectui/pulls/*/ccr/auto_merge`), placed after their objectstack twins. **They are not in this diff.** Both attempts to write them were refused by this session's own permission classifier with reason `[Self-Modification]` — once through a scripted edit, once through the editor tool — and a third route was not attempted. The working tree is clean and nothing partial landed. This is the same classifier, on the same file, that had permitted the four entries above earlier in the same round: a third observation of the non-determinism this card was filed for, now on a file surface rather than a write channel. A seat with a channel adds those two lines, or the maintainer adds them at merge. ## Tier and landing **Tier S by the register** (`GOVERNED_SURFACES` in `scripts/pm/check-governed-merges.mjs`): the diff touches `.claude/**`. Per the maintainer's directive above (「受管面,走 PR 您合」) **the maintainer merges this by hand**. ⛔ This PR is not flipped to ready, not queued, and auto-merge is not armed. `Check Changeset` wants `skip-changeset`: no released package is touched. The four paths are `.claude/settings.json`, `scripts/pm/close-cards.mjs`, `.github/workflows/lint.yml` and the root `package.json` (private, `@objectstack/spec-monorepo`, a `scripts` entry only) — every one of them on the non-publishing fast track. The label is the seat's to apply. ## Acceptance notes Noted, not filed: - `references/rest-channel.md` has **headroom 0** (82 lines, ceiling 82), and so does every other ceilinged file in that ratchet. The channel table therefore cannot gain a row for this script without a ruled raise or an equal deletion. Carrier: the next PR that raises that ceiling. Observation, not a defect. - `--dry-run` buys one card read per card and a timeline walk only for a card that would otherwise be acted on. A future batch larger than this one may want a `--json` summary for the completion comment; nothing needs it today. Carrier: none. ## 维护者速读(草稿) **改了什么** — 新增一个具名脚本 `scripts/pm/close-cards.mjs`,把「评论 · 摘标签 · 关卡」这三步合成一条可被允许规则整条命中的命令;并在 `.claude/settings.json` 的 `permissions.allow` 里加了 4 条规则(这个脚本两种拼写,加上两个既有工具的无 flag 拼写)。`deny` 一个字没动。 **为什么改** — 批量关卡以前是 shell 循环,每一步都由会话的写分类器逐条判,判得不稳:同一条命令刚成功两次就被拒,90 张卡关到第 1 张就停了。停是对的——评论落了标签没落就是半状态——但代价是这批清理走不动。一条具名脚本 = 一条前缀,分类器不再掷硬币。 **风险与代价(含回滚)** — 风险最集中的一点是「半写」:脚本在第一张半写的卡上立刻停,退出码 4,并点名是哪张卡、哪几步落了,⛔ 不继续、⛔ 不重试。回滚代价为零:删掉这个文件和那 4 行规则即回到今天,没有任何其它代码读它。另一项要请您留意的是,允许规则本身是放宽面——它放宽的是「跑本仓自己的三个 PM 脚本」,不是任何网络写端点。本轮还有 2 条 objectui 的规则被会话分类器当场拒写(见上节),不在这个 diff 里。 **席位意见** — (留空,待席位复审填写) **你要做的** — 读一眼那 4 行允许规则是不是您想给的面,然后手工合。⛔ 本 PR 不翻 ready、不入队、不挂 auto-merge。合完之后,这批 90 张卡的关闭由分诊席跑一次 `--dry-run` 再跑一次实关,日志回贴 #19458。 --- _Generated by [Claude Code](https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8015dc8 commit d00692f

4 files changed

Lines changed: 1173 additions & 0 deletions

File tree

.claude/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
"Bash(curl -sS -X PUT https://api.github.com/repos/objectstack-ai/objectstack/pulls/*/ccr/auto_merge *)",
5353
"Bash(node --use-env-proxy scripts/pm/post-stamped.mjs *)",
5454
"Bash(node --use-env-proxy scripts/pm/label-write.mjs *)",
55+
"Bash(node scripts/pm/post-stamped.mjs *)",
56+
"Bash(node scripts/pm/label-write.mjs *)",
57+
"Bash(node scripts/pm/close-cards.mjs *)",
58+
"Bash(node --use-env-proxy scripts/pm/close-cards.mjs *)",
5559
"Bash(curl -sS -X PATCH https://api.github.com/repos/objectstack-ai/objectstack/issues/* *)",
5660
"Bash(curl -sS -X PATCH https://api.github.com/repos/objectstack-ai/objectui/issues/* *)"
5761
],

.github/workflows/lint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,29 @@ jobs:
13071307
- name: Label-write helper self-test
13081308
run: pnpm check:pm-label-write
13091309

1310+
# Card-closure helper self-test (#19469) — same family and the same split
1311+
# as the two steps above, and it drives BOTH of them: `close-cards.mjs` is
1312+
# the three-step closure (comment · label · close) as one named command,
1313+
# so a seat's batch closure is one allow-listed prefix instead of a loop
1314+
# the session's write classifier judges act by act. The LIVE path posts
1315+
# comments, strips labels and closes cards, so it is emphatically not a
1316+
# thing CI runs against the board; the offline half is all CI can hold.
1317+
#
1318+
# What the self-test instruments is the set of rules a clean tree cannot
1319+
# exercise, every one of which is silent when it breaks: the skip matrix
1320+
# (not open · assigned · `pm:retriage` · a pm-state that is not EXACTLY
1321+
# the expected one · an open PR cross-referencing the card); that an
1322+
# unreadable card or timeline STOPS the run rather than being read as a
1323+
# "no"; that a 200 whose body does not read back `closed`, or reads back
1324+
# another `state_reason`, is not a close; and — the one this tool exists
1325+
# for — that a comment which lands whose label write then fails STOPS at
1326+
# that card, naming it and which of the three writes landed, instead of
1327+
# turning one half-state into a page of them. Weakening any of them leaves
1328+
# every ordinary invocation just as green, which is the shape
1329+
# `check:self-test-wired` requires this step for.
1330+
- name: Card-closure helper self-test
1331+
run: pnpm check:pm-close-cards
1332+
13101333
# Board-snapshot archiver self-test (#17390) — same family and the same
13111334
# split as every step around it. The LIVE run reads the whole board over
13121335
# REST and commits the result to an orphan branch, so it is not a thing

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"check:pm-stale-finding": "node scripts/pm/sweep-stale-finding.mjs --self-test",
8989
"check:pm-post-stamped": "node scripts/pm/post-stamped.mjs --self-test",
9090
"check:pm-label-write": "node scripts/pm/label-write.mjs --self-test",
91+
"check:pm-close-cards": "node scripts/pm/close-cards.mjs --self-test",
9192
"check:pm-board-snapshot": "node scripts/pm/board-snapshot.mjs --self-test",
9293
"check:pm-changeset-deadline-census": "node scripts/pm/changeset-deadline-census.mjs --self-test",
9394
"check:pm-governed-merges": "node scripts/pm/check-governed-merges.mjs --self-test",

0 commit comments

Comments
 (0)