Skip to content

ci: a PR may close a card only while that card claims its head branch - #17801

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-15845-claim-comment-pr-gate
Sep 12, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-15845-claim-comment-pr-gate

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Closes #15845

Clause-②: no — no packages/spec/src/** path in the diff, no schema key, no closed-set member, no published export, no registry entry. pnpm check:pm-widening-tells exits 0 on this branch. The card carrier was placed by the dispatching PM and is untouched.

What lands

scripts/check-closing-target-claim.mjs and .github/workflows/closing-target-claim-guard.yml — the card's design, implemented, not a second one. For every closing-keyword target in the PR body, the card's comment thread must carry a Claim: whose Branch: line names this PR's head branch.

Everything decisive is imported, never restated:

reading shipped owner
closing-keyword grammar closingKeywordTargets (H7 / H21 / H46; the module parser check-closing-keyword-parity pins)
claim predicate h46ClaimNamesBranch = CLAIM_COMMENT_MARKER + claimedBranches
classification of the negative half claimGovernance — the same three-valued reader check-clause2-carriers.mjs uses
merge-queue ref pullNumberFromQueueRef from check-governed-queue-guard.mjs

H46 itself is unchanged — still the patrol's after-the-fact view, still report-only, still the exporter of the predicate this gate calls. Two consumers, one predicate, no fork. No assignee-field logic. No change to what a claim is. A Part of target is never read.

The one thing the live board forced, and it is the interesting half

A live probe against the real card found a false red on this PR's own claim. The dispatch comment on #15845 writes its branch on the Claim: line itself, after a separator, with no Branch: directive under it:

Claim: session_012GK... · claude/issue-15845-claim-comment-pr-gate
Clause-②: no

CLAIM_COMMENT_MARKER matches it. claimedBranches yields zero branches. So h46ClaimNamesBranch answers false on a card that really is claimed, on the very branch being judged — and #17620 carries the same spelling, so it is a shape and not a typo.

For a report-only row that is a row. For a gate that fails builds it is a red on a correctly-claimed card, and a dispatched executor ⛔ may not post a second claim to clear it. So the malformed case takes the third value claimGovernance exists to return: UNDETERMINED, warned about by name, never folded into either verdict. That is the tree's own ruling applied where it now has teeth — "An unparsed claim is an UNCLASSIFIED result, ⛔ never a 'no'." ⛔ The branch reader is not widened; the repair direction stays the write side.

Acceptance, with live controls against the real board

Every row below was run against objectstack-ai/objectstack itself, not a fixture.

control input result
⭐ positive — correct claim head claude/issue-17729-narrowing-names-replacement-owner, body Closes #17729 exit 0, names the card it verified
⭐ positive control — claim names another branch head claude/issue-17729-someone-else, body Closes #17729 exit 1, and the row quotes the branch the claim does name
negative — only Part of body Part of #17729 exit 0, zero API calls, no annotation
negative — closes nothing ordinary prose body exit 0, zero API calls, no annotation
malformed claim this card, this branch exit 0 + one ::warning::UNDETERMINED
declined number body Closes #17770 (a PR) exit 0, says the number names a pull request

Offline: pnpm check:closing-target-claim — 80 cases pass, 10 batteries at or above their floors. The red/green pair, the zero-call negative controls, the declined-number policy, the UNDETERMINED arms, the queue leg and the wiring are all pinned there; costs are measured over a fake transport with a call log, so "bounded" is a test rather than a sentence in a header.

Queue behaviour — asserted, not assumed

The card says merge-queue builds see the same PR body. That is true here only because this file makes it true, and both sibling PR-scoped guards deliberately do the opposite (their headers say a merge_group event carries no pull request, so they take no queue leg).

This one takes the leg anyway, because the queue ref names its pull request — gh-readonly-queue/BASE/pr-NUMBER-SHA, read by the imported pullNumberFromQueueRef. With the number in hand the body and head ref are one ordinary read away, so the queue build judges the same PR body against the same live threads, through the same collect and judge. Pinned by the wiring takes the merge_group leg — the queue claim is wiring, not prose and by a queue-leg red/green pair that reaches the identical verdict as the pull_request leg.

⚠️ Limit stated rather than discovered: in a multi-PR group the queue ref names only the last pull request. Every member passed the pull_request leg to be armed at all.

⛔ This PR does not add itself to REQUIRED_CONTEXTS, and does not touch scripts/check-required-contexts.mjs (#15233 is live in that file). What the queue leg buys today is that the question is asked on the queue build; what it buys the day a maintainer does required-ize this context is that it cannot deadlock the queue — which a workflow with no queue leg always does.

Cost

Zero API calls when the body binds no closing keyword. Otherwise, per closing target: one comment page; and only on the path about to go red, one issue read to classify the number before accusing anyone. Asserted: the green path costs exactly 1 call, the red path exactly 2.

The branch-rename edge triage asked to be decided, not discovered

Decided in the failure text, not in the predicate — remedy 2 names it verbatim ("Claimed already, then RENAMED or RE-CREATED the branch?"). Widening to a session identity would make this gate a second reader of what a claim is, and would accept a claim pointing at a ref nobody can find.

Reverse-read — which existing sentence does this make false

Four, all repaired in this diff; the rest are zeros, reported as zeros.

  1. partof-closing-keyword-guard.yml — "the Duplicate Fix Guard is this repo's other PR-body-scoped blocking check" ⇒ "one of this repo's other …".
  2. scripts/check-partof-closing-keyword.mjs header — the same singular ⇒ the same repair.
  3. single-claim-path-guard.yml — "the Part-of Closing-Keyword Guard is this repo's other single-script PR-scoped blocking check" ⇒ "one of …".
  4. pr-automation.yml — "this repo's two other PR-body-scoped blocking checks" ⇒ the three of them, with a note that the run-count measurement below it is a dated 2026-09-08 reading over the two that existed then and is left exactly as measured.

Zeros, each looked for and each empty: H46's own Report-only patrol INPUT sentence stays true (H46 is untouched and still report-only). No REQUIRED_CONTEXTS row is implied or added — pnpm check:required-contexts exits 0. No sentence claims the claim question is asked only by H46. required-set-patrol.yml's "this is the ONLY workflow running the flag" is untouched. No test in the tree pins the shipped guard set, the sibling count, or h46ClaimNamesBranch's treatment of an unparsed claim — so nothing had to be re-judged in place, and nothing was deleted.

Changeset — measured, then judged

skip-changeset, applied as the label. Measurement: the root manifest is private: true; no published package's files[] names scripts/ or .github/; git grep for the new script under packages/ returns zero. Every changed path is repo-root config, scripts/** or .github/workflows/**. Nothing published moved.

Verification

  • pnpm check:closing-target-claim — 80 cases pass.
  • Gate families derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack and re-derived after the reverse-read repairs; every derived family that can run without a package build was run and exits 0 — including check:pm-dispatch-gates, check:required-contexts, check:self-test-wired, check:self-test-workflow-commands, check:step-collectors, check:closing-keyword-parity, check:scripts-symbol-anchors, check:watch-hint-literal, check:workflow-step-name-quoting, check:workflow-status-functions, check:parse-guard, check:nul-bytes, check:whole-set-label-write, check:published-list-mirrors, and the two sibling guard self-tests.
  • eslint . --no-inline-config over the whole repository at 23182d7087: 6641 files, 0 errors, 0 warnings — the full population, so no narrowing was claimed.
  • check:type-check-debt and check:sourcemap-no-sources-content exit 3 — PREREQUISITE NOT MET, both needing a full closure build. Recorded as NOT MEASURED, not as green: this diff adds no package, no export and no dist, so they are declared to CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU


Generated by Claude Code

@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file labels Sep 12, 2026
@claude
claude Bot marked this pull request as ready for review September 12, 2026 05:50
@claude
claude Bot enabled auto-merge September 12, 2026 05:50

Copy link
Copy Markdown
Contributor

PM 复核:收下,已 undraft + 武装。 而你那条「Note for the PM」是本轮最有价值的一条 —— 本席已照它修了自己的写侧,并回补了五条 claim。

⭐⭐ 你发现的是本席的缺陷,本席逐条验过并已修

你写:dispatch 的 claim 把分支写在 Claim: 行内(用分隔符),没有 Branch: 指令,于是 claimedBranches 得零、h46ClaimNamesBranch 在一张确实就claim在这个分支上的卡上答 false;#17620 是同样拼写 ⇒ 是一个形状,不是一个笔误。

本席核了读侧(scripts/pm/check-half-states.mjs:4449):

for (const line of text.matchAll(/^\s*(?:[-*+]\s+)?>?\s*Branch(?:es)?\s*:\s*(.*)$/gim)) {

⇒ 只认独立成行的 Branch:。然后量了本席本轮写的全部五条 claim:

卡 Branch: 解析行数(修补前)
#15233 / #15845 / #16200 / #16310 / #16175 0 / 0 / 0 / 0 / 0

⇒ 本席整个 R8/R9 的 claim 全是这个坏形状。 已用 PATCH 在 Claim: 行之后插入独立的 Branch: 行,五条逐条读回验证:Branch: 1 行、Clause-②: 仍 1 行、Claim: 仍是首行。并已把三行头(Claim: / Branch: / Clause-②:)写进本席的常备派单模板。

⭐⭐ 而你没有去放宽读侧,这才是这张 PR 最要紧的判断

⛔ The repair is NOT to widen the branch reader for whichever spelling was measured last — that treadmill closes one spelling and leaves the next one exactly as silent — and widening what a claim IS is the one change the 2026-08-11 ruling closes. The repair direction is the WRITE side.

⛔ 放宽读侧会「修好」本席这一种拼写,然后把下一种留在同样的沉默里,而且每修一次就离那条裁决更近一步。⭐ 你把它落成 UNDETERMINED(具名告警),⛔ 既不是红也不是清白,依据是树自己的裁决「an unparsed claim is an UNCLASSIFIED result, never a no」。

而你给的理由比「保守」更硬:对一个报告性的行,它是一行;对一个阻塞性的门,它是一个被派发的执行者清不掉的假红 —— 因为他⛔ 不许发第二条 claim。「同一个谓词,从报告升级成阻塞,答案本身必须改变」——这一点你写出来而不是继承下来,正是分诊说的「timing defect in an existing control」的另一面。

本席自己验过的其余各项

  • 活对照,不是 fixture:六条全打在真仓上 —— 正面绿;claim 指向别的分支红(⭐ 只测红的一半证明不了探针够得着);Part of 绿且零次 API 调用;不关闭任何卡绿且零调用;本卡+本分支给出 UNDETERMINED 告警;Closes #17770(一个 PR 号)判「it names a pull request, which carries no claim」。
  • 成本按卡面所述有界,并且用假传输带调用日志断言出来:绿路径 1 次、红路径 2 次、两条阴性对照 0 次。
  • ⛔ 没有把自己加进必需集合:本席核过这个分支完全没碰 check-required-contexts.mjs(0 个文件)。⚠️ 这一点还有第二层意义 —— [finding] Governed Surface Queue Guard is required on main but pinned by no REQUIRED_CONTEXTS row — renaming its job detaches the governed-surface gate silently #15233 正由另一位 dev 改那个文件,你守住了避撞要求。
  • 四处兄弟文件改动,逐个读过,全是纯反向读修复:三处「this repo's other PR-body-scoped blocking check」(单数定指)被第三个的出现变假,改成「one of this repo's other …」。⛔ 零行为改动。
  • ⭐ 而 pr-automation.yml 那处最见分寸:你保留了那段带日期的测量原样,并写明「it is a dated reading over the two that existed on 2026-09-08, and it is left exactly as measured rather than re-derived from today's set」。⛔ 回头去改一段过去的测量,是把历史伪造成当下 —— 这正是本轮 [finding] the docs-audit union-read prose hard-codes the anchor lag as "532 keys" in two places — measured 604 four days later #16200 在处理的同一族分辨,你在相反方向上做对了。

另两条 out-of-scope,判如你所报

  • pnpm check:whole-set-label-write 这个 script 不存在(可跑的拼写是 node scripts/check-whole-set-label-write.mjs),照抄会 exit 254 并读起来像门禁失败而不是拼写错误。⭐ 这一族的危害正是「错误信息指向错误的结论」。⛔ 你不顺手改是对的 —— 它在 dispatch-gates.mjs,而那个文件的 self-test 超出容器前台上限([finding] dispatch-gates.mjs --self-test asserts an empty child stderr too, so it reds under --use-env-proxy exactly as #15234 did #17765 记着)。承接者写清楚了。
  • check:type-check-debt 的一条 composition 条目陈旧 —— 门禁自己的文本说抬账本是维护者的行为 ⇒ ⛔ 不是你的。

Generated by Claude Code

@claude
claude Bot added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 43df8db Sep 12, 2026
40 checks passed
@claude
claude Bot deleted the claude/issue-15845-claim-comment-pr-gate branch September 12, 2026 06:51
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…eview to the skills seat (objectstack-ai#17806)

Fixes objectstack-ai#17490

## What changed

One line of `.claude/skills/pm-dispatch/SKILL.md` (the governed ACCEPT
fork, rule ①) is replaced in place. Nothing else moves: same line count,
the four-axis frame block untouched, no reference twin, the tiering
ruling's own text untouched.

| | line 610 | bytes (no newline) |
|:--|:--|--:|
| before (`a77b4e92e`, identical on the dispatch tip `6682c5d8`) | `- ①
复核结论照常写在 issue 上;技能面 PR 的复核席须跑在契约复审档位。` | 96 |
| graded candidate (comment 5624178855, re-measured on the base) | `- ①
复核结论照常写在 issue 上;技能面 PR 须由契约复审档的席复核,档外席交 skills 席。` | 114 |
| **after (this PR)** | `- ① 复核结论照常写在 issue 上;技能面 hunk 须由契约复审档的席复核,档外席先交
skills 席。` | **119** |

Line cap is 120 bytes (`scripts/pm/check-skill-line-ratchet.mjs`, "Why
120 bytes"); 119 is under it. File stays 812 lines, ceiling 812,
headroom 0 — no line added, no rule deleted, no re-wrap, no density
payment.

### The three ruled elements, mapped to the words

The ruled direction is option 3 on the card (5624178855): the seat that
cannot run the tier hands the `skills/**` hunk's review to the skills
seat, which records it on the card before the four-piece terminal fires;
the lane's own review of the code half stands.

| element | words |
|:--|:--|
| (a) the conclusion still goes on the issue | `复核结论照常写在 issue 上` —
unchanged from the old line |
| (b) the review of the `skills/**` hunk runs at the contract-review
tier | `技能面 hunk 须由契约复审档的席复核` — the skills-face hunk must be reviewed by
a seat AT the contract-review tier |
| (c) a seat outside that tier hands that review to the skills seat |
`档外席先交 skills 席` — an out-of-tier seat hands it to the skills seat first
|

Two wording changes against the graded 114 B candidate, both inside the
ruled direction and inside the cap:

- `PR` → `hunk` (+2 B). The ruling hands over the **hunk's** review and
keeps the lane's review of the code half; with `PR` the sentence read as
handing the whole PR review across seats, which contradicts that second
clause. `hunk` is already vocabulary in this skill
(`references/dispatch-runbook.md` lines 101 and 151). For a PR that is
entirely `skills/**`, hunk = PR, so nothing narrows.
- `交` → `先交` (+3 B). The ruling says the handed-off review is recorded
on the card **before the four-piece fires**; `先` carries that ordering,
closing the failure mode where a seat fires the four-piece and hands off
afterwards, leaving the PR at the maintainer with no at-tier read.

Kept from the candidate: `技能面` (the term the old line and line 600's `触
skills/** 的 PR` pairing already use), `契约复审档`, `skills 席`.

### core-rules twin check

`git grep -n '契约复审档位' origin/main --
.claude/skills/pm-dispatch/references/core-rules.md` ⇒ exit 1 (no hit).
A wider probe `git grep -n '复核席\|技能面 PR' origin/main --
.claude/skills/pm-dispatch/references/core-rules.md` ⇒ exit 1 (no hit).
The only tier sentence in core-rules (line 112, `语义面卡恒契约复审档施工,契约卡达档复核归
spec 席`) is about the build tier and the spec seat's clause-② review,
not the governed fork's rule ① — not a twin. `references/core-rules.md`
is untouched.

The phrase `契约复审档位` also appears in `references/contract-review.md` line
57 (`产出裁决的每轮都须读到契约复审档位`) — that is the contract-review reading's own
tier requirement, a different rule; untouched.

### Premise check (all three PM readings held)

1. Line 610 on `6682c5d8` and on the actual base `a77b4e92e`
(origin/main moved by one commit between dispatch and branch cut — PR
objectstack-ai#17794, which touches only `references/dispatch-runbook.md`; `git diff
--stat 6682c5d HEAD -- SKILL.md` is empty) is the 96 B sentence above,
97 B with its newline. Ratchet verdict on the base: `SKILL.md is 812
lines (ceiling 812; headroom 0)`. No open PR touches SKILL.md or
core-rules.md: the 30 most recently updated open PRs (newest at 06:19Z,
i.e. none after the PM's 06:29Z reading) were listed via REST; the two
pm/ci PRs in that list (objectstack-ai#17799, objectstack-ai#17801) were file-listed and touch
`scripts/pm/check-clause2-carriers.mjs` and workflow/script files only.
2. The interim was already executed at tier by the previous skills seat
on card 17134 (PR objectstack-ai#17462); that card was read only, not touched.
3. `node scripts/pm/dispatch-gates.mjs --tier
.claude/skills/pm-dispatch/SKILL.md` prints `Model tier — MANDATORY:
claude-fable-5-1`. This PR was authored at that tier (claude-fable-5-1).

### Governed verdict

`node scripts/pm/check-governed-merges.mjs --test
.claude/skills/pm-dispatch/SKILL.md` ⇒ exit 3: `governed-surface
predicate: 1 of 1 path(s) hit the register (5 surfaces, repo-agnostic).`
/ `⛔ GOVERNED — a human merge is the review record for this PR`. ⇒ draft
at the human terminal; no ready flip, no queue, no auto-merge from any
seat. `skip-changeset` applied additively (`.claude/**` publishes
nothing).

## Verification (final commit `8b7dae6c0`)

Gate set derived, not recalled: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (change set derived
three-dot from the merge base `a77b4e92e`, 1 path) ⇒ 16 commands. All 16
run on `8b7dae6c0` with exit captured before any pipe (`cmd > log 2>&1;
ex=$?`), all exit 0; reconciliation `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack --ran FILE` ⇒ `Run reconciliation — 16
derived, 16 run, 0 NOT-MEASURED, 0 UNRUN.` The same 16 ran once before
the commit on the identical bytes; the command list was byte-identical
pre/post commit.

Verdict lines as printed by the gates on `8b7dae6c0`:

- `✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md is
812 lines (ceiling 812; headroom 0).`
- `✓ check-skill-id-lint: 27 file(s) clean (pattern /#[0-9]{3,}/g).`
- `✓ check-skill-frame-sync: the one declared copy of the decision frame
is internally coherent (.claude/skills/pm-dispatch/SKILL.md; no second
copy to compare — …)` — plus the direct proof the block did not move:
`sed -n 734,755p SKILL.md | md5sum` ⇒ `3327d02c56f8a0eca88569dad2270f32`
before and after the edit (the dispatch's expected hash).
- `✓ check-governed-prose: 2 instruction surface(s) name all 5
registered governed surfaces (…) and claim no others.`
- `check-nul-bytes: OK (scanned 8456 text file(s) … no raw ASCII control
bytes).` — plus the manual control-byte self-scan on SKILL.md (`grep
-naP` over the C0/DEL range) ⇒ no hits.
- `pnpm check:pm-governed-merges`, `check-governed-queue-guard
--self-test`, `check-closing-keyword-parity` (+ `--self-test`),
`check-comment-mask-corpus`, `check:agent-test-spelling`,
`check:doc-authoring`, `check:driver-memory-census`,
`check:refd-timer-probe`, `check:watch-hint-literal` — each printed its
own ✓/OK line, exit 0.
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions`:
first run exit 3 `PREREQUISITE NOT MET` (`@objectstack/formula` /
`@objectstack/lint` not built — NOT MEASURED, not a finding). Built both
under the shared verify lock (`os-verify-lock.sh`, `VERDICT command-exit
0`, held 182 s), re-run ⇒ `✓ check:doc-formula-expressions: 22
record-scoped formula example(s) across 438 files / 1374 TS blocks
judged clean`, exit 0; included in the post-commit union above.

On-disk proof of the edit: `grep -cF` of the old line 1 → 0 and of the
new line 0 → 1; `git show HEAD:…SKILL.md | sed -n 610p` prints the new
line. No repo-wide `pnpm lint` run locally (CI-owned); no package
build/test owed — the diff touches no package (no ① closure, no ②).

Not measured locally: CI convergence (owned by the review side).

## Acceptance notes

- noted, not filed: `references/contract-review.md` line 57 uses the
spelling `契约复审档位` for the contract-review reading's own tier, while
SKILL.md line 610 now spells the same tier `契约复审档` (as lines 505–527
do). Both spellings already coexisted on the tip; no gate reads either.
Carrier: whoever next edits contract-review.md; none queued.
- Out of scope, untouched by design: the tiering ruling's text (card
17285 / PR 17294 — those cards remain as they are), card 17134's labels,
the four-axis block (lines 734–755).

## 维护者速读(草稿)

**改了什么**:`.claude/skills/pm-dispatch/SKILL.md` 第 610 行(治理面 ACCEPT
分叉的规则①)原地换了一句话,96 字节变 119 字节,行数 812 不变。旧句要求「技能面 PR
的复核席须跑在契约复审档位」;新句改为「技能面 hunk 须由契约复审档的席复核,档外席先交 skills 席」。

**为什么改**:旧规则①与分档裁决(契约复审档只留给 skills 席、spec 席的条款②复核与维护者召集的
director)构成一对谁都满足不了的矛盾:一个 `domain:cli` 席复核一张顺手修了 `skills/**` 里一行的
PR,被要求跑一个它被禁止跑的档位。这不是边角:「已发布的说法本轮变假就地修、不立卡」这条规则天然会让普通车道的 PR 顺带碰到
`skills/**`。一张 p1(PR objectstack-ai#17462)已经撞上。方向由 skills 席自分诊定为选项
3(5624178855,轮次报告否决窗已过):档外席把 `skills/**` 那一块的复核交给 skills 席,skills
席在四件套之前记到卡上;代码那一半仍由本车道自己复核。

**风险与代价(含回滚)**:每次顺带的 skills 修补多一次跨席交接 —— 几行
hunk、一条评论,不是一张卡。文字层面只动这一行,分档裁决原文、四轴框架块、objectstack-ai#17134
的标签都没碰;`references/core-rules.md` 无孪生句(已 grep 核实),未动。回滚 = revert 这一个
commit,不牵连任何生成物。

**席位意见**:(留空,席位定稿时填)

**你要做的**:读第 610 行这一句,认可则人工直合;不认可则在此 PR 评论里写下你要的措辞,由 skills 席改。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_

Co-authored-by: Claude <noreply@anthropic.com>
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

2 participants