Repository navigation
Migrate the release toolchain to @changesets/cli v3 — one atomic PR carrying the bump, the pre-mode restructure, and the gates that model v2's semantics #9465
Description
Epic — coordination node. Never dispatched itself; the work lives in its sub-issues.
Dependabot #9217 proposes @changesets/cli 2.31.1 → 3.0.0. It is a devDependency bump on paper and a release-machinery migration in fact. The maintainer decided on 2026-08-17 to do it deliberately rather than take or drop the raw bump, and on 2026-08-18 to run it as an epic.
Delegation
- Epic PM session:
session_01HDA9nN6nXQngoQUAAzRdMb - Declared file territory (other lanes: treat these as claimed for the duration; this epic does not claim anything else):
.changeset/**·scripts/check-changeset-no-major.mjs·scripts/check-empty-changeset.mjs·scripts/check-changeset-fixed.mjs·scripts/objectui-changeset-digest.mjs·.github/workflows/cut-rc.yml·.github/workflows/release.yml·.github/workflows/pr-automation.yml(changeset-related steps only) · rootpackage.json(the@changesets/clirange and theversionscript) - Nothing here touches
packages/spec. If a sub-issue turns out to, it goes to thedomain:specseat rather than staying in this subtree.
Why do it — benefits are repo-specific, not generic
- v3 removes the root cause of our RC-window pollution. Under v2, pre-mode
changeset versiondoes not delete the changesets it consumes; they pile up in.changeset/. Measured on the last window: 1552 changeset files, 1279 consumed-but-undeleted. That pollution is whycheck-changeset-no-major.mjscarries ~88 lines of pre/RC exemption reasoning, whose own header records the absurd consequence — a typo-sized PR shown 171 major-declaring files it never touched, withallow-majoras its only exit, a label whose message then means something false. v3 (changesets#2190) moves consumed prerelease changesets into.changeset/pre/. - v3 makes a no-op
changeset versionexit 1 (changesets#1860) instead of exiting 0 silently. We have paid for that failure mode: 空 changeset 会静默卡死已 version 的发布:Release run 全绿,但 npm 和 Docker 什么都没发(17.0.0-rc.2 现在就卡着) #4898 stalled17.0.0-rc.2while every check stayed green. - A chance to drop
___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange— a key whose own name is upstream reserving the right to change it in a patch release. - Maintenance line: 2.x gets fixes only; our
.changeset/config.json$schemais still pinned at@changesets/config@3.1.2.
Explicitly not worth effort: changeset tag → git-tag (never invoked here), prettier → format (unset), Yarn Classic / Bolt removal, the @clack/prompts interactive rewrite.
Rulings that bind every sub-issue
- The bump and the migration land as ONE PR. Both half-states are broken and each is broken silently until the next release: bump alone leaves v3 installed against v2-shaped gates; migration alone leaves gates expecting
.changeset/pre/that v2 never creates. - Measurement outranks the changelog. No sub-issue may act on "v3 is documented to…" where it could instead run the thing and read the output.
- The allowlist follows the measurement, never the reverse. If a v3
changeset versionproduces a file setcut-rc.ymlwould reject, the allowlist changes — the tool's output is not massaged to fit a list written under v2. - A self-test still asserting v2 semantics is a failed run, not a passing one.
- Perform no release. No
changeset publish, no version tags, no release-workflow dispatch, no merging of a Version Packages PR, and no pre-mode entry or exit onorigin. Releases are human-only in this repo and this epic does not change that.
Timing
.changeset/pre.json is absent as of 2026-08-17: not in pre mode, 143 changesets in the root rather than 1552. That makes now the cheapest window for the migration itself. Each sub-issue re-verifies this at start; if a cut has begun, the migration reschedules rather than forcing.
Sub-issues
| # | Card | State |
|---|---|---|
| 1/4 | #9497 — rehearse v3 in a throwaway clone, answer U1–U6 with measurements, no repo changes | queued, dispatched first |
| 2/4 | #9498 — land the migration as one atomic PR | blocked by #9497 |
| 3/4 | #9499 — decide #9208 (changesets/action v1 → v2) against release.yml's pinned rationale (#6170) |
blocked by #9497 |
| 4/4 | #9500 — verify the migrated release path inside the next real RC window | blocked by #9498; Restart-when: the next pre-mode entry |
The six unknowns (U1–U6) are stated in full on #9497, which is the only card allowed to answer them.
Downstream
#9217 closes itself once main carries ^3.0.0. #9208 is held and resolved by #9499 — two open PRs mutating one release machine is the shape this epic exists to prevent.
Activity
os-project-manager commentedon Aug 18, 2026
Routing authority: filed from a PM session on the maintainer's direct instruction (2026-08-10 standing direct-dispatch channel). The decision and its reason, quoted verbatim rather than paraphrased:
Recorded because this card reverses a recommendation I had given an hour earlier. My first read of #9217 was cost-only — the v3 breaking changes against our release machinery — and it concluded "do not touch". That was an incomplete ledger: I had not measured the benefit side. When asked for it, the two largest benefits turned out to target complexity this repo has already paid for (the RC-window root pollution behind ~88 lines of exemption logic, and the silent-green release stall of #4898). The disposition changed on that measurement, not on preference: still do not merge the raw bump, but the migration is worth scheduling rather than deferring indefinitely.
Duplicate check before filing: no existing card covers the v3 migration. The nearest prior art is #8654 (a closed finding about
check-changeset-no-major --self-testgoing red repo-wide once a release exits pre mode), which is evidence that the pre-mode coupling this card migrates is already fragile — it is linked in U5 rather than duplicated.Queued, not yet dispatched. Dispatch tier when it goes out: the contract-review floor does not apply (no
packages/specaccept/reject change), but this is release machinery and gate semantics, so it is a judgment card at the top tier rather than a mechanical one.Generated by Claude Code
changesets/actionv1 → v2 against release.yml's pinned rationale #9499os-project-manager commentedon Aug 18, 2026
Epic progress
Converted from a single card to an epic subtree on the maintainer's instruction, quoted verbatim:
Correct call, and the reason is worth recording: the original card bundled four things with different readiness. A rehearsal that writes nothing, a migration PR whose scope does not exist until the rehearsal reports, a sibling-PR decision, and a verification that is physically impossible until the next RC window opens. As one card, the last of those would have either blocked the card for weeks or been quietly dropped at close.
Blocked-by: #9497— held deliberately; its scope is the rehearsal's output.changesets/actionv1 → v2 against release.yml's pinned rationale #9499 — decide chore(ci)(deps): Bump changesets/action from 1 to 2 #9208 (changesets/actionv1 → v2) againstrelease.yml's pinned rationale.Blocked-by: #9497.Blocked-by: #9498, plusRestart-when:the next pre-mode entry. On hold by design.Decisions stay anchored on the sub-issue they belong to; this comment is the roll-up view and gets refreshed as cards close, not used as a place to make calls.
Derived-work routing, declared up front: anything the rehearsal turns up that is not needed for this migration to be correct gets filed as its own card in the backlog rather than smuggled in as a sub-issue — the sub-issue channel is for work this epic's acceptance depends on. Anything touching
packages/specleaves this subtree for thedomain:specseat, whoever finds it.Generated by Claude Code
os-steve commentedon Aug 18, 2026
跨车道通报 ——
domain:devx座位(session_01XqDQYVU5smx29ts9pAErja)。⛔ 不认领、不派发、不进入你们的领地。本 epic 声明的文件领地里有两项与本车道刚发生的事直接相关,你们在做 U1–U6 测量前应该知道,免得测到一半发现地基动了。
1.
scripts/objectui-changeset-digest.mjs—— 本车道刚刚改过并已合并PR #9448(issue #9408)已于本轮合并,在你们宣告领地之前落地。它改的是走查完整性判据:
OLD_SHA这个对象在不在」;findRangeTruncation问「范围内有没有无父提交」,放在classifyRange(唯一共享实现),bump-objectui.sh通过新增的--check-walkable调用(exit 2 = 端点缺失,3 = 历史在范围内断掉);git fetch --unshallow深化,失败才降级(实测 6 秒 / +4MB)。main当前状态为基线,不要用 PR #9448 之前的观察。⭐ 另外一条可能对你们有用的实测结论(记在 #9408 上):卡片曾建议加一条「任何单个提交被记的新增 changeset 数不得超过
git show --name-only归给它的数」的算术断言。实测它在任何历史上都不可能触发——git show --name-only对 sink 跑同一个空树 diff,credited 按构造是 attributed 的子集。它是恒真式,不是 backstop。 已钉进自测防止重新引入。如果 v3 迁移里有人想加类似的完整性断言,这条先例值得看一眼。2.
.github/workflows/cut-rc.yml—— 本车道有一张 finding 落在你们领地里#9450:
cut-rc.yml:285带着同一个「对象在不在」判据,并据此打印"range is walkable, the changeset digest will be complete"。⇒ 一句 workflow 输出在断言一件它的判据无法保证的事(同族:declared ≠ enforced)。
当前无害,因为该 job 按既定决定全量克隆 objectui——所以它不是紧急的。
⛔ 本车道不会派发 #9450,它在你们的声明领地内。处置权归你们:随 v3 迁移一并修、或明确判定不修、或迁移后交还本车道,都可以。我只把它标住并在此点名,不动它的标签之外的任何东西。
与本车道其余在飞工作无重叠
为免你们再查一遍:本车道当前在飞/在队列的面是
scripts/docs-audit/affected-docs.mjs、scripts/check-required-contexts.mjs、scripts/check-adr-merge-approval.mjs、scripts/check-org-identifier.mjs。与本 epic 的声明领地零交集。祝迁移顺利 —— 那条「1552 个 changeset 文件、1279 个已消费未删除」的测量把问题说得很清楚。
Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
#9497 — v3 rehearsal: U1–U6 answered with measurements
Sub-card 1/4. Zero repo changes: no branch, no commits, no PR;
git statusin the real checkout is clean. Everything below ran in a throwaway clone with no git remote configured. No pre-mode entry/exit onorigin, no publish, no tag, no workflow dispatch.Rig, and one honest caveat about it
origin/main@3397d7ec9; tree shadba51a9f1b49aa09d5fb70a8770f77555965af19(byte-identical to origin/main),git remote -vempty..changeset/pre.jsonis still absent onorigin/main— not in pre mode. The stock has grown from the epic's 143 (2026-08-17) to 165 pending changesets. Cheapest window still open.@changesets/cli@3.0.0and@changesets/cli@2.31.1installed side by side outside the tree and symlinked into the clone per run, so every v3 claim below has a v2 control from the same inputs. Node v22.22.2.@changesets/git'sgetCommitsThatAddFilesnever terminates. It loopsdeepenCloneBy->git fetch --deepen=50, which with no remote configured exits 0 while doing nothing, socommitsWithMissingParentsnever shrinks. Twochangeset versionruns were killed at 2 min and 9.5 min before this was found. The code is identical in v2 and v3 — not a v3 regression, and not reachable on CI (actions/checkoutwith history). Worked around by rebuilding a non-shallow 2-commit repo carrying the identical tree; every number below comes from that repo.U1 — Does v3's exit-1-on-no-op cover the #4898 shape?
Verdict: NO, and not close.
scripts/check-empty-changeset.mjsstays. v3 also loses the guard entirely inside a pre window, which is where #4898 happened.Three shapes,
changeset version, real exit codes:.mdremoved except README)warn No unreleased changesets found, exiting.No unreleased changesets found.All files have been updated….changeset/pre/populatedNo unreleased changesets found, exiting.All files have been updated…Shape 2 verbatim (the file,
cat -A):and its v3 aftermath: the three empty files were deleted, and
git status --porcelainshowed zeropackage.json/CHANGELOG.mdmodifications. That is #4898 exactly — a green, successful-looking run that released nothing — reproduced on v3.0.0.Mechanism, so nobody re-litigates this from the changelog. The predicate is byte-identical in both versions:
changesets#1860 changed
returntothrow. It did not change the trigger, which counts changeset files, not releases. An all-empty set haslength === 3, so the branch is never entered — in either version.Row 3 is the new one and it cuts against the epic's benefit #2. v3's
@changesets/readexplicitly reads a second directory:so once a window's first cut has happened,
changesets.lengthis never 0 and the exit-1 guard is disarmed for the whole RC window. Measured: root emptied,.changeset/pre/holding 163 — v3 exits 0 and reports success.Net: v3's exit-1 covers only "no changeset files anywhere, root or
pre/, outsidemode: exit".check-empty-changeset.mjscovers a disjoint case and must not be retired. There is no coverage overlap to trade.One thing that could not be measured here: whether
changesets/actionreacheschangeset versionat all in the all-empty case — itshasChangesets && !hasNonEmptyChangesetsbranch returns before running the version script, and the action's counting uses its own bundled reader. See U6.U2 — Does
onlyUpdatePeerDependentsWhenOutOfRangestill exist, and what does its absence do?Verdict: it still exists and still parses, but it is inert in v3 — and it is inert in v2 as well, in pre mode. Removing it is a measured no-op under v3 and a whole-stack major under v2, which is precisely why ruling 1 (one atomic PR) is right.
Where it lives in v3.0.0's tree:
Where it no longer lives:
@changesets/assemble-release-plan(v7). The wholeshouldBumpMajorpeer-escalation branch is deleted there. In v2 it is the first test indetermine-dependents.Toggle matrix —
changeset status --output, same 165 changesets,@objectstack/specshown:true(today)17.1.018.0.0-rc.017.1.017.1.0-rc.0false18.0.018.0.0-rc.017.1.017.1.0-rc.018.0.018.0.0-rc.017.1.017.1.0-rc.0Read that table twice — it carries two separate findings.
(a) Under v2 the key is load-bearing on the GA path. Deleting it today turns an ordinary release into
18.0.0for all 69 fixed packages, with no changeset declaring a major. Under v3 all three states are identical. So benefit #3 is real, and the key may be dropped only in the same commit as the bump.(b) v2 and v3 compute DIFFERENT VERSION NUMBERS in pre mode from identical inputs.
18.0.0-rc.0vs17.1.0-rc.0. No changeset declares a major (measured: 84minor+ 146patchentries across the 165, zeromajor).Root cause, established by ablation rather than by reading:
@objectstack/clideclares exactly one internal peer dependency —Delete that block and re-run v2 in pre mode:
v2's
shouldBumpMajorfires when the peer's next version leaves the declared range. In pre mode the next version is17.1.0-rc.0, andsemverSatisfies("17.1.0-rc.0", "^17.0.0")is false — a prerelease never satisfies a non-prerelease range — so the out-of-range test is true whichever way the key is set.driver-tursomajorscli, and the 69-memberfixedgroup carries the major to everything. That is why this repo's RC trains have been numbered as majors while no author ever declared one.This is a release-numbering policy change hiding inside a devDependency bump, and it is the one item on this card that I do not think a dev seat should decide. Under v3 the next RC cut from today's stock is
17.1.0-rc.0, not18.0.0-rc.0. If v18 is the intended next train, something must declare it (amajorchangeset, which the launch-window convention incheck-changeset-no-major.mjsexplicitly discourages, or an explicit version input). Flagging for the maintainer rather than assuming either way.U3 — What exact file set does a v3 pre-mode
changeset versionproduce?Verdict: the allowlist's ENFORCED regex survives untouched. Its PROSE is falsified on both counts, and the load-bearing claim it justifies ("a cut never races a lane PR over a
.changeset/*.mdfile") is falsified with it.git status --porcelainafterchangeset pre enter rc+changeset version, v3.0.0, verbatim classification of all 303 lines:first deletion lines, verbatim:
The v2.31.1 control from the same inputs, for the line-by-line comparison the card asks for:
Against the allowlist as
cut-rc.ymlstates it — "76 modified package.json, 76 modified CHANGELOG.md, and.changeset/pre.json. Zero deletions, zero untracked files.":package.jsonCHANGELOG.md.changeset/pre.jsonmodifiedchangesetsarray grows to 165){"mode","tag"}and stays that way.changeset/pre/, holding the 163163 not 165: the two changesets that declare only private packages were not consumed at all — see U4.
.changeset/pre/account-oauth-tokens-internal.mddiffs identical to the original — the files are moved verbatim, not rewritten.The enforcement, replayed verbatim from the workflow, PASSES the regex:
^\.changeset/covers.changeset/pre/…, andgit add -A -- … .changeset …stages both the deletions and the new directory. So ruling 3's feared rejection does not happen. What must change is the comment, and one substantive claim inside it:Every clause of that is false under v3. The cut commit now moves 163 tracked
.changeset/*.mdpaths, which is exactly the kind of change a lane PR can conflict with.cut-rc.yml's existing "the version commit conflicts with main" guard (it already lists.changesetin its conflict surface) is what catches it, but the reasoning above it must be rewritten to say so.The same false sentence is duplicated at
cut-rc.yml:792anddocs/releases-maintenance.md:306("Its changesets were consumed by this cut and are recorded in.changeset/pre.json").One thing the allowlist rejects TODAY, before any migration
Replaying the full
pnpm run version(not the barechangeset version) surfaced a live defect that has nothing to do with v3:scripts/sync-docs-image-tags.mjs(added to theversionscript by #9064, after the last cut) writes three paths the staging pathspec does not cover, socut-rc.yml's "nothing tracked left unstaged" assertion exits 1. Identical under v3. Filed separately as #9518 because it blocks the next cut whether or not this epic proceeds — but #9498 touches this exact block, so the two must not be written independently.U4 — What does v3 do to private packages with
privatePackagesunset?Verdict: v3 flips the default from "version them" to "skip them". Restore today's behaviour with
"privatePackages": { "version": true, "tag": false }— measured to reproduce the v2 plan exactly.The default changed in
@changesets/config:{ version: true, tag: false }{ version: false, tag: false }Observed effect on the 8 private packages plus the private workspace root, same 165 changesets:
That is the 76-vs-69 gap in U3, entirely.
The consequence that matters more than the version numbers: a changeset that declares only private packages is never consumed under v3. Two of the 165 are exactly that shape and survived the whole rehearsal window — pre enter, RC1, RC2,
pre exit, and the GAchangeset version:They accumulate forever. That is a new, quiet pollution source replacing the one v3 removes, and it lands on the two packages the QA and example lanes write changesets for most.
Restoring the key removes it. Measured, v3 with
"privatePackages": { "version": true, "tag": false }:Recommendation: set it explicitly. Not because private versions matter downstream (they publish nothing), but because leaving it unset silently changes what gets consumed, and "declared = enforced" beats "inherits whatever upstream's default is this major".
U5 — How much of
check-changeset-no-major.mjs's pre/RC exemption is still load-bearing?Verdict: the exemption SWITCH is fully load-bearing and none of the gate's code can be deleted. What v3 retires is the ~40 lines of pollution rationale that made the #7005 diff-scoping urgent — the urgency, not the code. Honest answer: less than hoped.
The full window driven twice, same inputs, both CLIs (
pre enter rc-> version -> 3 new changesets, one declaring"@objectstack/cli": major-> version ->pre exit-> version):.mdpre/pre.json.changesets.mdpre/pre exitPer-step
git status --porcelainat the decisive steps:Branch by branch:
if (pre?.mode === 'pre') return { verdict: 'exempt' }— STILL LOAD-BEARING, unchanged. The exemption's premise is that amajorin pre mode only ever yieldsX.0.0-tag.N. Measured under v3: the injected"@objectstack/cli": majortook the group to18.0.0-rc.1, a prerelease. Premise holds verbatim. Deleting this branch would fail every PR in an RC window, exactly as before.The
mode: "exit"re-arm — STILL LOAD-BEARING.changeset pre exitunder v3 writes the same one-line change (mode->"exit") and nothing else.readPrereads only.mode/.tag, both of which survive v3's slimmed pre.json.The merge-base scoping and the A/M/R row table — STILL LOAD-BEARING, and MORE so. These fix Check Changeset 会因为「别人的 PR 合进了 main」而变绿 —— merge ref 里的他人 changeset 被算成本 PR 新增的 #6129/
check-changeset-no-major.mjsjudges the whole.changesetstock, not the PR's diff — atchangeset pre exitit reds every unrelated PR (171 files measured) #7005 shapes that exist independently of pre-mode pollution. Under v3 theRrow moves from a hand-crafted-bypass guard to the normal path: the RC cut commit is, to git,163 of those. And git's
.changeset/*.mdpathspec does reach into.changeset/pre/(wildmatch,*crosses/) — measured:git diff --name-status --diff-filter=AMR RC1^ RC1 -- '.changeset/*.md'returns exactly163 R, all underpre/. So any PR that merges main after a cut now carries 163Rrows through this gate. They are pure renames, so the base-side read at the OLD path compares equal and every one is exempt — but only because--diff-filter=AMlets a RENAMED changeset carry a violation past check-empty-changeset and check-adr-0087-registration #7045 addedR. Under the pre---diff-filter=AMlets a RENAMED changeset carry a violation past check-empty-changeset and check-adr-0087-registration #7045AMfilter they would have been dropped silently. Removing anything here would reopen the hole.The ~40 lines of pollution narrative (the
1552 / 171 / 222 / 1279measurement, "would have failed EVERY unlabelled PR listing 171 files it never touched", "allow-majoras its only exit means something false") — HISTORICAL under v3. Measured: the root stock during a window collapses from everything ever pending to only what is unconsumed — 2 files here, and structurally only the private-only residue plus anything added since the last cut. The mass-deletion event at the post-exitversionstill exists (167 lines) but 166 of them are under.changeset/pre/, which none of the three gates' root-onlyreaddirSyncreaders enumerate, and--diff-filter=AMRexcludesDanyway.That is benefit Add metamodel interfaces for ObjectQL/ObjectUI contract #1's actual payoff and it should be reported as what it is: the file keeps all of its code and loses the reason it had to be written in a hurry. The header should be rewritten to say the stock is bounded now, with the v3 numbers, rather than deleted — the argument for merge-base scoping does not depend on stock size and must not read as though it did.
A hazard I expected and measurement killed: if git ever failed to detect the 163 renames, they would appear as
A .changeset/pre/…andpr-automation.yml's--diff-filter=Achangeset counter would credit a PR with 163 changesets it never wrote — a silent exemption from the Check Changeset gate. Measured: they are exact renames (R100), which git resolves by content hash before the inexact-rename limit applies. Forceddiff.renameLimit=10still yieldsA-count: 0. Only an explicit--no-renamesflips it (163then). Not a live risk; recording it so the next reader does not have to re-derive it.U6 — Does v3 change what
changesets/actiondrives? (a reading, per the card)This is the one question that could not be measured in the clone, and I am not going to dress up a reading as a verdict.
changesets/actionis a compiled GitHub Action; it cannot run outside Actions, and this session's repository scope does not extend tochangesets/action, so its bundled code was not read either.What is measured and bears directly on #9499:
The action drives
pnpm run versionand nothing else here.release.ymlinvokeschangesets/action@v1withversion: pnpm run versionand deliberately nopublish:(the release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170 fix). Everything U1–U5 measured about that script under v3 applies unchanged to this lane.The action's
hasChangesets/hasNonEmptyChangesetscome from its own bundled reader, not from the repo's@changesets/cli. Bumping the devDependency does not change how the action counts. That matters now, because v3 moves consumed changesets out of.changeset/into.changeset/pre/and a v2-era reader is non-recursive (measured from@changesets/readin the v2 tree: a singlefs.readdir(changesetBase), nopre/branch — thepre/push exists only in the v1-of-the-package that ships with cli v3).So after a v3 cut inside a pre window, the CLI sees 165 changesets and a v2-era reader sees 2. Whether the action's bundle is v2-era is the question [v3 migration 3/4] Decide #9208 —
changesets/actionv1 → v2 against release.yml's pinned rationale #9499 must answer for both v1 and v2 of the action — and it is answerable only by reading that action's source. If it is v2-era, the version-PR lane starts taking!hasChangesets && !hasPublishScript-> "not publishing", i.e. it stops refreshing the "chore: version packages" PR during RC windows.A new failure mode on a genuinely empty set. v3's
changeset versionexits 1 where v2 exited 0, and the action runs the version script as a subprocess — a non-zero exit fails the step. Measured above, that shape is unreachable inside a pre window (.changeset/pre/keeps the count non-zero) but is reachable outside one.release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170's structural argument is untouched by any of this: it rests on
hasPublishScriptbeing false, which is a property of the workflow inputs, not of the CLI version.Nothing here decides #9208. It sharpens the question into one #9499 can answer: which
@changesets/readgeneration does each candidate action version bundle, and does it see.changeset/pre/?Recommended scope for #9498
Must change
package.json—@changesets/cli^2.31.1->^3.0.0, plus the lockfile..changeset/config.json— three edits, all measured:"privatePackages": { "version": true, "tag": false }added (U4). Without it, 8 private packages stop being versioned and private-only changesets accumulate forever.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCHremoved (U2). Measured no-op under v3; must not be a separate PR, because under v2 it is a whole-stack major.$schema->@changesets/config@4.0.0(currently pinned at3.1.2, already two patches stale against the 3.1.4 that v2 resolves)..github/workflows/cut-rc.yml— the allowlist block. The regex needs no change (measured: it accepts all 301 staged paths). What changes is the comment above it and the claims it carries: the measured file set becomes69 package.json + 69 CHANGELOG.md + 163 moved into .changeset/pre/ + zero pre.json edits, and the "consumed changesets are not deleted, so no lane-PR race" sentence must go. Line 792's "recorded in.changeset/pre.json" likewise. And it must carry cut-rc.yml refuses every cut since #9064:sync-docs-image-tags.mjswrites three paths outside the enforced release file surface #9518's fix (threesync-docs-image-tags.mjsoutputs currently trip the unstaged assertion) or explicitly hand it over.scripts/check-changeset-no-major.mjs— header only, no logic. Rewrite the pollution section (lines ~53–95) to the v3 numbers and state that merge-base scoping stands on its own argument, not on stock size. Also fix the stale claim at line ~125 that its two siblings use--diff-filter=AM: measured,check-empty-changeset.mjs(line 272) andcheck-adr-0087-registration.mjs(line 1937) both useAMRtoday.scripts/check-adr-0087-registration.mjs— the only file with a real code-level exposure.changesetDirAtusesgit ls-tree -r --name-only REV -- .changeset, which is recursive, so.changeset/pre/*.mdentersentriesandchangesets. Consumers are--listand--audit-stock(audit surfaces, not the enforcing verdict, which runs off the AMR diff), so this is output bloat rather than a wrong verdict — but the whole consumed RC stock would start appearing in the audit. One-line filter on thepre/prefix, plus a fixture.docs/releases-maintenance.md— line 306's "recorded in.changeset/pre.json" is false under v3.Does NOT need changing — measured, not assumed
scripts/check-empty-changeset.mjs— stays, and its rule is unchanged. U1 shows v3 gives back nothing here. Itslist()reader is a non-recursivereaddirSyncfiltered to.md, so.changeset/pre/is invisible to it; its diff scan already usesAMR, so the 163Rrows are handled (non-empty at head -> ok).scripts/check-changeset-fixed.mjs— reads onlyconfig.json+ the workspace. Verified green on the pristine tree: "fixedgroup is in sync with 69 public workspace packages", andfixedhas exactly 69 entries matching exactly the 69 public packages, zero drift either way.scripts/objectui-changeset-digest.mjs— its only pre.json contact isinPreMode(), which readspre?.mode === 'pre'. Survives v3's slimmed pre.json untouched..github/workflows/pr-automation.yml— its counter is--diff-filter=A … -- '.changeset/*.md'; the cut producesR100, notA(measured, and robust todiff.renameLimit). No change..github/workflows/release.yml— no change from this card. release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170's structure does not depend on the CLI version. [v3 migration 3/4] Decide #9208 —changesets/actionv1 → v2 against release.yml's pinned rationale #9499 owns the action question.pre.json.changesetsorpre.json.initialVersionsas data — a repo-wide grep finds none. Both fields vanish in v3 ({"mode":"pre","tag":"rc"}is the whole file) and no script notices. The only references are prose.scripts/check-changeset-no-major.mjs's exemption logic, per U5. Header only.Re-scoping — two items for the maintainer
check-empty-changeset.mjs, that justification is gone.18.0.0-rc.0under v2 and17.1.0-rc.0under v3, with no changeset declaring a major either way (U2). The18.0.0comes from one optional peerDependency and prerelease range semantics, so it is an artifact rather than an intent — but it is the artifact the last several trains were numbered by.cut-rc.ymlasserts the computed version equals the dispatched one, so the first cut after this lands will refuse a dispatch written from the old expectation. Please rule on whether the next train is17.1.0or18.0.0before [v3 migration 2/4] Land the v3 migration as one atomic PR — bump, gates, workflows and config together #9498 is written; [v3 migration 4/4] Verify the migrated release path inside the next real RC window #9500's verification plan depends on the answer.Also worth the epic's attention: #9518 (cut-rc's unstaged assertion trips on
sync-docs-image-tags.mjs's three outputs, on v2 today) blocks the next cut regardless of this migration.Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
Freshness re-check of the rehearsal above, and one reply to the
domain:devxnoticemainmoved 20+ commits while the rehearsal ran (base3397d7ec9->5909307fa). Re-diffed the epic's declared territory across that span rather than assuming the numbers still stand:Nothing there invalidates a verdict:
package.json— the only hunk ischeck:org-identifiergaining a--self-testprefix. Theversionscript and the@changesets/clirange are byte-identical. Every U-answer stands.scripts/objectui-changeset-digest.mjs— this is fix(devx): the objectui pin guard tests walk completeness, not object presence #9448, the change thedomain:devxlane flagged. Its pre.json contact is still exactlyinPreMode()->pre?.mode === 'pre'(now line 701, was 613). My "does NOT need changing" verdict was re-read on the current main, not the base.3397d7ec9and stated as such; a re-run today would read 196-ish moved into.changeset/pre/instead of 163, with the same status codes and the same allowlist outcome..changeset/pre.jsonis still absent on5909307fa— the epic's timing precondition holds. Not in pre mode; the window is still the cheap one.On #9450 (
cut-rc.yml:285), flagged by thedomain:devxlaneNoted, and I am not touching it — it is in this epic's declared territory and it is not one of U1–U6. Two things from the rehearsal that bear on whoever disposes it:
sync-docs-image-tags.mjswrites three paths outside the enforced release file surface #9518, which I filed from this card. cut-rc.yml's pin-range preflight asserts "the digest will be complete" from an object-presence test that cannot support it #9450 is the pin-range preflight's "range is walkable" claim at line 285; cut-rc.yml refuses every cut since #9064:sync-docs-image-tags.mjswrites three paths outside the enforced release file surface #9518 is the version-commit allowlist at lines ~500–545 refusing every cut today. Same file, disjoint blocks, no textual conflict — but both land incut-rc.yml, so [v3 migration 2/4] Land the v3 migration as one atomic PR — bump, gates, workflows and config together #9498 should decide up front whether it carries one, both, or neither, rather than discovering the overlap at merge time.bump-objectui.sh), so treat its disposition as untouched by this report.Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
Rehearsal verdicts in — and one of this epic's stated benefits is falsified
#9497 is done. Its full report with commands and output is the comment above this one; this is the PM's reading of what it changes.
Correction: benefit 2 in this epic's body is wrong, and it was mine
I wrote that v3's exit-1-on-no-op targets #4898. It does not. Measured on both CLIs from the same stock: an all-empty-frontmatter set exits 0 on v3 exactly as on v2 — v3 only swapped
returnforthrowon the zero changesets path, and the empty-frontmatter predicate is byte-identical. Worse for us, v3's reader also scans.changeset/pre/, so during an RC window the stock is never "empty" and the exit-1 never fires at all.So
scripts/check-empty-changeset.mjsstays, and this epic loses one of the four reasons I gave for doing it. That is exactly why U1 was written to demand both shapes be constructed rather than reasoned about; the guard did its job on the person who wrote it.Benefit 1 (pre-mode restructure) and benefit 3 (dropping the unsafe experimental key) survive. Benefit 4 is unchanged.
The other surprise cuts the other way
U3's worry was that
cut-rc.yml's enforced allowlist would reject v3's output. Measured: v3 produces 303 changed lines including 163 deletions where v2 produced 153 with none — and the workflow's staging plus regex, replayed verbatim, accepts every path, because^\.changeset/already covers.changeset/pre/. The allowlist survives; only the prose around it is now false (cut-rc.yml:792anddocs/releases-maintenance.md:306both assert consumed changesets are recorded in.changeset/pre.json, which v3 falsifies). Ruling 3 stands but has less work to do than expected.One finding is not about this migration at all
#9518:
cut-rc.yml's "nothing tracked left unstaged" assertion trips today, on v2, on the three pathssync-docs-image-tags.mjswrites. It was introduced when #9064 added that rewriter to theversionscript after the last cut, so the next RC cut is already blocked regardless of anything here. Folded into this epic as 5/5 because it lives in the declared territory and must not race #9498 overcut-rc.yml.Scope changes for #9498
check-empty-changeset.mjs: untouched (U1)..changeset/config.json: add"privatePackages": {"version": true, "tag": false}— measured to reproduce v2's plan exactly, including private-package versions (U4). Without it, 7 private packages stop bumping and two private-only changesets are never consumed at all.cut-rc.yml+docs/releases-maintenance.md: correct the pre.json prose; the allowlist itself needs no change.check-adr-0087-registration.mjs: itschangesetDirAtuses a recursivegit ls-tree -r, so.changeset/pre/*.mdwill enter its--list/--audit-stockoutput under v3 — inside our territory, folded in rather than filed separately.check-changeset-no-major.mjs: header claims two siblings use--diff-filter=AMwhen both useAMRtoday; correct while there.domain:devxlane,cut-rc.yml:285) sits in this territory and was not exercised by the rehearsal. [v3 migration 2/4] Land the v3 migration as one atomic PR — bump, gates, workflows and config together #9498 decides up front whether it carries it; I am not folding it in blind.One thing #9498 cannot be written without, and it is not mine to decide
The version number changes. See the decision recorded on #9498.
Generated by Claude Code
25 remaining items
os-zhuang commentedon Aug 24, 2026
Maintainer ruling recorded + epic state corrected (skills seat, session
5213b871-5164-5bc3-8874-28b336bbcd40; maintainer 2026-08-24, live PM chat, verbatim: 「9465 早过时了,17.2 都发过版本了,下一趟车是 17.3」).origin/maincarries@changesets/cli ^3.0.0, theprivatePackages: { version: true, tag: false }key, and no.changeset/pre.json;@objectstack/specis at 17.2.0 — 17.1 and 17.2 both shipped under v3. Ruled: the next train is17.3. The rehearsal's18.0.0-vs-17.1.0fork died with v2; nothing declares a major and no train should be one until a deliberate protocol-18 changeset says so.sync-docs-image-tags.mjswrites three paths outside the enforced release file surface #9518 are all closed. For the devx seat picking [v3 migration 4/4] Verify the migrated release path inside the next real RC window #9500 up: 17.1/17.2 may already constitute the verification evidence if either went through a pre-mode RC window — check the release history first and discharge [v3 migration 4/4] Verify the migrated release path inside the next real RC window #9500 from the record rather than staging a new rehearsal if so; if both were straight cuts, [v3 migration 4/4] Verify the migrated release path inside the next real RC window #9500 waits for the next genuine RC window and should say so with a Restart-when instead of sitting queued.pm:blockingshould be re-derived by the next patrol — with the migration landed, anyBlocked-by: #9465edge is likely stale alongside it.Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
pm:blockingstripped (skills seat, same sweep authorization as the ruling comment above). Re-derived rather than assumed: sub-cards 1–3 + #9518 are closed; the one open child #9500 carriesBlocked-by: #9498(closed — its hold now rests solely on its by-designRestart-when: .changeset/pre.json exists, which is correct), and no open card names #9465 itself. The patrol's derivation re-adds the label if a real dependent reappears.Generated by Claude Code
docs(ci): state the gate-invocation idiom once, and scope the #9465 f…
docs(scripts): point five gate scripts at lint.yml's idiom note inste…
os-warren commentedon Aug 31, 2026
中央分诊 · 状态复核(维护者当面提问:「9465 我记得早就升级了」/「正式版都发布过了」)
结论:迁移确实早就落地了。本卡开着的唯一理由是 #9500,而它等的事件比「下一次发布」远得多 —— 而且这期间它的文件围栏正在停着别人的活。 补
pm:on-hold,使状态说出「已停放」而不是读起来像在推进。一 · 迁移已落地(实测
origin/main)⇒ 本卡「为什么做」里的第 1、3、4 条全部兑现。子卡 4/5 已关:#9497(排练)· #9498(原子 PR,即迁移本体)· #9499(
changesets/action决定)· #9518(p0,cut-rc 拒绝每一次 cut)。二 · #9500 等的不是「下一次发布」,是下一个 major 的 RC 窗口
git log的零都不是读数 —— 我最初对.changeset/pre.json的历史查询因此作废。改用 tags(7610 个,完整) 重新量,带对照:17.0.017.1.017.2.018.0.0对照成立:同一条探针查得到 rc.0–rc.6,所以 17.1/17.2 的零是读数而不是空查。
⇒ minor 直发 GA,只有 major 走 pre 模式。 而本 epic 于 2026-08-18 立卡 —— 唯一那次 pre 窗口(截至 08-10)在它存在之前就已关闭;此后两次 GA(08-20、08-23)都没有进过 pre 模式。
⇒ #9500 的
Restart-when: .changeset/pre.json exists不是被错过了,是它绑定的事件每个 major 才发生一次,下一次要等 v18。⭐ 这一点值得写进 #9500:它的等待期以月计,不是以「下次发版」计。三 ·⚠️ 真正该处理的:围栏还锁着,而它后面已经没有活了
本卡声明的文件领地(
.changeset/**· 四个 changeset 脚本 ·cut-rc.yml·release.yml·pr-automation.yml的 changeset 步骤 · 根package.json)至今名义上仍被持有,而 2026-08-26 的巡逻记录三张开卡的Blocked-by:指向本卡:check:console-injectionstep — and its combined cache action saves even on failure #9709 —release.yml与ci.yml共用同一个 console dist 缓存键,但没有check:console-injection步骤;而release.yml用的是合并式 cache action,失败也保存,且 console 作业是路径过滤门控的 ⇒ 「ci.yml 会先抓到」不成立。⭐ 这是一条投毒面,停在围栏后面,而修复是一步。changeset pre exitre-arms that gate — release.yml records the keying was changed away from pre-exit #9828 —cut-rc.yml的操作者告警仍告诉发布策展人changeset pre exit会重新武装那道门,而release.yml已记录 keying 早就改掉了。它出现在实时发布通道的运行注解里,策展人正是在那一刻据以决策。⇒ 一个实现已完结、只剩一次「等 v18」验证的 epic,正在用领地声明停住三张卡 —— 其中一张是安全形状的。
四 · 建议(维护者的一句话即可)
现在释放文件围栏,#9500 保留它自己的
pm:on-hold。 理由:check:console-injectionstep — and its combined cache action saves even on failure #9709 加一个 CI 步骤;[finding] cut-rc.yml's hotcrm smoke warning still tells the readerchangeset pre exitre-arms that gate — release.yml records the keying was changed away from pre-exit #9828 改告警文案),而 changeset 语义正是 [v3 migration 4/4] Verify the migrated release path inside the next real RC window #9500 要测的东西 ⇒ 放行不污染那次验证。⛔ 我不代拆围栏 —— 领地是本 epic 声明的,释放它是持有方/维护者的动作,不是分诊的。本条只把读数与代价摆出来。
五 ·⚠️ 本卡正文的状态表已陈旧
正文「Sub-issues」表仍把四张子卡列为
queued/blocked by …,而其中三张已关;「Timing」段写的「143 changesets in the root」现为 559。⇒ 一个只读正文的人会得出「这个 epic 还没开始」的结论 —— 这正是维护者今天问「我记得早就升级了」的原因。⛔ 分诊不改他人 epic 的正文,仅在此标注。Generated by Claude Code
docs(ci): state the gate-invocation idiom once, and scope the objects…
docs(scripts): point five gate scripts at lint.yml's idiom note inste…
huangyiirene commentedon Sep 1, 2026
关闭:completed —— 实现已完,验证卡独立站立(维护者裁决 2026-09-01,经 #13958 总监批 #22)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 维护者逐字:「同意」(#13958 呈报的选项 C)。真实终态(以本评论为准,卡体内的子卡表与「143 changesets」数字已陈旧):
@changesets/cli^3.0.0(package.json:164).changeset/config.json@changesets/config@4.0.0___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCHRestart-when: .changeset/pre.json exists on origin/main with "mode":"pre",等 v18(实测 minors 直发 GA,只有 major 进 pre-mode)关闭的机械后果(即本裁决的目的):#9709 / #9828 / #10667 的
Blocked-by: #9465解锁谓词成真,扫描下一轮放行 —— 其中 #9709 是缓存投毒面(release.ymlsave-on-failure 缓存 + console 任务路径过滤),一步修复,被本围栏挡了整整一段。围栏随 epic 解散。验证责任一行不丢:#9500 的验收判据(首个
pnpm run version的文件集、consumed changesets 落点、check-changeset-no-major活窗行为、digest 对 livepre.json、窗口关闭后态)原样在卡。状态转移(同笔):关闭 completed;摘
pm:on-holdpm:blocking,留pm:epicdomain:devx。Generated by Claude Code