Skip to content

fix(pm): a Blocked-by PR target is released on merge in every unlock path, and H19 holds an abandoned PR - #27983

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3r8sm7
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3r8sm7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:skills#2 · os-elon-musk · session_016WV4cwiU8WPKdGvRr5uhB6

Fixes #27925

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:卡可以直接停在一个 PR 号上(Blocked-by: #PR),现在规则与巡查都按「已合并」放卡。state-machine.md 改写 PR 等待的写法和整点全量红卡刷新的放回条件;dispatch-runbook.md 的小时解锁扫描改为逐张 pm:blocked 卡正向读正文目标(issue 已关、PR 已合并才命中),与分诊现跑的扫描一致;platform-readings.md 定下一条通道规则(Blocked-by: 写正文,只有整点全量红卡写评论),lanes/devx.md 一句随之对齐。半状态巡查 H19 不再把「关闭未合并」的 PR 读成阻塞已过期。
  • 为什么改:上一张卡允许停在 PR 号上之后,旧规则仍把 PR 等待指向它修的卡;旧的反向扫描所用的关闭列表列不出 PR(实测 MCP 列表 38 行、0 个 PR);H19 把弃置的 PR 读成「已关」并列为放回候选。卡可能再被困住,或被一个从未落地的 PR 放出。
  • 风险与回滚:只动内部 PM 规则文本和 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。
  • 需要你做的:契约复审(contract review)尚欠,由席位达档复审;落地档位以登记册为准(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 closed whether it merged or was abandoned; only pull_request.merged_at tells them apart. Measured 2026-10-30 with GET /issues/N: #27888 closed, merged_at set; #27296 closed, merged_at: null; #27846 (an issue) has no pull_request key. resolveBlockerTarget kept state only, 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/N answer. A closed PR with no merged_at keeps state: 'closed' and carries unmerged: true, the field scheduled-full-run.mjs already judges. A missing merged_at on 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 named closed UNMERGED on whichever row fires. Outside the judged set (archive-only beside a body line) it is the ordinary superseded clause, like any other spent close.
  • namedTargets renders closed UNMERGED for 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 each pm:blocked card's body targets forward (issues closed, PRs merged), which is what the triage seat's unlock scan does (triage 6515340724). The old reverse join (state=CLOSED + since against the index) is gone.
  • platform-readings.md:250-252: a Blocked-by: line is written in the body, except on the full-run card (its body is rewritten on every red), matching the triage-duties.md line 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 from merged_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.md still 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 battery H19 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:

  • projection: merged PR → closed, unmarked; PR closed unmerged → closed + unmerged; PR with no merged_at key → unmerged; open PR → open; issue → judged by state alone; close time and labels kept.
  • H19: MERGED → outlived (full discharge); CLOSED UNMERGED → held, no row; OPEN → held, no row.
  • a closed issue beside an abandoned PR → fires on the issue only, 1 of 2, PARTIAL, never "every target is closed", the PR named closed UNMERGED.
  • the UNJUDGED row names a judged abandoned PR; an archive-only abandoned PR is the superseded clause, not a withhold; an ordinary closed target renders byte-identically.

Acceptance notes

  • Declared deviation, lanes/devx.md:26 (reported to the seat first, seat go). Before: 「卡片正文冻结在立卡那一刻,而本仓刻意把裁定、更正与 Blocked-by: 放在评论里。」 After: 「卡片正文冻结在立卡那一刻,而本仓刻意把裁定与更正放在评论里(Blocked-by: 归正文)。」
  • Declared deviation, 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 :25 already 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.
  • Noted, not filed: proseBlockerClause (H4's prose half) still reads a prose-named PR that closed unmerged as 「ALREADY CLOSED」. The resolution now carries unmerged: true, so honouring it is one filter. Report-only and outside the H19 ruling.
  • Noted, not filed: a block whose only target is an abandoned PR is now silent in H19 (held), and no merge will ever come to release it. H26 asks "can it ever close" only of open targets parked in pm:on-hold / needs-user-decision. Triage ruled no new gate; the seat may route it.
  • Noted, not filed: SKILL.md's status-model row still says 「#N 关闭时由解锁扫描…放回」. state-machine.md carries the merged condition for a PR; SKILL.md is outside this claim and on its own ratchet row.
  • Noted, not filed: H28 counts a body line naming an abandoned PR as a spent CLOSED target beside a live comment target. Its remedy (rewrite the body to name the live blocker) is still the right one.
  • Measured: REST /issues?state=closed&since=2026-10-30T02:30:00Z returns 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); MCP list_issues with state: CLOSED and the same since returns 38 rows and no PR. The gap is the MCP listing. Either listing still needs merged_at to tell a merge from an abandonment, so the rule names the forward per-card read.
  • Branch name. The card thread's platform-assigned branch claude/project-thread-3r8sm7 names no issue; claim 6515710355 (amended) is the identity record.

Verification

  • Ablation 1 (projection: unmerged forced false): self-test red, 9 of 5084 cases, all in the new battery (e.g. H19 PR: CLOSED UNMERGED -> held: no row got the full "outlived its blocker" row). Restored: blob == HEAD 8c1ace852181, git diff HEAD empty.
  • Ablation 2 (judge: unmerged allowed to found the row): self-test red, 4 of 5084 cases. Restored the same way.
  • Both through scripts/ablation-replace.mjs (anchor hit 1 → 0, marker 0 → 1), on committed head 1b32a159.
  • Gates: head ffe534a1 (merge base bacb3e6a0): node scripts/pm/dispatch-gates.mjs --commands derived 56 families (re-derived after the last commit: identical). After pnpm install --frozen-lockfile --ignore-scripts and pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint (for check: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 includes check: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).
  • Not measured locally, cited from CI instead: the repo-wide pnpm lint and the type-check lanes. This diff touches no TypeScript.
  • Register tier: check-governed-merges.mjs --test on the five paths answers Tier S (.claude/** ×4; scripts/pm/check-half-states.mjs not 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

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; ESLint (--no-inline-config) on scripts/pm/check-half-states.mjs exits 0, and CI's pnpm lint is cited from the check-runs.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing: skip-changeset)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwiU8WPKdGvRr5uhB6

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
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 30, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

Contract-review-claim: session_016WV4d1QkPVVzNz3FJqb2iS · head ffe534a · 2026-10-30T06:21Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ffe534a14cc8adf560179c66dd1cf0aa9c0dad16
Local-runs: probe — on a detached worktree of the head (removed afterwards): scripts/pm/check-half-states.mjs --self-test (green, 5084 cases, battery H19 PR target merge reading 18/16 present), one ablation of blockerResolutionFromRow (unmerged forced false: red, 9 of 5084, all in the new battery; restored, worktree clean), check-skill-line-ratchet.mjs and check-skill-id-lint.mjs with their self-tests (green; id-lint pattern /#[0-9]{3,}/g, 105 files clean), the self-tests of the three importers the PR names (scheduled-full-run.mjs, board-snapshot.mjs, check-governed-queue-guard.mjs, all green), check-governed-merges.mjs --test on the five paths (Tier S); plus REST reads of the card, the PR, PR #27924's record and the head's check-runs
Scope: .claude/skills/pm-dispatch/references/state-machine.md (:24, :31), .claude/skills/pm-dispatch/references/dispatch-runbook.md (:66), .claude/skills/pm-dispatch/references/platform-readings.md (:250-252), .claude/skills/pm-dispatch/references/lanes/devx.md (:26) + scripts/pm/check-half-states.mjs (blockerResolutionFromRow, namedTargets, h19BlockOutlivedBlocker, the H19 resolver in sweepInto, SELF_TEST_BATTERIES, the new battery) as the patrol that executes the rule, reviewed as one change with it; read, not reviewed: scripts/ci/scheduled-full-run.mjs (staleBlockVerdict, the card-step projection), check-half-states.mjs proseBlockerClause, h19DeclaredExitFired, h26BlockOnIndefiniteTarget, h28StaleBodyBlockerLine, .claude/skills/pm-dispatch/references/triage-duties.md:36, .claude/skills/pm-dispatch/SKILL.md:110, scripts/pm/check-skill-line-ratchet.mjs ceiling rows

① Derived judgments

Base: merge base bacb3e6a0; e5c64d9ec (PR #27924) is an ancestor, so the card-step reader the rule text now mirrors is on main. Three commits, five files, +119/−19, exactly the five files the claim 6515710355 lists (lanes/devx.md after the seat's go). git diff --name-status origin/main...FETCH_HEAD shows nothing else moved.

  1. Projection, API shape — correct. blockerResolutionFromRow reads state as before and sets unmerged only when state === 'closed' and pull_request != null and merged_at is falsy. Merged PR (merged_at set) reads closed, unmarked, so outlived; closed PR with merged_at: null or with the key absent reads closed + unmerged: true; open PR reads open; an issue row (no pull_request key) is byte-identical to the old inline projection (state, closedAt, detail: null, labels). This is the same predicate scheduled-full-run.mjs:623 uses in the card step, so the two readers of a PR target now agree. The sweep's resolver (sweepInto) now calls it on the raw /repos/o/n/issues/N answer; the local-open short-circuit above it is untouched.
  2. Judgement — correct and only withholding. h19BlockOutlivedBlocker filters unmerged rows out of closed (so they found no row), counts them in judgedCount (so a mixed card is "1 of 2", PARTIAL, never "Every target it names is closed"), and names them closed UNMERGED through namedTargets. A row that fires only on a merged PR or a closed issue renders byte-identically to main (pinned). An abandoned PR reachable only through the comment archive stays in superseded as before. Nothing that main held is now released; one thing main released (an abandoned PR) is now held. That is the triage ruling's direction exactly.
  3. Pins have teeth — verified, not taken from the report. Ablation (projection's unmerged forced false): 9 of 5084 cases red, all in H19 PR target merge reading, the CLOSED UNMERGED case receiving the full "outlived its blocker" row, exit 1; restored, git status clean. The battery's 18 cases drive the three measured API shapes through the exported projection and then through h19row, so reading and judgement are pinned together; floor 16 is a lower bound under the count, the convention of its neighbours. SELF_TEST_BATTERIES gains exactly that row; roster floor unchanged.
  4. Collateral readers of the same resolutions — read, not changed. h19DeclaredExitFired reads only state === 'open' rows (an abandoned PR is skipped, as it should be). h26BlockOnIndefiniteTarget asks only open targets (so an abandoned PR is not asked whether it can ever close — see ③ and should-fix 4). h28StaleBodyBlockerLine counts a body target with state === 'closed' as spent whether merged or not; its remedy (rewrite the body line to name the live blocker) is still the right act. proseBlockerClause (H4's prose half) filters state === 'closed' and calls every such candidate ALREADY CLOSED, due back in pm:queue — for a prose-named abandoned PR that wording now contradicts the ruling (should-fix 1). None of these is a release path: all four are report-only rows, and the label writers (the card step and triage's unlock scan) both read merged.
  5. state-machine.md:24 — TRUE to the ruling and the tree. 「等 PR 合并写该 PR 号:合并才放,关而未合并 ⛔ 不放」 is option A as triaged (6515340724) and is what the card step (staleBlockVerdict: unmerged withholds) and H19 now do; it agrees with triage-duties.md:36 「issue/PR 号」, which PR fix(ci): a pm:blocked card parked on a PR is released only once that PR merged #27924 added. Declared deviation accepted: the old line folded two waits into one 「指一张在该刻关闭的卡」; the release half is now 「等发布见下行」 and :25 (unchanged) says 「等发布的目标 = pin-bump 杂事卡」, which is the card whose close is the pin coverage, so 「且 pin 覆盖」 is carried by the target, not lost.
  6. state-machine.md:31 — TRUE. 「双通道目标全为本仓已关卡(PR 已合并)、无 Unlock-action: 行与 assignee 才放;跨仓已关不放」 is staleBlockVerdict line for line: held on any open, unread, unmerged, upstream or declared Unlock-action; then assigned on any assignee; only then expired. The merge condition the fix(ci): a pm:blocked card parked on a PR is released only once that PR merged #27924 record asked for (its should-fix 1) is now in the sentence.
  7. dispatch-runbook.md:66 — TRUE to the ruling. The old reverse join (state=CLOSED + since against the index) could never match a PR target through the MCP listing (card body item 2; the thread re-measured it: 38 rows, 0 PRs). Triage 6515340724 says its hourly unlock scan reads each pm:blocked body's targets forward and releases when all are closed or merged; the new line says exactly that: 「逐张 pm:blocked 正向读正文目标:issue 全已关、PR 全已合并才命中走双查」. Declared deviation (drop, not extend) accepted: the dispatch brief allowed either form and asked for what triage's scan does; the tool is not in the tree (ls scripts/pm has no unlock scanner), so the sentence follows the triage comment. 「走双查」 keeps the two mechanical double-checks of state-machine.md:19. 「正文目标」 is consistent with platform-readings.md:252's 「⛔ 不加常设评论读」 and with main's prior 「只 grep body」; the comment channel's old lines are backfilled incrementally (:251), as before.
  8. platform-readings.md:250-252 — one channel rule, settled as the dispatch asked. :250 「写入归 BODY,唯 full-run 卡写评论(其正文每红重写)」 matches triage-duties.md:36 「full-run 卡写评论」 and scheduled-full-run.mjs's own instruction that its body is rewritten on every red. :251 「半状态巡查与 full-run 刷新读双通道」 is true to the code: blockerTargetsFor(issue, fallbackFor(issue)) feeds H4/H19/H26/H28 from both channels, and the card step reads body plus every comment (readCardComments). :252 「解锁扫描 list_issues 列卡、issue_read 直读目标,⛔ 不加常设评论读」 is main's sentence re-cut; the one new clause, 「PR 判合并读 merged_at」, is the field the card step and H19 read. The channel naming (issue_read) is pre-existing; whether the MCP issue_read tool exposes pull_request.merged_at on a PR number was measured only through REST GET /issues/N (should-fix 3).
  9. lanes/devx.md:26 — the seat's go stands; line-neutral, 120 bytes. 「本仓刻意把裁定与更正放在评论里(Blocked-by: 归正文)」 keeps the lane's reading rule (:27 「先读晚于正文的评论再判可派性」 still follows) and removes the one clause that contradicted :250. No other rule in the file moved.
  10. Line ratchet and id-lint — hold. Head line counts: state-machine.md 41/41, dispatch-runbook.md 245/245, platform-readings.md 458/458, lanes/devx.md 35/36; the ceiling rows (check-skill-line-ratchet.mjs :463, :442, :1153, :1426) are untouched by the diff; every changed line is at most 120 bytes (measured 120, 115, 116, 117, 120, 115, 120). check:pm-skill-id-lint is green and its pattern refuses #NNN in skill text; the diff's rule text carries only #N. #27925 appears only in the script, which the lint does not walk.
  11. The PR body's tests and gates paragraphs reproduce: 5084 cases, 18/16 battery, ablation 1 at 9 of 5084 (I re-ran it), the three importer self-tests green, Tier S on the paths with the half-states script off the register. Ablation 2 (judge lets unmerged found the row) was not re-run; the code path it covers is the r.unmerged !== true filter on closed, and the mixed-card pins (items 11-15 of the battery) are the ones that would go red.

② Semver level

No published package is touched (.claude/** and scripts/pm/** only). Clause-②: no and Metadata-protocol: none on the PR body; skip-changeset applied and the Check Changeset run is green. The patrol only gains a withholding branch and a rendering word; the rule text narrows nothing a seat could previously do that the ruling allows. Consistent.

③ Boundary flags

  • open_questions: none in the report; nothing to answer.
  • Author: PR user.login is objectstack-fleet[bot] (attribution comment names os-elon-musk); no login note owed. Draft PR, Fixes #27925, head sha matches claim 6516419081, the only comment on the PR; no competing claim.
  • Declared deviations, all accepted: lanes/devx.md:26 (seat go, ① 9); state-machine.md:24 release half pointing at :25 (① 5); dispatch-runbook.md:66 forward scan replacing the reverse join (① 7); no assignee or reviewer request (process, no tree effect); register tier Tier S read locally, triage graded Tier H — the register rules (AGENTS.md Tier S: .claude/**), and --test agrees; the seat reads --pr before landing.
  • The four noted-not-filed readings, routed:
    (a) H4 prose clause reads an abandoned PR as ALREADY CLOSED — real, same file, same resolution object; it predates this PR (false since e5c64d9 allowed PR targets) and is advisory text in a report-only row, not a governed sentence, so not a must-fix; SHOULD-FIX in this PR (one filter), see should-fix 1.
    (b) An only-target abandoned PR is now silent in H19 — real and created by this PR's withholding, but it is the ruling's direction (held, not released), the patrol is report-only so "no new gate" is not in play, and the seat's duty on an upstream close is already state-machine.md:27 「先重新推导是否有新阻塞并改写该行」. The row that should ask it is H26's question (can this target ever release), which asks only open targets today. FOLLOW-UP CARD, filed as a finding, see should-fix 4.
    (c) SKILL.md:110 「#N 关闭时由解锁扫描或 … 刷新放回」 — for a PR target this is false, but it has been false since e5c64d9 (the refresh already held an unmerged PR); this PR corrects the three detail rules and leaves the summary row. Outside the claim, own ratchet row (319/319, in-row edit is line-neutral; the row is a table line so the 120-byte convention does not apply). SHOULD-FIX, fold in or carry on the follow-up card, see should-fix 2.
    (d) H28 counts an abandoned body PR as spent — the row's verdict and remedy are unchanged in direction (rewrite the body line to name the live blocker); the only imprecision is the word closed for a spent PR. FOLLOW-UP CARD note only, no edit owed.
  • Measurement: REST /issues?state=closed&since=… does list PRs (78 rows, 40 PRs); the MCP listing does not. The rule names the forward per-card read, which needs merged_at either way; consistent with the thread's measurement, which I did not re-run.
  • CI at review time (check-runs on ffe534a1, REST): success — filter, Type Check source gates, Test UI, Test Core (2/8), Snapshot the board, Release section-coverage sweep, Platform checklist gate sweep, Plan Test Core shards, PR-body guards, Live required-set sweep, Lint scoped (tooling self-tests, slot lookup and corpus, query-options erasure), Lint repo and tooling gates, Lint ESLint, Governed Surface Queue Guard, Dogfood Regression Gate, Check PR Size, Check Changeset, Auto Label; still in progress — Type Check (workspace, debt ledger, consumer gates), Test Core (1/8), Live half-state sweep, Lint docs and code guards, Lint PM dispatch-gates, Lint CI/release/engine gates; the rest skipped by path filter. No failure. The all-green condition is still owed by the landing gate (the queue guard reads it); the local runs above predict the PM lanes pass.
  • mergeable_state: blocked (draft). Tier S: after this record the owning seat readies and queues; no human approval owed by the register.

④ Design judgment

Rules face, per hits file, against the triage ruling 6515340724 and the code on this head:

  • state-machine.md — the pm:blocked section now has one target form (a card or a PR number, line-initial, body) and one release condition per kind (issue closed, PR merged), and the full-run sentence carries the same condition. It teaches nothing the two label writers do not do. Holds.
  • dispatch-runbook.md — the hourly unlock scan is described as what triage's tool does; the old reverse join described a read that could not see a PR target and is gone rather than extended, which is the honest sentence given the measured MCP listing. Holds.
  • platform-readings.md — the channel rule is now one rule with its one measured exception (the full-run card, whose body is rewritten), and the two readers that read both channels are named; the unlock scan keeps its no-standing-comment-read posture. Holds, with the issue_read shape unmeasured (should-fix 3).
  • lanes/devx.md — the lane keeps its reading rule; the one clause that contradicted the channel rule is replaced by the rule itself. Holds.
  • scripts/pm/check-half-states.mjs — the resolver's projection is the minimum the judgement needs, shared in shape with the card step, exported so the self-test drives the real API shapes through it; H19 withholds and names, never releases. The same resolution reaches H4's prose clause unhonoured (should-fix 1). Holds.

No file in hits is a schema or ADR, so no first-consumer or execution-plan judgment is owed.

Implemented-by: claude/project-thread-3r8sm7
Reviewed-by: session_016WV4d1QkPVVzNz3FJqb2iS

VERDICT: PASS

Must-fix: none.

Should-fix:

  1. scripts/pm/check-half-states.mjs proseBlockerClause (H4's prose half) still filters r.state === 'closed' and tells the reader a prose-named abandoned PR is ALREADY CLOSED and the card is due back in pm:queue; the resolution it receives now carries unmerged: true, so honouring it is one filter plus a pin in the new battery. Same file, same reading; cleanest folded into this PR before readying, otherwise the follow-up card carries it.
  2. .claude/skills/pm-dispatch/SKILL.md:110 (pm:blocked status row) says 「#N 关闭时由解锁扫描或 scheduled-full-run-card.yml 刷新放回」; for a PR target both releasers now read merged. A few-byte in-row edit (for example 「#N 关闭(PR 则合并)时」) is line-neutral at 319/319. Outside this claim's surface; fold in or carry on the follow-up card so the summary row every seat reads does not disagree with state-machine.md:24.
  3. platform-readings.md:252 names issue_read as the direct read of a target and merged_at as the merge signal; the merged_at shape was measured through REST GET /issues/N only (fix(rest): a refused xlsx export answers the 400 envelope, not a 200 workbook stream #27888, pm:mailbox — sunhaolin #27296, pm-dispatch: triage parks a card pm:blocked "on PR #N" in prose, with no Blocked-by: line, so the unlock never fires when the PR merges (14 domain:server cards stranded 2026-10-30) #27846). Measure once whether the MCP issue_read tool on a PR number returns pull_request.merged_at; if it does not, the line should name the REST read, as the card step does.
  4. File a finding for (b): a pm:blocked card whose only target is a PR that closed unmerged is now held by H19 and surfaced by nothing (H26 asks only open targets whether they can ever release). A report-only H26 clause naming a closed-unmerged target, or an unlock-scan rule to re-derive per state-machine.md:27, is a patrol row, not a gate. Note (d) on the same card: H28's spent-target wording says closed for an abandoned PR; verdict and remedy unchanged.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-elon-musk · session_016WV4d1QkPVVzNz3FJqb2iS

维护者速读

  • 改了什么:卡片可以停在一个 PR 上,PR 合并才放行,关闭但未合并不算。state-machine.md 〈pm:blocked〉 写明目标可以是卡也可以是 PR 号,full-run 刷新句同样写明合并条件。dispatch-runbook.md 的解锁扫描改为逐卡向前读目标,删掉看不到 PR 的反向关联。platform-readings.md 定下一条通道规则:Blocked-by: 写在正文,唯一例外是 full-run CI 卡,写在评论里。lanes/devx.md:26 与之对齐。check-half-states.mjs 的 H19 读 merged_at:关闭未合并的 PR 判 held,不再判过期,并配 18 条自测钉。5 个文件,+119/−19。
  • 为什么改:pm-dispatch: triage parks a card pm:blocked "on PR #N" in prose, with no Blocked-by: line, so the unlock never fires when the PR merges (14 domain:server cards stranded 2026-10-30) #27846(PR fix(ci): a pm:blocked card parked on a PR is released only once that PR merged #27924)让卡可以停在 PR 上,但规则文字仍把 PR 等待指向它要修的卡,反向扫描看不到 PR,H19 也把被放弃的 PR 当成已解锁。这样卡又会像 10-30 那 14 张 server 卡一样被闲置,或被一个没落地的 PR 放行。
  • 风险与回滚:不加门禁。四份规则文件行数不变,都在上限之内。H19 只报告,不改标签。消融后新电池 9 条转红,说明钉子有效。revert 即回滚。
  • 先看:state-machine.md 第 24–25 行,再看 check-half-states.mjs 的 blockerResolutionFromRow。
  • 两分钟验证:node scripts/pm/check-half-states.mjs --self-test exit 0(5084 条,含 H19 PR target merge reading)。
  • 需要你做的:无。.claude/** 按登记册是 Tier S,契约复核 PASS(记录在本评论之前,Head-sha: ffe534a14cc8adf560179c66dd1cf0aa9c0dad16),由本席转 ready 并开自动合并。

席位意见:ACCEPT。契约复核 PASS,没有 must-fix。四条 should-fix 都转到跟进卡 #27989:H4 散文子句、SKILL.md 状态行、issue_read 的 merged_at 形状实测,以及唯一目标是被放弃 PR 的卡静默被扣的问题。它们早在 e5c64d9 之前或之时就已不准,不是本 PR 引入的。CI 全绿。

席位核对

  • 形态: one PR, Fixes #27925, five files: state-machine.md, dispatch-runbook.md, platform-readings.md, lanes/devx.md (all under .claude/skills/pm-dispatch/references/), scripts/pm/check-half-states.mjs; skip-changeset. check-governed-merges.mjs --pr 27983: Tier S (.claude/** ×4; the script is off the register), 138 changed lines.
  • 前提实测: blockerResolutionFromRow: merged → outlived; closed with merged_at null or absent → held (unmerged: true); open → held. Issue rows are byte-identical to the old projection, and the predicate is the same as scheduled-full-run.mjs:623. Self-test 5084 pass; one ablation turns 9 rows red, all in the new battery (review ①).
  • 门禁 check-runs: head ffe534a14cc8adf560179c66dd1cf0aa9c0dad16: 51 check-runs, 31 success, 20 skipped, 0 failed, 0 pending. Line ratchet 41/41, 245/245, 458/458, 35/36, with no ceiling row touched; id-lint green.
  • 接受的偏差: lanes/devx.md:26 (the seat's go before the edit); state-machine.md:24's release half points at :25; runbook :66 replaces the reverse join with the forward per-card scan, which is what triage's hourly tool does.
  • 范围外去向: pm-dispatch(unlock): after PR-target release-on-merged, H4 prose, the SKILL.md status row and the issue_read reading still say closed, and a card whose only target is an abandoned PR is held silently #27989 (pm:blocked on this PR): H4 prose clause, the SKILL.md:110 status row, the issue_read merged_at measurement, the silent hold on an only-target abandoned PR, and the H28 wording.
  • 落地条款: Tier S: the at-tier contract review PASS is on the current head, so the seat flips it ready and arms auto-merge under the maintainer's landing grant for this term (seat thread, 2026-10-29 22:04Z).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant