Skip to content

fix(ci): a pm:blocked card parked on a PR is released only once that PR merged - #27924

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ucikco
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ucikco

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

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

Fixes #27846

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:triage-duties.md 加一行:分诊把卡挂 pm:blocked 时,同一笔在卡正文写行首 Blocked-by: #N(issue 或 PR 号;整点全量红卡写在评论里),散文提及不算。整点全量红卡刷新时的放回判断,目标是 PR 的只认「已合并」:关闭未合并的 PR 不再把卡放回队列。
  • 为什么改:今晚 14 张 server 卡因为分诊只在评论散文里写「blocked on PR #N」,PR 合并后没有任何机制把它们放回,车道一直报「没卡」。实测还发现,刷新读者把关闭未合并的 PR 也读成「已关」并照样放卡,和它自己写的「关闭即修复已进 main」相矛盾。
  • 风险与回滚:只动内部 PM 规则文本和 scripts/ci/ 下的工具,不发布任何包。放回只会更严(多一种「不放」),不会多放。回滚即 revert。
  • 先看:scripts/ci/scheduled-full-run.mjs 的 judgeCardBlock 与 staleBlockVerdict;scripts/ci/scheduled-full-run-card.mjs 的 getIssue;triage-duties.md 第 36 行。
  • 两分钟验证:node scripts/ci/scheduled-full-run.mjs --self-test && node scripts/ci/scheduled-full-run-card.mjs --self-test,预期两行都报 OK(后者 8 个用例)。
  • 需要你做的:契约复审(contract review)尚欠,由席位达档复审;落地档位以登记册为准(node scripts/pm/check-governed-merges.mjs --pr 本 PR 号)。

What changes

The release reader tells a merged PR from an abandoned one. When the hourly full-run card is refreshed while it carries pm:blocked, every Blocked-by: target is resolved through issues.get, and the card step kept only { state, closed_at }. The issues API answers a PR number closed whether it merged or was closed unmerged, so a closed-unmerged PR released the card, against this file's own rationale ("whose closure means its fix landed on this main"). Measured before the fix by driving judgeCardBlock with that projection: merged → expired, closed unmerged → expired, open → held.

  • scripts/ci/scheduled-full-run-card.mjs: getIssue now carries pull_request.merged_at when the answer is a PR (no extra call: issues.get already returns it).
  • scripts/ci/scheduled-full-run.mjs: judgeCardBlock marks a closed PR with no merged_at as unmerged: true (a missing merged_at reads the same, so the unknown case holds). staleBlockVerdict holds on it, and the summary row names it (#N closed unmerged (a PR that never landed)). THE RULE table gains the row.

Triage names its park line. .claude/skills/pm-dispatch/references/triage-duties.md gains: 「停 pm:blocked 同笔在卡正文写行首 Blocked-by: #N(issue/PR 号;full-run 卡写评论),散文不算。」 This is triage's option 1 as ruled in 6513844891. The line is paid in the same file: the two reporting-discipline bullets fold into one, dropping 「⛔ 不弹给维护者」 (already implied by 「只进座位贴和触发器」) and 「运维」. The file stays at 102 lines, its ceiling row is untouched, and no line exceeds 120 bytes.

Why

Triage parked cards with pm:blocked on PR #N in comment prose and no line-initial Blocked-by:. No unlock path reads prose, so 14 domain:server cards whose PR had merged sat blocked while seats reported nothing claimable.

Tests

  • scheduled-full-run.mjs --self-test, battery "card refresh: a stale pm:blocked": a park on a PR, body channel and comment channel, MERGED → released to pm:queue; the same PR closed UNMERGED → held, no label write, no note, the summary names the abandoned PR; the same PR OPEN → held.
  • scheduled-full-run-card.mjs --self-test: new case "a pm:blocked card whose blocker is a PR closed unmerged is held": issues.get answers the PR shape (pull_request.merged_at: null), the step makes no addLabels/removeLabel call and reports block: held -- #601 closed unmerged (a PR that never landed). The battery roster gains the row and its floor moves 7 → 8.

Acceptance notes

Out of this card's surface. The seat files one follow-up card for triage:

  • scheduled-full-run judges only its own CI card, and only on a new red. It never reads any other pm:blocked card, so it would not have released the 14 stranded cards even with a body line. Ordinary cards are still released only by the seat and triage unlock scans.
  • The hourly reverse unlock scan never sees a PR. dispatch-runbook.md 〈分诊两级盘点细则〉 item 2 intersects a state=CLOSED + since listing with the Blocked-by: index. Through MCP list_issues, that listing holds no PRs: measured 2026-10-30, since 02:30Z, 15 rows and none a PR, while 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 merged inside the window. A PR target is found only by the forward scan.
  • state-machine.md 〈pm:blocked〉 points a PR wait at a card. It says a card waiting on a PR merge names 「一张在该刻关闭的卡」 (the card the PR fixes). Option 1 allows the PR number itself. A target that is the fixed card avoids both the reverse-scan gap and the closed-unmerged question.
  • The patrol's H19 resolver shares the closed-unmerged reading. scripts/pm/check-half-states.mjs resolveBlockerTarget reads /issues/N state only, so it reports a block on a closed-unmerged PR as outlived. It is report-only, and the refresh is now the stricter of the two, in the direction that cannot release a card.

Also noted, not filed:

  • H4 clears a card whose Blocked-by: is in a comment, while platform-readings.md says the seat unlock scan greps the body only. A comment-borne line passes H4 and stays invisible to that scan. lanes/devx.md says this repo deliberately keeps Blocked-by: in comments, which disagrees with the body-only rule.
  • state-machine.md's sentence on this refresh (「双通道目标全为本仓已关卡…」) does not yet say that a PR target must have merged.
  • Deviation: the new line carries 「full-run 卡写评论」. The full-run card's body is rewritten on every red, and the refresh note already tells a seat to re-block it in a comment. A body line there would be erased by the next refresh and strand the card with no line. The seat may strike the clause.
  • Branch name. The card thread's platform-assigned branch claude/project-thread-ucikco names no issue; claim 6514262092 (amended) is the identity record.

Verification

  • Ablation, scheduled-full-run.mjs: with unmerged forced to false the self-test fails with expected a refresh judged 'held', got refreshed 'expired' (expired -- #24305 closed); restored byte-identical (cmp).
  • Ablation, scheduled-full-run-card.mjs: with the projection back to { state, closed_at } the new case fails (github.rest.issues.addLabels is not modelled by this case); restored byte-identical.
  • Gates, head 928dcbc5 (merge base 2a3dec2d): node scripts/pm/dispatch-gates.mjs --commands derived 40 families (re-derived after the commit: identical). After pnpm install --frozen-lockfile --ignore-scripts, each ran with its exit code recorded. check:doc-formula-expressions first exited 3 (prerequisite) and exited 0 after pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint; the other 39 exited 0 on the first run. --ran: 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN. This includes check:pm-skill-ratchet (triage-duties.md 102/102) and check:pm-skill-id-lint.
  • Not measured locally, cited from CI instead: check-half-states.mjs --format=markdown --provenance="$PROVENANCE" (takes a workflow value), the Plan Test Core shards step, and the type-check lanes. This diff touches no TypeScript.
  • dispatch-gates --tier: no path-derived mandate. 121 changed lines, under the 3000 threshold.

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 the two changed scripts 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_016WV4cxBhj4jCJgyxHY42p5

…PR merged

The full-run card refresh resolved every `Blocked-by:` target through
issues.get and kept `{state, closed_at}`. The issues API answers a PR
number `closed` whether the PR merged or was closed unmerged, so an
abandoned PR released the card as though its fix had landed on main.

The card step now carries `pull_request.merged_at`, and the verdict
holds a closed PR with no `merged_at` and names it on the summary row.
The self-tests pin merged (released, body and comment channel),
closed unmerged (held) and open (held), plus the card step's
projection.

triage-duties.md names the line triage writes when it parks a card:
a line-initial `Blocked-by: #N` in the card body, an issue or PR
number, beside the label; the full-run card takes it in a comment
because its body is rewritten on every red. The line is paid in the
same file by folding the two reporting-discipline bullets and
dropping their restated clause.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxBhj4jCJgyxHY42p5
@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 928dcbc · 2026-10-30T04:05Z

@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: 928dcbc5340b3efae7e5591eb27e1eb5c36a4e14
Local-runs: probe — on a detached worktree of the head (removed afterwards), as the dispatch brief permitted: scheduled-full-run.mjs --self-test and scheduled-full-run-card.mjs --self-test (both green, 8 cases), the two declared ablations re-run (both red, messages match the PR body), a third ablation of the battery floor, and check-skill-line-ratchet.mjs plus check-skill-id-lint.mjs with their self-tests (green); plus three read-only issues.get probes of the live API shape
Scope: .claude/skills/pm-dispatch/references/triage-duties.md + scripts/ci/scheduled-full-run.mjs, scripts/ci/scheduled-full-run-card.mjs (the gate that executes the rule, reviewed as one change with it); read, not reviewed: scripts/pm/check-half-states.mjs (blockerTargetsFor, resolveBlockerTarget), .claude/skills/pm-dispatch/references/state-machine.md, platform-readings.md, lanes/devx.md, dispatch-runbook.md

① Derived judgments

  1. Reader fix, API shape — correct. issues.get on a PR number returns a pull_request sub-object carrying merged_at; on an issue the key is absent. Probed live: fix(rest): a refused xlsx export answers the 400 envelope, not a 200 workbook stream #27888 (merged) answers state: closed, pull_request.merged_at: 2026-10-30T03:29:36Z; pm:mailbox — sunhaolin #27296 (closed unmerged) answers state: closed, pull_request.merged_at: null; 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 (issue) has no pull_request key. The card step's projection res.data.pull_request ? { pull_request: { merged_at: ... ?? null } } : {} is faithful to that shape and costs no extra call.
  2. Reader fix, judgement — correct. judgeCardBlock sets unmerged only when state === 'closed' and pull_request != null and merged_at is falsy. An issue target (no key) is judged by state alone, byte-identical to main's path: no regression for closed issues. A cross-repo target still reaches the upstream filter unchanged; a cross-repo closed-unmerged PR lands in both unmerged and upstream, held either way, and targetReading names the unmerged reading first. The comment channel is untouched: targets come from blockerTargetsFor in check-half-states.mjs, which the PR does not edit, and the merged-PR pin runs both channels.
  3. Callers — no collateral. judgeCardBlock has one caller, fileOrRefreshCard at scheduled-full-run.mjs:464, on the CI card only. getIssue is injected solely by the card step and the self-test boards; no other file in the repo references getIssue, judgeCardBlock, staleBlockVerdict or targetReading. The held verdict gains an unmerged array; renderBlockNote returns null on held (the unmerged pin checks the body stays NEW_BODY) and summariseBlock reads resolutions, so the new key reaches only the log line.
  4. Pins have teeth — verified, not taken from the report. Ablation 1 (unmerged forced false): expected a refresh judged 'held', got refreshed 'expired' (expired -- #24305 closed), exit 1. Ablation 2 (projection back to {state, closed_at}): the new card-step case throws github.rest.issues.addLabels is not modelled by this case, exit 1. Both restored, worktree clean.
  5. Battery floor 7 to 8 — honest. SELF_TEST_CASES gains one case, SELF_TEST_BATTERIES gains exactly its row, and the floor is a lower bound on the roster length (card.mjs:490-492). A third ablation (floor back to 7, roster intact) still passes, as a lower bound should; the floor's teeth are against deleting a roster row, which 8 now guards.
  6. Measurement claim stands. On main judgeCardBlock has no pull_request reading, so a closed-unmerged PR resolved to closed and expired, against the file's own 「closure means its fix landed on this main」; ablation 1 reproduces exactly that path.
  7. THE RULE table and the fileCardStep comment gain the row; the staleBlockVerdict doc-comment names the new field. Prose and code agree.

② Semver level

No published package is touched (.claude/** and scripts/ci/** only). Clause-②: no and Metadata-protocol: none are on the PR body, skip-changeset is applied and the Check Changeset run is skipped. The release reader only gains a withholding branch; nothing it previously held is now released. Consistent.

③ Boundary flags

  • open_questions: none in the report; nothing to answer.
  • PR author is objectstack-fleet[bot]; no login note owed. PR is a draft; Fixes #27846 present; head sha matches the claim 6514697364.
  • Declared deviation (「full-run 卡写评论」) — true to the tree. scheduled-full-run.mjs:675 tells the seat to 「re-block the card with a line-initial Blocked-by: in a COMMENT -- this body is rewritten on every red」, and the WHY THE UNION paragraph says the comment thread is the only place a line survives on that card. A body line on the full-run card would be erased by the next refresh. Accepted as written; it narrows nothing in the ruling.
  • Line merge — no rule lost. 「⛔ 不弹给维护者」 is entailed by 「只进座位贴和触发器」; 「运维」 was a qualifier, not a rule. The file is 102/102, the ceiling row at check-skill-line-ratchet.mjs:1312 is untouched, the longest line is 118 bytes, ratchet and id-lint green locally and the CI Lint · scoped · tooling self-tests job is green.
  • Report-vs-tree: the report's deviations 2-4 (labels leg, no MCP write, attribution placement) are process notes with no tree effect.
  • Out-of-scope findings, checked: (a) real — fileOrRefreshCard judges only the CI card it filed; the 14 stranded cards are released by the seat/triage scans, which now have a line to read, exactly the ruling's 「the existing unlock paths read it」. (b) plausible and outside — the reverse scan via MCP list_issues lists issues, not PRs; the thread measured it, I did not re-measure. (c) real and the closest to this card: state-machine.md:24 says a PR-merge wait 「指一张在该刻关闭的卡」 and :31 says the refresh releases on 「本仓已关卡」, saying nothing of merge; triage-duties.md:36 now says 「issue/PR 号」 per the ruling. The ruling is the authority and the claim surface excludes state-machine.md, so this is a follow-up, but it is a live inconsistency between two governed files the moment this lands (should-fix 1). (d) real — check-half-states.mjs:23903-23910 reads row.state only; report-only and the refresh is now the stricter reader.
  • CI at review time (check-runs on the head): Lint · scoped · tooling self-tests, Lint · scoped · slot lookup and corpus, Type Check · debt ledger, TypeScript Type Check, Test Core (2/8), Test UI, Dogfood Regression Gate, Plan Test Core shards, Auto Label all success; Test Core (1/8) and Lint · repo and tooling gates still in progress; the rest skipped. No failure. The queue guard reads all-green at landing.

④ Design judgment

  • triage-duties.md:36 against the ruling 6513844891 and the reader on main: option 1 as ruled — same stroke as the label, in the card body, line-initial, issue or PR number, prose does not count. 「散文不算」 matches state-machine.md:23 「句中提及刻意拒收」 and blockerTargetsFor's line-anchored read (list and bold decorations accepted, mid-sentence refused). Placed after the six-state line, where the park decision is made. Holds.
  • scheduled-full-run.mjs against its own rationale: the new row is the one reading under which 「closure means its fix landed on this main」 is true for a PR. A missing merged_at on a closed PR reads as held, which is the one direction that cannot release a card. Holds.
  • scheduled-full-run-card.mjs against the API: the projection is the minimum the judgement needs and the self-test case drives the real PR shape through issues.get. Holds.

Implemented-by: claude/project-thread-ucikco
Reviewed-by: session_016WV4d1QkPVVzNz3FJqb2iS

VERDICT: PASS

Must-fix: none.

Should-fix:

  1. state-machine.md:24 and :31 lag the ruling this PR implements: :24 points a PR-merge wait at a card, :31 says the refresh releases on closed local cards without the merge condition. The follow-up card the thread names should carry both sentences; if that file has line budget, folding it into this PR is cleaner than landing two governed files that disagree.
  2. The H19 resolver (check-half-states.mjs resolveBlockerTarget) should take the same pull_request.merged_at reading so the patrol stops reporting a block on a closed-unmerged PR as outlived; report-only, so a follow-up.
  3. platform-readings.md:250 (body-only reverse index) versus lanes/devx.md:26 (lines kept in comments) is a pre-existing disagreement the thread noted; worth a line on the follow-up card so it is not lost.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

维护者速读

  • 改了什么:triage-duties.md 第 36 行规定:分诊停卡 pm:blocked 时,在同一笔里往卡片正文写行首 Blocked-by: #N(issue 或 PR 号;full-run 的 CI 卡写在评论里,因为正文每次红都会重写),散文不算。scheduled-full-run-card.mjs 的 getIssue 带上 pull_request.merged_at,judgeCardBlock 对关闭但未合并的 PR 判 held,不再放回。3 个文件 +101/−20,含 4 条自测钉,self-test 底线 7→8。
  • 为什么改:分诊过去只在散文里写「停在 PR #N」,没有标准行,PR 合并后没有任何解锁路径触发。2026-10-30 有 14 张 server 卡因此闲置,席位却报「没卡」。改的过程中实测发现,full-run 读者把关闭但未合并的 PR 也当成已解锁。
  • 风险与回滚:不加门禁。读者改动只影响 issue 号以外的 PR 目标(issue 目标照旧按状态判);两处消融都会转红,说明钉子有效。revert 即回滚。
  • 先看:scheduled-full-run-card.mjs 里 getIssue 那几行,再看 scheduled-full-run.mjs 的 judgeCardBlock / staleBlockVerdict。
  • 两分钟验证:node scripts/ci/scheduled-full-run.mjs --self-test 与 node scripts/ci/scheduled-full-run-card.mjs --self-test 均 exit 0。
  • 需要你做的:无。.claude/** 按登记册是 Tier S,契约复核 PASS 6514786457(Head-sha: 928dcbc5340b3efae7e5591eb27e1eb5c36a4e14),由本席转 ready 并开自动合并。

席位意见:ACCEPT。契约复核 PASS,没有 must-fix;三条 should-fix(state-machine.md 的目标写法、H19 的 merged_at、正文与评论通道的说法不一)都转到跟进卡 #27925,由分诊在 A/B 两种写法里定。CI 全绿。

席位核对

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