Repository navigation
fix(pm): a Blocked-by PR target is released on merge in every unlock path, and H19 holds an abandoned PR - #27983
Conversation
The issues API answers a PR number `closed` whether it merged or was abandoned, and only `pull_request.merged_at` tells the two apart. The H19 resolver read `state` alone, so a block parked on a PR that closed unmerged was reported as outlived: an unlock candidate whose fix never landed. The resolver's projection is now `blockerResolutionFromRow`, which marks a closed PR with no `merged_at` as `unmerged: true` (the field the scheduled full-run refresh already judges). Such a target founds no row and, carried by the judged set, withholds a full discharge; the row names it as closed UNMERGED. A new self-test battery drives the measured API shapes through the projection: merged -> outlived, closed unmerged -> held, open -> held. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwiU8WPKdGvRr5uhB6
…by channel rule - state-machine.md: a card waiting on a PR names the PR itself and is released when it merges, never when it closes unmerged; the full-run refresh sentence names the same merged condition. - dispatch-runbook.md: the hourly unlock scan resolves each pm:blocked card's body targets forward (issues closed, PRs merged), the scan the triage seat runs; a closed-since listing joined with the index never sees a PR target. - platform-readings.md: a Blocked-by line is written in the body, except on the full-run card, whose body is rewritten on every red; the patrol and the full-run refresh read both channels; the unlock scan reads a PR target's merge from merged_at. Line-neutral in all three files. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwiU8WPKdGvRr5uhB6
…y in the body The lane note said the repo deliberately keeps Blocked-by lines in comments, which contradicts the channel rule now in platform-readings.md (the line is written in the body; only the full-run card writes a comment). Line-neutral. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwiU8WPKdGvRr5uhB6
|
domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS Contract-review-claim: session_016WV4d1QkPVVzNz3FJqb2iS · head ffe534a · 2026-10-30T06:21Z |
|
domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS Contract reviewServed-tier: ① Derived judgmentsBase: merge base
② Semver levelNo published package is touched ( ③ Boundary flags
④ Design judgmentRules face, per
No file in Implemented-by: VERDICT: PASS Must-fix: none. Should-fix:
|
|
domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS 维护者速读
席位意见:ACCEPT。契约复核 PASS,没有 must-fix。四条 should-fix 都转到跟进卡 #27989:H4 散文子句、SKILL.md 状态行、 席位核对
|
Requested by Elon · project thread
domain:skills#2 · os-elon-musk · session_016WV4cwiU8WPKdGvRr5uhB6
Fixes #27925
Clause-②: no
Metadata-protocol: none
维护者速读
Blocked-by: #PR),现在规则与巡查都按「已合并」放卡。state-machine.md改写 PR 等待的写法和整点全量红卡刷新的放回条件;dispatch-runbook.md的小时解锁扫描改为逐张pm:blocked卡正向读正文目标(issue 已关、PR 已合并才命中),与分诊现跑的扫描一致;platform-readings.md定下一条通道规则(Blocked-by:写正文,只有整点全量红卡写评论),lanes/devx.md一句随之对齐。半状态巡查 H19 不再把「关闭未合并」的 PR 读成阻塞已过期。scripts/pm/check-half-states.mjs(只读巡查,不写标签),不发布任何包。H19 只多一种「不报过期」,不会多报。回滚即 revert。scripts/pm/check-half-states.mjs的blockerResolutionFromRow与h19BlockOutlivedBlocker;state-machine.md第 24、31 行;platform-readings.md第 250-252 行。node scripts/pm/check-half-states.mjs --self-test,预期末行报全部用例通过,电池清单里有H19 PR target merge reading 18/16。node scripts/pm/check-governed-merges.mjs --pr本 PR 号)。What changes
H19 reads a PR target's merge, not its close. The issues API answers a PR number
closedwhether it merged or was abandoned; onlypull_request.merged_attells them apart. Measured 2026-10-30 withGET /issues/N: #27888closed,merged_atset; #27296closed,merged_at: null; #27846 (an issue) has nopull_requestkey.resolveBlockerTargetkeptstateonly, so H19 published a block on an abandoned PR as outlived, an unlock candidate whose fix never landed.blockerResolutionFromRow(new, exported): the resolver's projection of one/issues/Nanswer. A closed PR with nomerged_atkeepsstate: 'closed'and carriesunmerged: true, the fieldscheduled-full-run.mjsalready judges. A missingmerged_aton a PR reads as unmerged, the direction that cannot release a card. Issue rows are byte-identical to before.h19BlockOutlivedBlocker: an unmerged target founds no row. Carried by the judged set it withholds a full discharge (PARTIAL), is counted in the denominator, and is namedclosed UNMERGEDon whichever row fires. Outside the judged set (archive-only beside a body line) it is the ordinary superseded clause, like any other spent close.namedTargetsrendersclosed UNMERGEDfor such a row; every other row renders as before.Rule text: one form, one channel. All four files stay at their line count (ceilings untouched), every changed line is at most 120 bytes.
state-machine.md:24: a card waiting on a PR writes the PR number, released on merge, never on a close without merge; the release-wait half now points at:25, which names that target.state-machine.md:31: the full-run refresh releases only when every target is a closed local card, a PR one merged.dispatch-runbook.md:66: the hourly unlock scan reads eachpm:blockedcard's body targets forward (issues closed, PRs merged), which is what the triage seat'sunlockscan does (triage 6515340724). The old reverse join (state=CLOSED+sinceagainst the index) is gone.platform-readings.md:250-252: aBlocked-by:line is written in the body, except on the full-run card (its body is rewritten on every red), matching thetriage-duties.mdline PR fix(ci): a pm:blocked card parked on a PR is released only once that PR merged #27924 added. Old comment-borne lines are still backfilled incrementally; the half-state patrol and the full-run refresh read both channels. The unlock scan lists cards, reads each target directly, adds no standing comment read, and reads a PR's merge frommerged_at.lanes/devx.md:26: the lane keeps rulings and corrections in comments,Blocked-by:in the body (seat go, see Acceptance notes).Why
After the previous card a card may park on a PR number, but
state-machine.mdstill pointed a PR wait at the card the PR fixes, the reverse unlock scan could never see a PR target, and H19 read an abandoned PR as a released block. A card parked on a PR could strand again, or be released by a PR that never landed. Option A of the triage ruling: one body form that every unlock path reads, PR targets released on merged. No new gate.Tests
node scripts/pm/check-half-states.mjs --self-test: new batteryH19 PR target merge reading, 18 cases, pin 16. The measured API shapes go through the sweep's own projection, so the reading and the judgement are pinned together:closed, unmarked; PR closed unmerged →closed+unmerged; PR with nomerged_atkey →unmerged; open PR →open; issue → judged by state alone; close time and labels kept.closed UNMERGED.Acceptance notes
lanes/devx.md:26(reported to the seat first, seat go). Before: 「卡片正文冻结在立卡那一刻,而本仓刻意把裁定、更正与Blocked-by:放在评论里。」 After: 「卡片正文冻结在立卡那一刻,而本仓刻意把裁定与更正放在评论里(Blocked-by:归正文)。」state-machine.md:24. The old line covered two waits in one clause (a PR merge, and a release with a pin), both pointing at a card that closes at that moment. The PR half now names the PR itself; the release half reads 「等发布见下行」, since:25already names that target (the pin-bump chore card). The qualifier 「且 pin 覆盖」 is carried by that target, not restated, to stay within the line's 120 bytes.proseBlockerClause(H4's prose half) still reads a prose-named PR that closed unmerged as 「ALREADY CLOSED」. The resolution now carriesunmerged: true, so honouring it is one filter. Report-only and outside the H19 ruling.pm:on-hold/needs-user-decision. Triage ruled no new gate; the seat may route it.state-machine.mdcarries the merged condition for a PR; SKILL.md is outside this claim and on its own ratchet row./issues?state=closed&since=2026-10-30T02:30:00Zreturns PRs (78 rows, 40 PRs, fix(rest): a refused xlsx export answers the 400 envelope, not a 200 workbook stream #27888, feat(studio): the organization-flows publish toast names the flows that went live #27882 and feat(studio): Add view in the Interfaces list-view panel creates a kanban, calendar or other list view #27870 included); MCPlist_issueswithstate: CLOSEDand the samesincereturns 38 rows and no PR. The gap is the MCP listing. Either listing still needsmerged_atto tell a merge from an abandonment, so the rule names the forward per-card read.claude/project-thread-3r8sm7names no issue; claim 6515710355 (amended) is the identity record.Verification
unmergedforcedfalse): self-test red, 9 of 5084 cases, all in the new battery (e.g.H19 PR: CLOSED UNMERGED -> held: no rowgot the full "outlived its blocker" row). Restored: blob == HEAD8c1ace852181,git diff HEADempty.scripts/ablation-replace.mjs(anchor hit 1 → 0, marker 0 → 1), on committed head1b32a159.ffe534a1(merge basebacb3e6a0):node scripts/pm/dispatch-gates.mjs --commandsderived 56 families (re-derived after the last commit: identical). Afterpnpm install --frozen-lockfile --ignore-scriptsandpnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint(forcheck:doc-formula-expressions), each ran with its exit code recorded before any pipe: all 56 exited 0.--ran: 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN. This includescheck:pm-half-states,check:pm-skill-ratchet(all four rule files at their ceilings: 41, 245, 458, 35/36),check:pm-skill-id-lint, and the self-tests of the scripts that import this file (scheduled-full-run.mjs,board-snapshot.mjs,check-governed-queue-guard.mjs).pnpm lintand the type-check lanes. This diff touches no TypeScript.check-governed-merges.mjs --teston the five paths answers Tier S (.claude/**×4;scripts/pm/check-half-states.mjsnot on the register), head repo and size not measured by that mode; the seat reads--pr.dispatch-gates --tier: no path-derived mandate; 138 changed lines (+119 / -19), under 3000.Human Minimum
packages/specholds no runtime logic. (not touched)--no-inline-config) onscripts/pm/check-half-states.mjsexits 0, and CI'spnpm lintis cited from the check-runs.main, one pull request.skip-changeset)packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cwiU8WPKdGvRr5uhB6