Commit 4351033
skills(pm-dispatch): the contract-review PASS leaves its provenance comment on the PR, not only the card (#17856)
Fixes #13417
A contract-review PASS left **zero trace on the PR itself**: the gate
label cleared on both
carriers, `get_reviews` answering `[]`, and the verdict living on the
card only — so a
legitimate un-park was indistinguishable from an intruder read from the
PR side. The Phase-3
rewrite of `references/contract-review.md` already requires a provenance
comment at every
clear, and C6 of `check-clause2-carriers.mjs` already reads the review
record on the PR **or**
the card. What was still open is exactly what the card measured: the
provenance comment's
**carrier was unpinned**, so a seat recording both the record and the
provenance comment on the
card side left the PR as blind as the incident found it.
## The change — one line, `references/contract-review.md` :38
Before (119 B, `origin/main` `9c577c18a`):
> `- 清标即落地:PASS ⇒ 同席同笔剥双载体;凡清标同笔留 provenance 评论,引记录 id 与所判 head。`
After (119 B, this PR):
> `- 清标即落地:PASS ⇒ 同席同笔剥双载体;清标同笔落 PR provenance 评论,引记录 id 与所判 head。`
The provenance comment is now pinned to the **PR** — the carrier that
gets un-parked — while
still citing the record id and the judged head. The card-side one-line
verdict is untouched
(载体纪律不动), `:28` 「复核记录 = 一条评论落 PR 或卡」 is untouched, and no script
changed.
**Measurements.** 60 lines before, 60 lines after (ceiling 60, headroom
0 —
`scripts/pm/check-skill-line-ratchet.mjs`); the line is 119 B, one byte
under the before-line's
own width and under the gate's `MAX_LINE_BYTES = 120`. No ceiling raise,
no second line, no
density payment inside the file, and no cross-file move.
**Wording adjusted from the dispatch's suggested shape, because the
suggested shape does not
fit.** 「清标同笔在 PR 上留 provenance 评论」 measures **126 B**, over the 120-byte
rule; the
cheapest spelling of that pin with 「在 … 留」 is 123 B. Two bytes-level
adjustments bring it to
119 B without dropping a rule: the emphatic 「凡」 goes (its subject 「清标」
stays, so the clause
still speaks about every clear, not only this PASS stroke), and the pin
is spelled with the
file's own verb for a comment landing on a carrier — 「落 PR」, as at `:28`
「一条评论落 PR 或卡」 — instead of 「在 PR 上留」. Every rule token survives: 同席, 同笔,
剥双载体, 清标, the PR pin, `provenance 评论`, 引记录 id, 所判 head.
## Rulings this edit stands on
- The 2026-09-03 Phase-2 ruling of audit #13597 (recorded on the audit;
the maintainer's word
「同意,然后执行契约复审」) — item 5 admits a same-PR cross-file line-budget move
under the
no-total-increase gate. **Not used**: the line fits in place at 60 → 60,
so no move and no
raise was needed.
- The skills seat's 2026-08-31 grading of this card: the fix is the
mechanical form the card
proposed — the PASS disposition leaves a one-line pointer **on the PR**
in the same stroke as
the label clear, with the card-side verdict line unchanged (载体纪律不动).
## Acceptance reading
After this text, a PASS clear that follows it leaves the PR thread
carrying a comment that
names the record id and the head it judged. `get_reviews` answering `[]`
therefore no longer
reads as "no review happened" from the PR side: the observer's own
reasonable read now lands on
the pointer instead of on an unexplained state change. Anchored
evidence: the pointer wording is
absent on `origin/main` (`git grep -n -i -E 'PASS at head|PR 上|落
PR|指针|pointer'` over this
file → 0 hits, lit control `git grep -c '清标'` → 4) and present on this
head (1 hit at `:38`).
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
in the worktree (1 path, no paths passed by hand) — 14 families, all
run, **all exit 0**,
reconciled with `--ran` carrying each exit code: "14 derived famil(ies)
accounted for — 14 run,
0 NOT-MEASURED (a DERIVED zero — all 14 recorded an exit code and none
of them is 3)".
| command | exit |
|:---|:---|
| `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`) |
0 / 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 (first run exit **3**, PREREQUISITE NOT MET — not a measurement;
`@objectstack/formula` + `@objectstack/lint` built under
`scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0`, then re-run) |
| `pnpm check:agent-test-spelling` | 0 |
| `pnpm check:doc-authoring` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:pm-skill-id-lint` | 0 |
| `pnpm check:pm-skill-ratchet` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:skill-frame-sync` | 0 |
| `pnpm check:watch-hint-literal` | 0 |
Gate verdict lines quoted rather than a bare `$?` (exit captured before
any pipe):
- `✓ check-skill-line-ratchet:
.claude/skills/pm-dispatch/references/contract-review.md is 60 lines
(ceiling 60; headroom 0).`
- `✓ check-skill-line-ratchet:
.claude/skills/pm-dispatch/references/contract-review.md: widest table
row is 0 bytes (pin 0; headroom 0).`
- `check-nul-bytes: OK (scanned 8483 text file(s) ... no raw ASCII
control bytes).`
- `✓ check-skill-id-lint: 27 file(s) clean (pattern for three-plus-digit
card ids).`
The ratchet family was re-run **after** the final commit, on `b54e3954e`
with a clean worktree:
exit 0, same two lines. Control reading, no script changed:
`node scripts/pm/check-clause2-carriers.mjs --self-test` → exit 0, "546
cases pass".
No changeset: the diff is `.claude/**` only, which publishes nothing
from any released package
(the fast lane in AGENTS.md's Post-Task Checklist step 3).
`skip-changeset` applied on the PR.
The derivation's 14 changeset-dependent families are derived against a
path that does not exist
and are correspondingly not in the run.
## Acceptance notes
- **Two script comments now quote the OLD wording of `:38` verbatim**
and go stale with this
edit: `scripts/pm/check-half-states.mjs` (the H61 header, quoting the
whole line "verbatim and
untranslated") and `scripts/pm/check-clause2-carriers.mjs` (the C6
docblock, quoting
「凡清标同笔留 provenance 评论,引记录 id 与所判 head」). Neither is gate-enforced — no
script
reads this file at runtime except the ratchet, for line counts, and
H61's own pin asserts only
「清标即落地」, which survives — and neither quote contradicts the new text;
each is simply
less specific than it (no carrier pin). Left untouched deliberately: the
dispatch fences this
card to `references/contract-review.md` with ⛔ no script change, and
touching either script
would add its own self-test surface to this PR. Successor: whoever next
edits H61 or C6, or a
quote-sync follow-up if the seat wants one.
- Not filed as a card: prose drift in a comment is neither a
reproducible defect, a declared
contract violation, nor a metadata-authoring trap.
## 维护者速读(草稿)
**改了什么**:`references/contract-review.md` 第 38 行一句话,把 PASS 清标时那条
provenance
评论钉到 **PR 上**(原文只要求「留一条评论」,没说落哪个载体)。60 行进、60 行出,119 字节,
不动天花板、不动别的文件、不动任何脚本。
**为什么改**:合法的契约复核 PASS 在 PR 侧不留任何痕迹 —— 标签没了、`get_reviews` 读空、结论
只在卡上。观察者做了最合理的那次阅读(看 PR),得出「没人复审过」,于是善意回停已通过复审的
PR。这一次的实测代价:一张 p1/security 卡、两次回停、一次总监调查、两个 PR 空等约 40 分钟。
文内早有读者侧缓解(「`get_reviews` 读空 ≠ 未复审」),事故照样发生 —— 所以承重的是写者侧的
这一行。
**风险与代价(含回滚)**:风险低。这是一行规则文字,不是机制:C6 已经同时读 PR 与卡两条线程,
本 PR 不动它,所以门禁语义零变化。代价是每次清标多一条 PR 评论(席位已在同一笔里写卡侧结论,
增量是一次 API 写)。回滚 = 撤销这一行,不留任何残留状态。两处脚本注释仍逐字引着旧措辞,已在
上面 Acceptance notes 里点名,未改动。
**席位意见**:(留待席位定稿)
**你要做的**:这是受管面(`.claude/**`)⇒ 只能人工合并。PR 保持 draft;确认这一行的措辞(尤其
「凡」字去掉后是否仍读作「每次清标」)后由维护者手工落地。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 80ef826 commit 4351033
1 file changed
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
| 38 | + | |
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| |||
0 commit comments