Commit 2951c0f
docs(skills): the retirement playbook prescribes a
Fixes #19360
Clause-②: no
## The contradiction
`.claude/skills/spec-property-retirement/SKILL.md` :291 — the
**Changeset** item of the
retirement checklist — told the executing agent to grade
`@objectstack/spec` a `major` bump.
That checklist is executed verbatim by every retirement dev, and the
per-PR **Check Changeset**
job (`.github/workflows/pr-automation.yml` :244, running
`node scripts/check-changeset-no-major.mjs --base "$MERGE_BASE"` at
:1075) refuses exactly that
bump while the launch window stands. So the line reddened the author's
own PR for doing what the
instructions said — and then left them to decide, mid-round on an
already delicate public-surface
deletion, whether the playbook or the gate was authoritative.
## Reproduction, both legs measured in this worktree
A throwaway `.changeset/repro-19360.md` was committed on this branch,
the gate run against the
branch point `23f1de078`, the exit captured before any pipe, and the
tree restored
(`git diff HEAD` empty, `git status --porcelain` empty) after each leg.
**Leg 1 — the checklist line copied verbatim (`'@objectstack/spec':
major`)** :: exit **1**
```
⛔ This PR introduces changeset(s) that declare a `major` bump.
.changeset/repro-19360.md
- @objectstack/spec: major
Every publishable package is in the Changesets `fixed` (lockstep) group, so a single
`major` promotes the ENTIRE monorepo to a new major version. During the launch window
ship breaking changes as `minor` instead (they do not burn a major version number).
```
**Leg 2 — the prescription this PR lands (`'@objectstack/spec':
minor`)** :: exit **0**
```
✓ This diff introduces no `major` bump.
```
The same file's own header states the rule the fix restores (:47-:51):
"During the launch window
we ship breaking changes as `minor` (pre-1.0 semantics …). This guard
makes that convention
enforceable instead of tribal". `.changeset/pre.json` is absent on
`main`, so the RC exemption is
not standing the guard down — the `enforce` branch is live.
## Before / after
**Before** (:291-:296, 6 lines):
```
- [ ] **Changeset** —— `@objectstack/spec` 用 `major`。AGENTS.md:breaking
changeset 必须带 FROM → TO 映射与一行修复;它作为 npm 包里的 `CHANGELOG.md`
发出,是升级中的 agent 撞上墓碑报错后 grep 的东西。
样板是任一条活着的 `.changeset/*-retired.md` —— 抄它的 "The retirement
kit:" 段。⚠ 并带上 ADR-0087 处置标记(AGENTS.md 的 changeset 一节;
`pnpm check:adr-0087-registration` 把门)—— 退役正是它点名的那一类。
```
**After** (:291-:296, 6 lines):
```
- [ ] **Changeset** —— `@objectstack/spec` 用 `minor`,⛔ 不用 `major`:逐 PR 的 `Check Changeset` 跑
`scripts/check-changeset-no-major.mjs`,发射窗口期内拒收 `major`,照抄即红。破坏性语义由 BREAKING
banner 承载 —— AGENTS.md:breaking changeset 必须带 FROM → TO 映射与一行修复;它作为 npm 包里
的 `CHANGELOG.md` 发出,是升级中的 agent 撞上墓碑报错后 grep 的东西。样板抄任一条活着的
`.changeset/*-retired.md` 的 "The retirement kit:" 段。⚠ 并带上 ADR-0087 处置标记(AGENTS.md 的
changeset 一节;`pnpm check:adr-0087-registration` 把门)—— 退役正是它点名的那一类。
```
Everything the item already carried survives — the `AGENTS.md:breaking`
pointer, the FROM → TO
mapping requirement, the `CHANGELOG.md` rationale, the
`.changeset/*-retired.md` template pointer
and the ADR-0087 disposition marker. What is added is the level
(`minor`), the prohibition
(`⛔ 不用 major`), the gate and job that enforce it, and the sentence that
the breaking semantics
ride the BREAKING banner rather than the bump level.
## Why the level is stated in the playbook at all, on the four axes
It would have been cheaper to delete the level and defer it to each
card's ruling. That is the
option this PR rejects, and the four axes say why.
- **实际业务需求** — measured, not assumed: the one retirement that ran
against this
contradiction is **#18623**, whose ruling (comment `5725503716`,
maintainer 「同意」, 2026-09-18)
states the level `minor` explicitly. That card therefore needed no stop.
A retirement whose
ruling is silent on the level has nothing to break the tie, and the
checklist is the only
artifact it reads. A blank slot in an executed checklist is not neutral
— it is a prompt to
guess.
- **项目长远合理性** — the split the fix writes down is already the repo's
declared architecture,
not a new convention: `pr-automation.yml`'s "WHICH LEVEL" prose says
verbatim "During the launch
window `major` stays refused by `check-changeset-no-major` and
breaking-ness is carried by the
BREAKING banner plus the ADR-0087 disposition, not by the level"
(maintainer ruling 2026-09-04,
decision batch #35). Naming the gate in the playbook makes the playbook
derive from that rule
instead of racing it, so the line goes stale only when the rule does —
and the gate's own header
carries the GA end condition that will retire both together. No
workaround: the consumer-side
alternative (teach the dev to add `allow-major`) would route around a
guard whose error message
says the label means "a whole-stack major release is genuinely
intended", which is false for a
property retirement.
- **防 AI 写代码犯错** — this is the axis that decides it. The playbook is a
checklist an agent
executes verbatim; the failure mode is 「示例照抄即失败」. A declared step that
the runtime
refuses is exactly the shape the house rules forbid — 声明即强制, never let
an agent declare
what the runtime will not honour. Deferring the level would replace a
wrong instruction with an
absent one, which is the softer version of the same defect: the agent
still has to guess, and
the guess is now invisible in review. Stating the level AND naming the
refusing gate makes the
instruction self-checking — the next author can run the named script and
see the verdict for
themselves rather than trusting the line.
- **创业阶段不扩散需求** — net 0 lines, one checklist item, one file. No new
gate, no new label,
no allowlist, no transition window and no second spelling kept alive:
the `major` prescription
is replaced outright rather than deprecated beside its replacement.
`scripts/check-changeset-no-major.mjs`
is untouched — the launch-window guard is the thing being obeyed, not
the thing being changed.
## Budget
| reading | before | after |
|:--|--:|--:|
| `.claude/skills/spec-property-retirement/SKILL.md` lines | 337 | 337 |
| ceiling in `scripts/pm/check-skill-line-ratchet.mjs` | 337 | 337 |
| widest line in the touched item (bytes, cap 120) | 98 | 120 |
Net 0. The item is repacked in place at 6 lines; every rewrapped line
was produced by the
ratchet's own `wrapLine`/`breakLegal` atoms, so no break lands at a Han
+ ASCII-punctuation
junction. `node scripts/pm/check-skill-line-ratchet.mjs` :: exit 0.
## Gates
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran` at head `ee26ec6`:
**18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN** — a derived zero, every
family recorded an exit
code and none of them is 3. Route-named additions beyond the derived
union, all exit 0:
`check-skill-line-ratchet.mjs`, `check-skill-id-lint.mjs`,
`check-governed-prose.mjs`,
`check-skills-token-ratchet.mjs`, `check:pm-settings-deny-roster`, and
the declared cross-package
consumer `packages/spec/src/shared/retired-key-migrate-sentence.test.ts`
(14 tests passed), which
reads this playbook as a scanned corpus.
`pnpm lint` narrowed to a measurement rather than skipped: eslint's
covered population is
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` read from `eslint.config.mjs`
:971 — a `.md` path is not in
it; `--format json` over the one changed file returns 1 entry,
`errorCount` 0, with the message
"File ignored because no matching configuration was supplied"; and the
config declares no
`parserOptions.project` (type-aware linting deliberately off, :328), so
this diff cannot move the
verdict on any untouched file. The intersection of this diff with
eslint's population is empty.
## Acceptance notes
- Swept for the same defect elsewhere and found none: `grep` for a
`major` changeset prescription
across `AGENTS.md`, all of `.claude/**` and the published `skills/**`
catalog returns zero hits
outside the line this PR fixes. The playbook was the only site.
- `scripts/check-changeset-no-major.mjs` is deliberately untouched: the
launch-window guard is
correct and carries its own GA end condition. This PR moves the
instruction to the gate, not the
gate to the instruction.
## 维护者速读(草稿)
**改了什么** —— 退役 playbook 的 Changeset 那一条,原来写「`@objectstack/spec` 用
`major`」,现在
写「用 `minor`,不用 `major`」,并点名是哪个门禁在拒收(`Check Changeset` job /
`scripts/check-changeset-no-major.mjs`)。一个文件、一条清单项、净增 0 行。
**为什么改** —— 这条清单是 agent 照着一条条执行的,不是读着理解的。照抄这一行写出来的 changeset
会被逐 PR 的门禁当场拒收,PR 变红。更贵的是红之后:agent 得在一张本来就很敏感的「删除公开键」
卡上,临时判断到底是 playbook 算数还是门禁算数。上一次撞上它的是 #18623,那张卡的裁决里恰好写了
`minor`,所以没停;下一张裁决里没写级别的退役卡就没有东西能打破平局。
**风险与代价(含回滚)** —— 风险面就是这一条清单项的措辞,没有代码、没有门禁、没有 schema 改动。
本地实测两条腿:写 `major` 的 changeset 门禁退出码 1,写 `minor` 退出码 0。回滚 = revert 这一个
commit,退役流程回到今天的状态(即:回到会红的那一版)。发射窗口在 GA 结束时,那条门禁自己的
文件头写着终止条件;到那天 `major` 重新合法,这一行要跟着门禁一起改——所以这里点名了门禁,
让下一位读者顺着指针找得到。
**席位意见** ——
**你要做的** —— 确认一句话:发射窗口期内,退役的 changeset 用 `minor` + BREAKING banner 承载破坏性
语义,这与 `pr-automation.yml` 里 2026-09-04 的裁决一致。如果同意,这张卡按 Tier S 走席位
contract
review 落地,不需要你再做别的。
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_
Co-authored-by: Claude <noreply@anthropic.com>minor changeset, not the major a live gate refuses (#19446)1 parent 57ceb9d commit 2951c0f
1 file changed
Lines changed: 6 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
| |||
0 commit comments