Skip to content

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

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-19469-close-cards-script-allow-rule
Sep 21, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-19469-close-cards-script-allow-rule

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

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.mjslabel-write.mjsPATCH /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 readingGET /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-testnode 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 a04f59d49673f455f723528c; 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.jsonpermissions.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

… closure

The three-step closure (comment · label · close) had no named spelling, so a
seat's batch ran as a bash loop and the session's write classifier judged it
act by act — non-deterministically, which is how a 90-card sweep stopped after
one card rather than risk a comment landing without its label write.

`scripts/pm/close-cards.mjs` is that spelling: it re-reads every card live,
applies the skip matrix, drives post-stamped (child process, its documented
flags) and label-write's exported `runLabelWrite`, closes with the reason and
reads the answer back, and STOPS at the first card left half-written, naming it
and which of the three writes landed. Stamping and the four-step label write are
imported, never re-implemented.

`.claude/settings.json` gains the four allow rules that let a seat run it and
its two helpers as typed, from the repo root.

Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>
Measured while dry-running the first batch this script was written for: of
those 90 cards, #13799 carries more than 100 timeline events, so the single
`?per_page=100` read returned a truncated history at HTTP 200 with nothing
saying so — and a cross-reference on page 2 read exactly like no cross-reference
at all, which is the open-PR skip answering "no" for a card that has one.

`readTimeline` now pages by NUMBER until a short page (the spelling the channel
table prescribes, cursor exhaustion having been measured 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.

Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 901b26ea78064da4a162a9cefd3b929631be4e83

Isolated at-tier reviewer; reviewed 2026-09-21T02:45Z. Merge-base fbc12be318de0713e82e1f38ab804b5b63478e64; origin/main read 841ed38947850738cf6ac12388abbd7029a2a7b9 at the first fetch and 5e7d83c90849d3c4dbf5a7eb66173f709090d4a6 at the last (PR #19468 landed mid-review; merge-base and diff surface unchanged); judged in the detached worktree ../objectstack-review-19479 pinned at the head, pnpm install --frozen-lockfile exit 0, removed at the end.

① Derived judgments

  1. Surface. git diff origin/main...901b26ea --name-only is exactly the PR's four files — .claude/settings.json, .github/workflows/lint.yml, package.json, scripts/pm/close-cards.mjs — and nothing under packages/** (identical against both origin/main readings). .claude/settings.json: the raw diff is one hunk of four added lines and zero removed; parsed, permissions.allow goes 53 → 57 with exactly these additions, verbatim — Bash(node scripts/pm/post-stamped.mjs *), Bash(node scripts/pm/label-write.mjs *), Bash(node scripts/pm/close-cards.mjs *), Bash(node --use-env-proxy scripts/pm/close-cards.mjs *) — and no removals, the 53 pre-existing entries in their original order; deny is byte-identical (raw block sha256 prefix 44df56999cd59bb3 on both sides, 17 entries); the parsed JSON minus allow is identical including key order (permissions, hooks; allow, deny) and the hooks block. RIGHT.

  2. Skip matrix. skipReason (pure) reads in the card's order: state not open → any assignee (assignees union assignee.login) → pm:retriage present → pm-state count against the IMPORTED PM_EXCLUSIVE_STATE_LABELS (0 states skips, 2 or more skips, one that is not --expect-state skips, quoting PM_STATE_CLAIM) → any open PR reference; the first reason wins. --expect-state is validated against the imported vocabulary at parse time (a typo is refused, never matched against nothing). The 14-case battery the skip matrix pins each row, including the no-state, two-states, other-state and first-reason-wins cases. node scripts/pm/close-cards.mjs --self-test: exit 0, 102 cases pass across 11 batteries, file blob a04f59d49673. Ablation A in my worktree — delete the assignee-skip line: blob → c69ca691954b; self-test exit 1, 4 of 102, naming an assigned card is skipped and the reason names who (plus the two dry-run count cases that ride on it); restored by copy, blob a04f59d49673, git diff HEAD --quiet exit 0, git status --porcelain empty, self-test exit 0 again. Ablation B — make the label-failure half-write path continue instead of returning exit 4: blob → 09daf822bd47; self-test exit 1, 3 of 102, naming a comment that lands whose label write fails is a HALF-WRITE, the run STOPS — the second card is never touched, and the second card is still open; restored the same way, git diff HEAD --quiet exit 0, self-test exit 0. RIGHT.

  3. Reuse, not reimplementation. The comment step is spawnSync(process.execPath, [proxy flag when routed, post-stamped.mjs, --repo=, --comment=N, --file=, --json]); the exit is child.status read off the spawnSync result before anything touches stdout, and post-stamped's imported EXIT_NOT_STORED (4) is routed to HALF-WRITE while every other non-zero exit is "refused before the write". The label step builds options with label-write's own parseOptions([--repo, --issue, --remove expectState]) and calls its exported runLabelWrite(options, { log }) in-process — the four-step with ④ read-back lives there, not here. PM_EXCLUSIVE_STATE_LABELS, PM_STATE_CLAIM, classifyHttp, proxyRearmPlan/proxyRoute, resolveSweepRepo are all imports. The new file's only network acts are the card GET, the timeline GET walk and the close PATCH; a grep finds no comment POST, no /labels write and no token substitution of its own — the pre-flight calls post-stamped's exported renderBody and claimKeyedLineRefusals once before card one. RIGHT.

  4. Half-write contract. Label write non-zero → halfWriteText naming the card with LANDED: comment and OUTSTANDING: label, close, then return result(EXIT_HALF_WRITE); close PATCH non-ok, or a 200 whose body reads state other than closed or state_reason other than the requested reason → the same refusal with LANDED: comment, label, return result(EXIT_HALF_WRITE); post-stamped exit 4 → HALF-WRITE with comment landed. No retry anywhere in the run loop, no continue on any of those paths. The 12-case battery the half-write refusal pins: a stubbed label exit 4 on card 21 of two → exit 4, #21 named, both LANDED and OUTSTANDING lines matched, only one comment posted, card 22 still open on the fake board; a 403 on the close → exit 4 naming both landed steps; a 200 with state: open → exit 4; a 200 with state_reason: completed against a not_planned request → exit 4; and a comment refused with exit 2 is exit 5, not a half-write, with no label write attempted. Ablation B above proves the stop is load-bearing. RIGHT.

  5. Open-PR reading. openPrReferences counts a timeline event only when event is cross-referenced, source.issue.pull_request is present and source.issue.state is open; readTimeline walks /issues/{n}/timeline?per_page=100&page=N for N from 1 to TIMELINE_PAGE_CAP (30), stopping on the first page shorter than the page size, classifying each page with the imported classifyHttp, and returning not-exhausted when page 30 is still full — which the run reports as TIMELINE NOT EXHAUSTED and STOPS on (exit 3), never deciding on the pages it read. This matches references/rest-channel.md (line 20 prescribes the page-number walk to a short page; line 27 declares the timeline endpoint reachable for cross-refs). The fake board slices its timeline per page, so the 8-case battery drives it for real: a short first page ends in one request, a full page is followed by page 2 and a cross-reference there is seen, a 502 mid-walk is a transport refusal, a cap of 4 at page size 1 over 50 events is not-exhausted at 4 pages, and a driven run over a card whose only open-PR reference sits on page 2 is SKIPPED. My own paginated read of 155 of 158 scripts/** self-tests have no assertion floor: a battery that never ran is indistinguishable from one that passed #13799: 304 events over 4 pages (100, 100, 100, 4), 101 cross-references, so above 100 as the PR body says; the dry run below read it correctly — it skipped 155 of 158 scripts/** self-tests have no assertion floor: a battery that never ran is indistinguishable from one that passed #13799 on an open PR reference found on page 4. RIGHT.

  6. Dry run. node scripts/pm/close-cards.mjs --repo objectstack-ai/objectstack --list [the 90 numbers parsed from #19458's p3 and p2 lists] --comment [#19458's fenced closing template with the UTC placeholder replaced by the token] --reason not_planned --dry-run: exit 0, nothing written (74 WOULD CLOSE lines, 0 PATCH); summary 90 read · 74 actionable · 16 skipped. Skips: #19325 not open (closed/not_planned); #19146 has an assignee (os-steve); #19408 open PR docs(skills): pages.md rule 2 states the enforceable reason, not the retired ADR-0048 claim #19445; #19240 open PR feat(spec)!: publish the $-prefix key ban the normalized filter enforces, and make the ratchet able to see it #19335 — those are the PR's four — plus twelve more, every one an open PR references it (#19479): [finding] check-widening-tells T4 把 ERROR_CODE_LEDGER 的出处行读成扩面 —— 并集实测 282→282、added/removed 皆空,却恒逼出假的 Clause-②: yes #19440 [finding] a job log refused over REST is still readable through the MCP read tool — a seat mis-read one blocked channel as no channel and mis-diagnosed a queue ejection #19404 [finding] the half-state patrol action keeps setup-node in the caller for a reason PR #19284 falsified — the censuses read composite actions now #19396 [finding] dispatch-gates derives ZERO families for a run: step whose script path is variable-carried — and PR #19225 removes the class's only live specimen #19395 platform-readings records the body-PATCH footer behaviour by ROUTE; measured, it follows the SURFACE — the issues route on a PR doubles the footer #19390 removed_from_merge_queue fires on a SUCCESSFUL merge too — 3 of 5 on this board today were merges, so the event is not a failure signal #19343 [finding] the REST proxy appends its footer to EVERY PR-body PATCH, so a body that already carries one comes back with two — the remedy is to PATCH the body with the footer STRIPPED #19336 post-stamped refuses a {{WAS:}}-declared stamp inside a fenced code block, and its refusal prescribes the declaration that is already there #19319 [finding] the hand-over rule 「Served-tier: takes a bare N/M」 is WRONG and would be refused — the checker requires the constant's NAME, with N/M only as an optional prefix #19309 finding(platform-readings): objectui's labeler runs with sync-labels: true, so a hand-applied PR label is silently removed on the next push — and the four-step read-back happens inside the window where it still looks fine #19179 [finding] the skip-changeset label suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375 155 of 158 scripts/** self-tests have no assertion floor: a battery that never ran is indistinguishable from one that passed #13799. Explanation, measured: this PR's own body names exactly those twelve cards (the eleven whose referencing PR had closed, and 155 of 158 scripts/** self-tests have no assertion floor: a battery that never ran is indistinguishable from one that passed #13799), and opening the PR wrote a cross-referenced event from an open PR onto each — [finding] check-widening-tells T4 把 ERROR_CODE_LEDGER 的出处行读成扩面 —— 并集实测 282→282、added/removed 皆空,却恒逼出假的 Clause-②: yes #19440's timeline reads PR fix(scripts/pm): the read-back names the collapse OVER the strip instead of calling the write lost #19456 closed and PR 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 open, event created 2026-09-21T02:14Z, after the body's 86/4 reading was taken. Against the triage seat's 77/13 at 2026-09-21T00:01Z: the 13 open-PR holds collapsing to 2 is consistent with PR fix(scripts/pm): the read-back names the collapse OVER the strip instead of calling the write lost #19456 reading closed on the cards' timelines, pulls/{n}.base.sha is the base branch tip, NOT the merge base — reading it as one made a moved base look unmoved and cost an at-tier review #19325 is the one card that seat closed, skills: re-key the three 事实层 = references/ spellings the Tier S ruling leaves false (os-dev.md :286–:287, landing-operations.md :27–:28, H48) #19146 has since gained an assignee. The script's reading is literally true; the board moved, and this PR is the mover. RIGHT — with the practical note in ③.

  7. Registration and gates. check:pm-close-cards sits in the root package.json between check:pm-label-write and check:pm-board-snapshot; the lint.yml step Card-closure helper self-test follows Label-write helper self-test directly — one script entry and one step, nothing else. Local, in the worktree, exit codes captured before any pipe: node scripts/check-self-test-wired.mjs 0 (220 scripts in CI's population, this one among them); node scripts/check-self-test-workflow-commands.mjs 0; pnpm check:pm-settings-deny-roster 0 (17 declared = 17 enforced); pnpm check:pm-widening-tells 0 (525 cases); node scripts/pm/check-widening-tells.mjs --declaration no --diff [the PR's three-dot diff, 70329 bytes] 0 — no declared surface covers it (4), the PR's reading; pnpm check:pm-skill-ratchet 0 (rest-channel.md 82 lines against ceiling 82, headroom 0, untouched by the diff); pnpm check:pm-close-cards 0; pnpm check:pm-post-stamped 0 (544 cases); pnpm check:pm-label-write 0 (75 cases); pnpm lint (eslint . --no-inline-config, repo-wide) 0. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: exit 0, 68 commands derived (the PR's count), with a STALE TREE note because origin/main advanced 3 commits during the review — the one derivation file that moved, scripts/check-published-files.mjs, is not on this diff. All 68 run: 62 exit 0 and 6 exit 3, the six being exactly the dist/-reading families the PR names — 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 answering PREREQUISITE NOT MET on the unbuilt tree; recorded as NOT MEASURED locally, not as green, and the whole tree was not built for them (this diff writes no package source and no dist/ byte). No other family is non-zero. CI on the head, paginated check-runs read at posting: all seven required contexts completed/successLint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard — and Check Changeset success (its path-filtered twin skipped), 39 check-runs, none in_progress, none failed; the seat applied skip-changeset. RIGHT.

② Semver level

No released package is touched: the four paths are .claude/settings.json, .github/workflows/lint.yml, the root package.json (private @objectstack/spec-monorepo, one scripts entry) and scripts/pm/close-cards.mjs. skip-changeset is the right disposition, and Check Changeset reads success on the head. Nothing to version.

③ Boundary flags

  • Scope addition, refused mid-round. The two objectui landing-endpoint rules (.../objectui/pulls/*/ccr/ready_for_review, .../objectui/pulls/*/ccr/auto_merge) are absent from the head's allow — a grep for objectui/pulls finds only the four pre-existing read/PATCH rules on both main and head. Their absence does not block this PR: they are ruled item [Decision] PM 席位被权限分类器判 Self-Approval 拦住落地动作 —— 章程要求席位入队,通道却不允许(三张全绿 PR 今天为此停摆) #19362, a separate card, and the card this PR closes never named them.
  • Acceptance notes. rest-channel.md headroom 0 (82 of 82) confirmed by the ratchet; the header carries the usage line alone, as the card's item 3 allows. The future --json summary note has no carrier and needs none today.
  • Outside the claim's declared surface. .github/workflows/lint.yml (+23, one step and its comment) and the root package.json (+1) are the registration of the new self-test — the wiring check:self-test-wired requires and the card's item 1 allows ("no new gate" names a gate, and a self-test wired into the existing job is not one). Nothing else is touched.
  • A practical note for the adopting seat, not a defect. While PR 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 is open, its body's enumeration of twelve card numbers makes the script skip those twelve as PR-referenced (judgment 6). Once this PR merges its state reads closed and they return to actionable — so the triage seat's --dry-run after the merge, not the 86/4 in this body, is the number to act on. A PR body that lists card numbers holds them for the duration.
  • Landing. Tier S by the register (.claude/**); the PR is draft, auto-merge unarmed, Governed Surface Queue Guard success; per the directive the maintainer lands it by hand. This record is the review of record for the head named above and nothing more.

Implemented-by: claude/issue-19469-close-cards-script-allow-rule
Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — director seat, summon #25 (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-21T02:46Z: contract review of record 5754723636, Served-tier: CONTRACT_REVIEW_TIER, judged head 901b26ea78064da4a162a9cefd3b929631be4e83 (the current head), VERDICT: PASS — seven ① judgments RIGHT; the reviewer's transcript re-read by this seat: every request stamped at the tier constant, zero fallbacks. Independence pair Implemented-by: claude/issue-19469-close-cards-script-allow-rule / Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE (director's mode:subagent dev; isolated reviewer subagent; verdict adopted verbatim). No carrier label was hung (Clause-②: no), so nothing is stripped. The maintainer flipped this PR ready and armed auto-merge in person; it is in the queue.

One operational reading from the review, for whoever runs the closure over #19458 after this lands: the review's dry run reads 74 actionable / 16 skipped against the PR's 86 / 4 because this PR's own body enumerates twelve of the card numbers, so opening it cross-referenced them; once the PR is closed (merged) those twelve return to actionable. ⇒ re-run --dry-run after the merge and act on that count, ⛔ not on 86 / 4.


Generated by Claude Code

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

3 participants