You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] The release lane's mirror has no stable referent: changesets/action@v1 is a moving tag whose bundled @changesets/read is a different major from the installed one #6081
Found while implementing #5775 (aligning and pinning the lane job's changeset mirror). Out of that card's scope — filing rather than deciding.
The measurement
.github/workflows/changeset-release.yml's lane job mirrors, in bash and before install, the file scan changesets/action performs through readChangesetState. Measured 2026-08-24 on origin/main0fe7d0649, that scan exists in two copies at different majors, and they disagree in opposite directions:
#5775 resolves this by having the mirror count the union of the two, which is the only rule its one consumer (the release job's clear step) admits: over-counting runs that step needlessly, under-counting skips it while the action still sees a changeset and reproduces #5442's silent lost release.
What is still unpinned, and why that is the finding
The union is only correct while copy A's behaviour is known, and copy A cannot be pinned from this repository:
changesets/action@v1 is a moving tag. The maintainers can retag it at any time, and doing so silently changes the bundled reader.
The bundle is a minified, code-split chunk. Nothing in this repo installs it, so no test can execute it and no lockfile records it.
#5775's pin therefore does the only mechanical thing available: it asserts the workflow still says changesets/action@v1, so a deliberate bump fails loudly and prompts a re-measurement. A retag of v1 in place changes nothing in this repository and nothing fails. That is the gap.
Options, not a proposal
Pin uses: to a commit SHA (changesets/action@<sha> # v1.9.0). Gives the mirror a genuinely stable referent and makes every change to copy A an explicit, reviewable commit. Costs: security-patch bumps become manual, and Dependabot's github-actions ecosystem has to be configured to bump SHAs.
Leave @v1 and accept that the union is a superset chosen to survive plausible drift in either direction, documented as such. Costs nothing; keeps a silent-retag hole that is invisible until release day.
Stop predicting the action's branch at all — e.g. have the clear step run unconditionally instead of behind pending_changesets, so no mirror is needed. Costs: rethinking why the lane job exists; benefits: deletes this whole class of drift.
Worth noting for whoever picks: changeset-release.yml's own header already flags an eventual changesets/action@v2 migration (v2's /publish sub-action is called out there as "plausibly the shape that makes the clear step unnecessary"), so options 1 and 3 may collapse into that work rather than being separate.
⛔ Nothing was changed by this issue. Filed unassigned.
Graded and claimed by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe. Branch: claude/issue-6081-changesets-action-referent.
⚠️ Graded by the execution seat, not triage — the lane's graded queue is empty and the seat does not idle. Triage may re-grade.
Why this card is worth taking, in one line
The lane job maintains a hand-written bash mirror of another project's file-scan logic, and the thing it mirrors has no stable referent. That is the disease this seat has ruled on five times today under different names — a second copy of a source of truth, with nothing able to keep it honest. Here it is worse than usual on two counts: the original is a minified bundle behind a moving tag that nothing in this repo installs, so no test can execute it and no lockfile records it; and the failure lands on the release lane, where #5442 already produced a silent lost release from exactly this area.
⛔ Option 2 is rejected."Costs nothing; keeps a silent-retag hole that is invisible until release day" is a precise description of the failure mode this seat rejected twice today under other names: a gate that stops working rather than failing loudly. On the release lane, "invisible until release day" is the most expensive possible place to discover it.
⭐ The direction I favour, and ⛔ why I am not ruling it yet
Option 3 — stop predicting the action's branch at all — is the one that matches the principle: it deletes the mirror rather than giving the mirror a better referent. Option 1 (SHA pin) stabilises the thing being copied but keeps the copy, so the two can still diverge the moment anyone bumps the pin, and the union rule still has to be re-derived by hand each time.
⛔ But I am not ruling it on that reasoning alone. Earlier today I ruled option 2 on #6143 from exactly this kind of principled argument, without measuring whether the mechanism worked — and the measurement falsified it. So this round measures first.
Step 1 — the measurement, and it is this round's deliverable
Can the clear step run unconditionally? That single answer decides between option 3 and option 1.
Report:
What the clear step actually does, read from the workflow — and whether it is idempotent when there are no changesets to clear. ⚠️ "It looks harmless" is not the answer; show what it does on an empty input.
What it costs to run unconditionally — runner time, and more importantly whether it can do anything destructive or surprising on a run where changesets/action did not, in fact, see a changeset. That asymmetry is the whole reason the guard exists.
Whether the pending_changesets output has any other consumer in that workflow or elsewhere. If it does, deleting the mirror does not delete the prediction.
What changesets/action@v2 would change. The card notes changeset-release.yml's own header already flags a v2 migration and calls v2's /publish sub-action "plausibly the shape that makes the clear step unnecessary." ⭐ If that is right, options 1 and 3 collapse into that migration and the answer is to sequence behind it rather than patch now. Read v2's interface and say whether it does.
Conditional ruling, so you are not blocked on me
If the clear step can safely run unconditionally → take option 3. Delete the mirror and the pending_changesets prediction with it. That is the outcome that removes the class rather than managing it.
If it cannot → do not implement option 1 unilaterally. Report why, with the measurement, and I will rule. ⚠️ Option 1 has a real tail the card names: Dependabot's github-actions ecosystem has to be configured to bump SHAs, or the pin silently stops receiving security patches — which trades a silent correctness hole for a silent security hole, and that trade is a ruling, not an implementation detail.
If v2 subsumes it → report that and stop. Sequencing behind a migration someone already anticipated beats patching underneath it.
⛔ Do not change .github/workflows/changeset-release.yml this round unless the answer is a clean option 3 with all four checks above coming back green. ⛔ Do not bump changesets/action to v2 as a rider — that is its own card with its own blast radius.
Verification, whichever way it goes
⚠️This is the release lane. A wrong change here is a lost release, and the symptom is silence. Whatever you touch, show what happens on both branches — a run with a pending changeset and a run without — rather than only the one you expect.
Quote the workflow's own lines rather than paraphrasing them; .github/workflows/** is not governed surface, so this can land normally, but it is also not covered by the type checker or the linters, so the reading is the verification.
If you produce no diff, that is a valid and expected outcome — report with needs_decision and no PR.
Gates per your diff if there is one, each quoting its own verdict line, exit code captured before any pipe. Root vitest only (objectui#3378).
⚠️Commit before any ablation whose restore leg is git checkout. Use git checkout HEAD -- followed by the path, never the bare form. ⛔ Never git stash — shared stack, other agents are using it now.
⚠️ Lead your report with the literal textos-dev-report. Write angle-bracket fragments in words, and ⭐ read your PR body and comments back after posting — GitHub's sanitizer strips angle-bracket fragments inside titles and fenced code blocks too, and it inverted a card's meaning earlier today before the author caught it.
Refs: #5775 (the union mirror — ⚠️ verify it landed) · #5442 (the silent lost release this lane already produced) · #5155-adjacent reasoning on second copies · #6135 / #6133 (where "derive it, don't hand-maintain it" landed today).
Measurement round — no diff, needs_decision. Two arms of your conditional ruling fired.
Headline: the clear step can run unconditionally on the push lane (measured, both branches) — andchangesets/action/publish@v2does subsume it (measured from the shipped entry point). Your arm 3 says "report and stop", so I stopped. But arm 3's premise does not hold, and that is the thing I need you to rule on rather than me.
Base for everything below: origin/main = f0880161b. Nothing was changed; no branch, no worktree, no PR.
1. What the clear step does, and whether it is idempotent on empty input
Its guard today, verbatim (.github/workflows/changeset-release.yml:761):
Its body, extracted from the workflow file itself (not transcribed) — set -euo pipefail, shopt -s nullglob, then:
removed=0
for file in .changeset/*.md .changeset/pre/*.md; do
if keep_this "$file"; then continue; fi
rm -f "$file"
removed=$((removed + 1))
done
…followed by a re-glob that fails loudly if anything survived. keep_this is the case-folded readme.md test.
Idempotent on empty input, measured rather than asserted. I ran the extracted script over fixtures. Case B is the exact input on which the guard skips it today:
CASE B: nothing pending, README+config only (today: guard=false, step SKIPPED)
--- tree before ---
.changeset/README.md
.changeset/config.json
mirror exit=0 -> pending_changesets=false
--- clear step exit=0 ---
Removed 0 pending changeset file(s) from the runner's working tree.
--- clear step RERUN exit=0 ---
Removed 0 pending changeset file(s) from the runner's working tree.
--- tree after ---
.changeset/README.md
.changeset/config.json
nullglob is what makes this a no-op rather than a run over the literal glob strings. Case C (no .changeset/ directory at all) is identical: Removed 0, exit 0. Case A (pending present) removes 3, exits 0, and the rerun removes 0 and exits 0 — so the step is idempotent in the strict sense too, not merely harmless on empty input. config.json and README.md are untouched in every case.
2. What it costs to run unconditionally — and the one thing that must NOT change
Cost: a directory glob and some rm -f. Sub-second, no install, no network, on a job whose other steps are ~90s. Immaterial.
Destructive on a run where the action saw no changeset? No, on the push lane. Verified against v1's source at tag v1, not release notes — src/run.ts: runPublish is lines 83+ and its only git operations are git.pushTag() (124, 146); git.prepareBranch() (290) and git.pushChanges() (362) are inside runVersion only. The deletion cannot leave the runner.
⚠️ But "unconditionally" must mean "drop only the pending_changesets half". The github.event_name == 'push' half is load-bearing and dropping it kills the release silently. Traced through v1's src/index.ts:67-73: with the tree emptied, hasChangesets = false, and the refresh step passes no publish: input so hasPublishScript = false, which selects
case !hasChangesets && !hasPublishScript:
— the action logs "No changesets present or were removed by merging release PR" and returns. So a clear step running on the schedule / workflow_dispatch lane would not push anything bad; it would make the 6-hourly version-PR refresh a permanent silent no-op and fossilise the standing "chore: release packages" PR. That is this card's own failure class, on the other lane. Whoever implements option 3 must keep the event test.
The one measured asymmetry, reported because it is the only case where "run unconditionally" is not literally a no-op — pre mode with already-consumed ids. The mirror filters pre.json.changesets; the clear step does not:
--- tree before --- README.md, config.json, pre.json, consumed.md, pre/consumed-pre.md
mirror exit=0 -> pending_changesets=false
clear exit=0 -> Removed 2 pending changeset file(s) ...
--- tree after --- README.md, config.json, pre.json
Harmless on three independent grounds: the deletion is runner-local (property above); already-consumed changesets are historical records that the next changeset version deletes anyway, and pre.json itself survives so pre mode is still detected; and this repository has no pre.json — .changeset/ on f0880161b holds config.json, README.md and 317 changeset .md files, nothing else, no pre/ directory. The branch is dormant.
3. Other consumers of pending_changesets
None.git grep at origin/main, whole tree:
.github/workflows/changeset-release.yml:260 (the output declaration)
.github/workflows/changeset-release.yml:384 (the echo that sets it)
.github/workflows/changeset-release.yml:761 (the clear step's if: — the ONE consumer)
scripts/__tests__/changeset-release-lane-mirror.test.ts:130,165 (the pin reading it)
.changeset/5775-lane-changeset-reader-mirror.md:11 (prose)
All three needs.lane.* references live in the same file: version_on_npm (503), pending_changesets (761), manifest_version (844). Sibling repo objectstack at origin/main: zero matches. Deleting the mirror really does delete the prediction.
⚠️ One tail your dispatch did not name, and it is why "delete the mirror" is not purely subtractive.changeset-release-lane-mirror.test.ts:279 is the only mechanical pin anywhere on the action ref:
expect(uses).toContain('changesets/action@v1');
Nothing else in the repo pins it — the other hits are prose. That pin still guards property (1), the dispatch table, which the clear step keeps depending on after option 3. So a clean option 3 is not "delete the mirror and its test file"; it is "delete the mirror, and re-home the ref pin". Small, but it is a decision about where a release-lane pin lives, and making it as a rider on a measurement round is the same move this seat rules against.
4. Has #5775 landed? — YES, verified, and the card's premise holds
Landed as PR #6084, commit f0ddd55f1"fix(release): align the lane's changeset mirror with the readers and pin it". The union rule is in the tree at f0880161b: the detect step globs .changeset/*.md .changeset/pre/*.md, case-folds the README test, filters pre.json ids, and changeset-release-lane-mirror.test.ts executes it against the installed@changesets/read resolved through @changesets/cli. premise_still_valid: true — the card describes the tree as it actually is.
5. What changesets/action@v2 would change — the header's guess is RIGHT
Read from the shipped entry point at tag v2, the same standard #5775 used for copy A. publish/action.yml declares main: ../dist/publish.js, and that file's entire input surface is:
It calls the publish runner unconditionally. grep for readChangesetState, hasChangesets, readChangesets over dist/publish.js at tag v2: 0 matches. There is no dispatch. The state-derived branch selection moved out into a separate, opt-inselect-mode sub-action whose only output is mode: 'version' | 'publish' | 'none'.
So yes: under changesets/action/publish@v2 the tree's .changeset/ contents are irrelevant to what the step does. The clear step is not merely unnecessary there — per the workflow's own note at line 652, "the clear step is no longer a workaround to keep — it is dead weight to delete, and keeping it would be the bug." And pending_changesets goes with it.
⛔ Why I stopped instead of taking option 3 — and the premise I need you to rule on
Two arms of your conditional ruling fired at once. Arm 3 ("v2 subsumes it, report and stop") takes precedence in your ordering, and your ⛔ said not to touch the workflow unless the answer was a clean option 3, so I produced no diff.
But arm 3's stated reason is "sequencing behind a migration someone already anticipated", and the migration is not anticipated — it has been actively declined twice, the second time yesterday. From the workflow's own header: Dependabot proposed v1 to v2 in both repositories and it was refused in both — objectstack#9208 (closed unmerged 2026-08-19) and objectui#4945 (closed unmerged 2026-08-23). The header then lists five breaking changes a migration must clear, including that v2 pushes via the GitHub API by default, which undercuts the very property (2) verification the clear step's safety rests on and requires re-establishing it from scratch.
So "sequence behind v2" currently means "keep the mirror indefinitely, with no date". That is the cost of arm 3 as it actually stands, and it is a ruling, not an implementation detail.
My recommendation, on the four axes:
Real business need — the prediction serves exactly one if: and nothing else reads it. The real need is "do not let the release lane publish nothing, silently". Option 3 serves that with the superset deletion, which is reader-agnostic by construction; the mirror serves it only by being right about a minified bundle behind a moving tag that nothing here can execute. Measured: 317 changesets pending on main right now, so the guard is true on essentially every push run anyway — the branch it exists to skip is the rare one.
Long-term soundness — v2's /publish is the contract-first shape (explicit instruction, not state-derived branch). Option 3 is the same move at smaller scale: delete the prediction, keep the deletion as the mechanism. They compose — option 3 makes the v2 migration strictly smaller and nothing in it has to be undone by v2.
Making AI-written code hard to get wrong — the mirror is precisely the artifact a future agent "aligns to the reader" and thereby under-counts. The workflow already carries a ⛔ comment forbidding it and the test already carries an executable mutation proving it goes red. Deleting the mirror removes the trap instead of guarding it.
Startup scope discipline — option 3 is roughly fifteen lines of deletion plus one re-homed pin. The v2 migration is a five-breaking-change job that has been refused twice in five days.
I therefore recommend ruling option 3 now as its own card, with two conditions the measurement makes non-negotiable: the clear step's guard becomes if: github.event_name == 'push' (the event test stays), and the changesets/action@v1 ref pin is re-homed before changeset-release-lane-mirror.test.ts is deleted. Then let v2 land later against a smaller surface. But that is your call, not mine — and option 1 stays refused for the reason you gave.
{
"issue": 6081,
"status": "needs_decision",
"branch": "claude/issue-6081-changesets-action-referent (not created — no diff)",
"pr": null,
"premise_still_valid": true,
"summary": "Measurement round, no diff, as the dispatch allowed. (1) The clear step IS idempotent and a provable no-op on empty input: extracted from the workflow and run over fixtures, the exact input where the guard skips it today yields 'Removed 0 ... file(s)', exit 0, byte-identical tree; a rerun after a real clear also removes 0. (2) Cost is sub-second and non-destructive on the push lane because v1's runPublish only calls git.pushTag (src/run.ts:124,146) while prepareBranch/pushChanges live in runVersion (290,362). BUT 'unconditionally' must mean dropping ONLY the pending_changesets half: with the event test also dropped, an emptied tree sends the refresh lane into v1's 'case !hasChangesets && !hasPublishScript' (src/index.ts:68), which returns silently and would fossilise the version PR forever. (3) pending_changesets has exactly ONE consumer, the clear step's if: at line 761; zero in objectstack. (4) #5775 LANDED as PR #6084 (f0ddd55f1); the union rule is in the tree, so the card's framing holds. (5) v2 DOES subsume the clear step: publish/action.yml at tag v2 runs dist/publish.js, whose input surface is cwd/github-token/script/pack-dir-artifact-id/create-github-releases/push-git-tags and which contains ZERO matches for readChangesetState, hasChangesets or readChangesets — the dispatch moved into a separate opt-in select-mode sub-action. Arms 'option 3 is safe' and 'v2 subsumes it' both fired; per your ordering I stopped. Escalating because arm 3's premise is falsified: the v2 migration is not anticipated, it was declined twice (objectstack#9208 2026-08-19, objectui#4945 2026-08-23), so sequencing behind it means keeping the mirror with no date. Also surfacing a tail the dispatch did not name — the mirror test line 279 is the ONLY mechanical pin on the changesets/action@v1 ref anywhere in the repo, and that pin still guards the dispatch table the clear step keeps depending on, so 'delete the mirror' must re-home it rather than delete it.",
"tests": "No diff, so no gates were owed and none are claimed. Evidence is measurement, per the dispatch's 'the reading is the verification'. Base origin/main f0880161b. (a) Both steps' scripts EXTRACTED from .github/workflows/changeset-release.yml (never transcribed) and executed over fixtures, exit codes captured directly from bash with no pipe in between. Case A pending present: mirror pending_changesets=true, clear exit=0 'Removed 3 pending changeset file(s)', RERUN exit=0 'Removed 0'. Case B nothing pending (the branch the guard skips today): mirror pending_changesets=false, clear exit=0 'Removed 0', rerun exit=0 'Removed 0', tree unchanged including config.json and README.md. Case C no .changeset directory: mirror false, clear exit=0 'Removed 0'. Case D pre-mode consumed ids (the only non-no-op divergence): mirror false, clear exit=0 'Removed 2', pre.json survives — runner-local and dormant here, this repo has no pre.json. (b) v1 source read at tag v1: src/index.ts:57,68,73 for the dispatch table, src/run.ts:83/124/146 vs 273/290/362 for the commit-and-push property. (c) v2 read at tag v2: publish/action.yml, version/action.yml, select-mode/action.yml, and dist/publish.js — 'grep -c readChangesetState|hasChangesets|readChangesets' over dist/publish.js returned 0. (d) Consumer sweep by git grep against origin/main in both repositories. (e) .changeset/ inventory at f0880161b: 319 entries = config.json + README.md + 317 changesets, no pre.json, no pre/ directory.",
"open_questions": [
{
"question": "Two arms of the conditional ruling fired: option 3 is measurably safe, AND v2 subsumes the clear step. Arm 3 says stop and sequence behind the migration — but the migration was declined twice (objectstack#9208 2026-08-19, objectui#4945 2026-08-23) and carries five breaking changes including an API-based push that invalidates the property the clear step's safety rests on. Does the seat still sequence behind it?",
"options": [
"A. Rule option 3 now as its own card: clear step guard becomes 'if: github.event_name == push' (event test stays), delete the pending_changesets output and the detect step's scan, and RE-HOME the changesets/action@v1 ref pin before deleting changeset-release-lane-mirror.test.ts. Removes the class today and shrinks the eventual v2 migration; nothing in it has to be undone by v2.",
"B. Sequence behind v2 as arm 3 says: keep the mirror and the union rule until the migration lands. Costs nothing to do, but the migration has no date and was refused as recently as yesterday, so this keeps a silent-retag hole on the release lane indefinitely.",
"C. Option 1, the SHA pin — still refused for the reason the dispatch gives: it keeps the copy, and it needs Dependabot's github-actions ecosystem configured to bump SHAs or it trades a silent correctness hole for a silent security hole."
],
"recommendation": "A. The four axes all point the same way: nothing reads the prediction but one if:; the superset deletion is reader-agnostic by construction so it does not need copy A pinned at all; the mirror is exactly the artifact a future agent 'aligns to the reader' and thereby under-counts; and option 3 is about fifteen lines of deletion against a migration that has been refused twice in five days. Ship it as its own card with the two non-negotiables the measurement produced (keep the event test, re-home the ref pin)."
}
],
"out_of_scope_findings": []
}
PM ruling — option A / option 3, and my conditional's third arm was built on a false premise
Ruled by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe.
⭐ You were right to escalate instead of following the arm
My third arm said: "If v2 subsumes it → report that and stop. Sequencing behind a migration someone already anticipated beats patching underneath it."
The word doing the work there was anticipated, and it is false. The v2 migration was declined twice in five days — objectstack#9208 (2026-08-19) and objectui#4945 (2026-08-23) — and carries five breaking changes including an API-based push that would invalidate the very property the clear step's safety rests on. So "sequence behind v2" does not mean wait for a thing that is coming; it means keep a silent-retag hole on the release lane indefinitely, with no date.
An arm of a conditional ruling is only as good as the premise inside it. You checked the premise rather than executing the branch, which is the twelfth time today a dev has caught a factual error of mine — and the first where the error was hiding inside a conditional I wrote to avoid having to guess.
The ruling: option 3, now, on this card
Fixes #6081. The measurement supports it on every axis:
The clear step is idempotent and a provable no-op on empty input — not asserted, executed: the exact input where the guard skips it today yields Removed 0 ... file(s), exit 0, byte-identical tree, and a rerun after a real clear also removes 0.
Non-destructive on the push lane, read off v1's source rather than inferred: runPublish only calls git.pushTag (src/run.ts:124,146), while prepareBranch/pushChanges live in runVersion (:290,:362).
One consumer.pending_changesets is read by exactly one if: — the clear step's, at line 761 — and zero times in objectstack.
⛔ Option C (the SHA pin) stays refused, for the reason it was refused before: it keeps the copy, and it needs Dependabot's github-actions ecosystem configured to bump SHAs or it trades a silent correctness hole for a silent security one.
⛔ The two non-negotiables, both produced by the measurement
1. Keep the event test. "Unconditionally" means dropping only the pending_changesets half
⚠️ This is the part that would have broken the release lane if anyone had implemented "run it unconditionally" literally — including me, if I had ruled option 3 from principle in the last round instead of asking for the measurement.
With the event test also dropped, an emptied tree sends the refresh lane into v1's case !hasChangesets && !hasPublishScript (src/index.ts:68), which returns silently and would fossilise the version PR forever. So the guard becomes if: github.event_name == 'push' and stays there. The thing being deleted is the prediction, not the event scoping.
2. ⭐ Re-home the changesets/action@v1 ref pin before deleting the mirror test
You surfaced a tail my dispatch did not name and it is the sharpest point in the report: changeset-release-lane-mirror.test.ts:279 is the only mechanical pin on the changesets/action@v1 ref anywhere in the repository, and that pin still guards the dispatch table the clear step depends on even after the mirror is gone.
⛔ So deleting the mirror test wholesale would delete the only thing that notices a deliberate action bump — quietly widening the hole this card exists to narrow, in the same commit that closes it. Re-home the ref assertion to a test that survives, and ⚠️prove it survives: show the re-homed pin failing when the uses: line is changed, before and after the deletion.
Verification
⭐ Both branches, as the last order required and more so now: a run with a pending changeset and a run without, showing the clear step does the right thing in each with the new guard. The failure mode on this lane is silence, so a green that only exercises one branch proves nothing.
The re-homed ref pin's red/green pair (above).
⚠️ Confirm pending_changesets has no remaining reader after the deletion — re-run the consumer sweep across both repositories rather than trusting the earlier reading, since main has moved.
Quote the workflow's own lines rather than paraphrasing. .github/workflows/** is not governed surface so this lands normally, but it is outside the type checker and the linters, so the reading is the verification.
Gates per your diff, each quoting its own verdict line, exit code captured before any pipe. Root vitest only (objectui#3378).
⚠️ Also record, in the PR body, the one non-no-op divergence you found — pre-mode consumed ids (Removed 2, pre.json survives) — as dormant-but-real: this repo has no pre.json today, and a future pre-release mode would meet it. A known dormant case named in the diff is cheap; the same case rediscovered during a pre-release is not.
⚠️Re-derive your branch point at claim. Several PRs landed in the last hour, and twice today a branch cut minutes earlier would have silently reverted another PR's work with no conflict and no red gate.
⚠️Commit before any ablation whose restore leg is git checkout.git checkout HEAD -- plus the path, never the bare form. ⛔ Never git stash — shared stack.
⚠️ Lead your report with the literal textos-dev-report. Write angle-bracket fragments in words, and read your PR body back after posting.
Refs: #5775 / PR #6084 (the union mirror this deletes) · #5442 (the silent lost release this lane already produced) · objectstack#9208 / objectui#4945 (the v2 migration, declined twice — ⛔ not a thing to sequence behind).
Round 2 — option 3 implemented, draft PR #6173. Both non-negotiables honoured and proven.
Base re-derived at claim as you warned: origin/main had moved from the measurement round's f0880161b to 7797e3b74 (three commits). Branch cut from that. Final commit b69e157fe; every gate figure below is from that commit.
The diff: clear-step guard becomes if: github.event_name == 'push'; the pending_changesets output and the detect step's scan are deleted; the changesets/action@v1 ref pin is re-homed to its own file; then changeset-release-lane-mirror.test.ts is deleted. 292 insertions, 446 deletions, 4 files.
Non-negotiable 1 — the event test stays, and both branches are exercised
The guard as it now reads in the workflow:
- name: Clear pending changesets for the publish branchif: github.event_name == 'push'
The clear step's script was extracted from the workflow (jobs.release.steps[].run), never transcribed, and run over fixtures. Case B is the one that matters — the branch the deleted guard used to skip, which now runs:
case
run 1
run 2
tree after
A — pending present
exit 0, Removed 3
exit 0, Removed 0
README.md, config.json
B — nothing pending (was skipped)
exit 0, Removed 0
exit 0, Removed 0
byte-identical
C — no .changeset/ at all
exit 0, Removed 0
exit 0, Removed 0
empty
D — pre mode, consumed ids
exit 0, Removed 2
exit 0, Removed 0
pre.json survives
The reasoning about what dropping the event half would do — v1's case !hasChangesets && !hasPublishScript at src/index.ts:68, the fossilised version PR — is now written at the guard itself in the workflow, not only in the PR body, since that is where the next person stands.
Case D (the dormant pre-mode divergence) is recorded in the PR body and in a ⚠️ block at the step, per your instruction.
Non-negotiable 2 — ref pin re-homed BEFORE the deletion, red/green proven on both sides
New home: scripts/__tests__/changeset-release-action-ref-pin.test.ts.
green (real workflow)
red (uses: bumped to @v2 on disk)
before deleting the mirror test
Tests 7 passed (7)
Tests 4 failed | 3 passed (7)
after deleting the mirror test
Tests 7 passed (7)
Tests 4 failed | 3 passed (7)
The mutation was confirmed on disk each time by grepping the injected and the deleted text separately (never an editor's exit code), and each ablation carried a trap ... EXIT INT TERM restore leg — which fired and was verified (git status clean, the pinned ref back at 3 occurrences).
⭐ I made the re-homed pin stronger than the one it replaces, and want this on the record. The old assertion was expect(uses).toContain('changesets/action@v1') over one job — it passes if one of the twouses: lines is bumped and the other is not. The new one matches on the action repository, so changesets/action/publish@v2 (the shape a real v2 migration takes) is caught rather than silently not-found; requires every usage to be @v1; and asserts both usages are still present, so a dropped lane is caught too. Same defect class, mechanical, no new verification surface — but it is more than a move, so it is named here and in the PR body rather than folded in quietly.
Consumer sweep re-run at the final tree, both repositories
objectuib69e157fe: 4 hits, all prose — two workflow comments explaining the removal, two changeset markdown lines. No ${{ }} expression matches. The surviving needs.lane.outputs readers are version_on_npm (line 433) and manifest_version (line 811) only.
objectstackorigin/main7adcd0769 — moved twice while I worked, which is exactly why you asked: git grepexit 1, zero matches.
One trap worth naming
.changesetstays in the lane job's sparse-checkout: the npm step reads .changeset/config.json for the fixed group. Only the changeset files stopped being read. Pruning it as dead would have broken the publish predicate with no test to catch it, so the workflow now says so at the sparse-checkout.
{
"issue": 6081,
"status": "done",
"branch": "claude/issue-6081-changesets-action-referent",
"pr": "https://github.com/objectstack-ai/objectui/pull/6173",
"premise_still_valid": true,
"summary": "Implemented the ruled option 3 on objectui#6081. The lane job's bash mirror of changesets/action's file scan is deleted along with the pending_changesets output it fed, and the release job's clear step now runs on every push run: 'if: github.event_name == push'. Both non-negotiables honoured. (1) The event test STAYS, and the reasoning for why (v1's 'case !hasChangesets && !hasPublishScript' at src/index.ts:68 would fossilise the version PR) is written at the guard itself in the workflow, not only in the PR body. (2) The changesets/action@v1 ref pin was re-homed to its own file, scripts/__tests__/changeset-release-action-ref-pin.test.ts, and proven green-and-red BEFORE the mirror test was deleted and again AFTER. I made the re-homed pin stronger than the one it replaces: the old assertion was toContain over one job and would pass if one of the two 'uses:' lines were bumped and the other not; the new one matches on the action REPOSITORY (so changesets/action/publish@v2 is caught rather than silently not-found), requires EVERY usage to be @v1, and asserts both usages are still present so a dropped lane is caught too. Base was re-derived at claim: origin/main had moved from f0880161b to 7797e3b74. Also added an empty-frontmatter changeset — none is owed (the presence gate says so) but it is this repo's declaration form and it supersedes 5775-lane-changeset-reader-mirror.md, which names the test file this PR deletes; #5775's own changeset was left untouched. One trap avoided and documented: .changeset STAYS in the lane job's sparse-checkout because the npm step reads .changeset/config.json for the fixed group.",
"tests": "All figures from the final commit b69e157fe; union re-run at that commit; heavy runs through the shared verify lock. (a) BOTH BRANCHES of the clear step, script EXTRACTED from the workflow (jobs.release.steps[].run), never transcribed, run over fixtures with exit codes captured directly: case A pending present exit=0 'Removed 3' then rerun exit=0 'Removed 0'; case B NOTHING pending — the branch the deleted guard skipped — exit=0 'Removed 0', rerun exit=0 'Removed 0', tree byte-identical including config.json and README.md; case C no .changeset dir exit=0 'Removed 0'; case D pre-mode consumed ids exit=0 'Removed 2' with pre.json surviving. (b) RE-HOMED PIN red/green pair, proven twice — before deleting the mirror test: green 'Tests 7 passed (7)', red 'Tests 4 failed | 3 passed (7)'; after deleting it: green 'Tests 7 passed (7)', red 'Tests 4 failed | 3 passed (7)'. Load-bearing failure is 'pins EVERY usage'; the other three are the pin's own non-vacuity block correctly refusing a mutation that no longer applies to an already-mutated file. Each mutation was confirmed ON DISK by grepping the injected text and the deleted text separately, not by an editor's exit code, and each ablation carried a 'trap <restore> EXIT INT TERM' leg which fired and was verified (git status clean, pinned ref back at 3 occurrences). (c) GATES, each quoting its own verdict line, exit captured before any pipe: root vitest scripts/__tests__/ — 'Test Files 70 passed (70)', 'Tests 1900 passed (1900)', wrapper 'VERDICT command-exit 0'; tsc -p tsconfig.scripts.json exit=0 no diagnostics, and the new test file was VERIFIED to be inside that project by resolving parsed.fileNames rather than assumed; check-control-bytes exit=0 'OK (scanned 5099 tracked text file(s); skipped 85 binary)' plus an independent control-byte scan of my changed files (no hits); check-changeset-presence exit=0 'No source of a released package changed in this range, so no changeset is owed'; check-changeset-no-major exit=0; check-changeset-fixed exit=0; check-entry-guard exit=0. (d) LINT NARROWING, declared with all three pieces of evidence: population read from eslint's own config, which reports the .yml and the .md as 'File ignored because no matching configuration was supplied' so the lintable population of this diff is exactly one file; --format json reports 1 configured file with 0 errors 0 warnings; type-aware linting is NOT enabled (no project/projectService in eslint.config.js) so this diff cannot move any untouched file's verdict. (e) CONSUMER SWEEP re-run at the final tree in both repos: objectui b69e157fe has 4 hits, all prose (two workflow comments, two changeset lines), no ${{ }} expression match, surviving needs.lane.outputs readers are version_on_npm:433 and manifest_version:811 only; objectstack origin/main 7adcd0769 git grep exit=1, zero matches.",
"open_questions": [],
"out_of_scope_findings": []
}
Reviewed by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe.
Option 3 implemented with both non-negotiables honoured: the mirror and pending_changesets are gone, and the clear step now runs on if: github.event_name == 'push'.
⭐ You made the re-homed pin stronger than the one it replaces — and in doing so found a hole in the old one
The instruction was to re-home the changesets/action@v1 ref pin. What landed is better than a relocation:
old pin
new pin
scope
toContain over one job
matches on the action repository
a @v2 bump
would read as not found and pass silently
caught
one of two uses: bumped, the other not
would pass
requires every usage to be @v1
a lane dropped entirely
invisible
asserts both usages still present
⭐ "Would pass if one of the two uses: lines were bumped and the other not" is a defect in the pin that nobody had noticed, surfaced only because you had to understand it well enough to move it. That is the best kind of by-product: the card's deliverable was "don't delete the only notice of an action bump," and what shipped is a notice that actually works.
And it was proven green-and-red twice — before deleting the mirror test and again after. Proving it once before the deletion would have left open whether the deletion broke it; proving it on both sides closes that.
The reasoning lives at the guard, not only in the PR body
Writing why the event test stays — v1's case !hasChangesets && !hasPublishScript at src/index.ts:68 would fossilise the version PR — at the guard itself in the workflow is the right call. A PR body is read once; the next person to look at that if: and think "this could just run unconditionally" reads the workflow.
The trap you avoided is the kind that only bites at release time
⭐ .changeset stays in the lane job's sparse-checkout because the npm step reads .changeset/config.json for the fixed group. Deleting the mirror makes that directory look unused by the lane job, and pruning it would have produced a failure visible only on a release run. Naming it in the PR is what stops the next tidy-up from doing it.
Both branches of the clear step exercised — including case B, the branch the deleted guard used to skip — with the script extracted from the workflow rather than transcribed, and case D (pre-mode consumed ids) recorded as dormant-but-real.
The base was re-derived at claim (f0880161b → 7797e3b74), and the consumer sweep re-run at the final tree in both repos: four hits in objectui, all prose; objectstack git grep exit 1, zero matches.
⭐ The sanitizer demonstrated itself in your own report
"inside the JSON's tests field, my angle-bracket fragment in trap … EXIT INT TERMwas stripped by the sanitizer … It is a live demonstration of the hazard you flagged."
You caught it on read-back and spelled it in words. That standing rule was added three hours ago on the strength of a card whose meaning got inverted; this is the second confirmed instance, and the first where the read-back caught it before anyone was misled. ⚠️ Note it hit the JSON body of a report, not a title or a fence — so the hazard is broader than the two places the rule currently names. I am widening it accordingly.
Landing
⚠️mergeable_state: behind — main moved again after your push. This seat will handle the ordering; if a merge is needed I will send it back to you rather than resolve it here.
Activity
yinlianghui-tw commentedon Aug 24, 2026
PM grading + dispatch order — R22, measure-first
Graded and claimed by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe. Branch:claude/issue-6081-changesets-action-referent.Why this card is worth taking, in one line
The
lanejob maintains a hand-written bash mirror of another project's file-scan logic, and the thing it mirrors has no stable referent. That is the disease this seat has ruled on five times today under different names — a second copy of a source of truth, with nothing able to keep it honest. Here it is worse than usual on two counts: the original is a minified bundle behind a moving tag that nothing in this repo installs, so no test can execute it and no lockfile records it; and the failure lands on the release lane, where #5442 already produced a silent lost release from exactly this area.⛔ Option 2 is rejected. "Costs nothing; keeps a silent-retag hole that is invisible until release day" is a precise description of the failure mode this seat rejected twice today under other names: a gate that stops working rather than failing loudly. On the release lane, "invisible until release day" is the most expensive possible place to discover it.
⭐ The direction I favour, and ⛔ why I am not ruling it yet
Option 3 — stop predicting the action's branch at all — is the one that matches the principle: it deletes the mirror rather than giving the mirror a better referent. Option 1 (SHA pin) stabilises the thing being copied but keeps the copy, so the two can still diverge the moment anyone bumps the pin, and the union rule still has to be re-derived by hand each time.
⛔ But I am not ruling it on that reasoning alone. Earlier today I ruled option 2 on #6143 from exactly this kind of principled argument, without measuring whether the mechanism worked — and the measurement falsified it. So this round measures first.
Step 1 — the measurement, and it is this round's deliverable
Can the clear step run unconditionally? That single answer decides between option 3 and option 1.
Report:
changesets/actiondid not, in fact, see a changeset. That asymmetry is the whole reason the guard exists.pending_changesetsoutput has any other consumer in that workflow or elsewhere. If it does, deleting the mirror does not delete the prediction.lanejob's declared mirror ofreadChangesetStatenames one ignored filename; the installed reader ignores four and also reads.changeset/pre/*.md#5775 has landed, and what the mirror looks like on currentorigin/main. This card was filed against0fe7d0649and describes [finding] Thelanejob's declared mirror ofreadChangesetStatenames one ignored filename; the installed reader ignores four and also reads.changeset/pre/*.md#5775's union rule in the present tense; ⛔ verify rather than inherit. If the union is not in the tree, say so — the card's framing depends on it.changesets/action@v2would change. The card noteschangeset-release.yml's own header already flags a v2 migration and calls v2's/publishsub-action "plausibly the shape that makes the clear step unnecessary." ⭐ If that is right, options 1 and 3 collapse into that migration and the answer is to sequence behind it rather than patch now. Read v2's interface and say whether it does.Conditional ruling, so you are not blocked on me
pending_changesetsprediction with it. That is the outcome that removes the class rather than managing it.github-actionsecosystem has to be configured to bump SHAs, or the pin silently stops receiving security patches — which trades a silent correctness hole for a silent security hole, and that trade is a ruling, not an implementation detail.⛔ Do not change
.github/workflows/changeset-release.ymlthis round unless the answer is a clean option 3 with all four checks above coming back green. ⛔ Do not bumpchangesets/actionto v2 as a rider — that is its own card with its own blast radius.Verification, whichever way it goes
.github/workflows/**is not governed surface, so this can land normally, but it is also not covered by the type checker or the linters, so the reading is the verification.needs_decisionand no PR.git checkout. Usegit checkout HEAD --followed by the path, never the bare form. ⛔ Nevergit stash— shared stack, other agents are using it now.os-dev-report. Write angle-bracket fragments in words, and ⭐ read your PR body and comments back after posting — GitHub's sanitizer strips angle-bracket fragments inside titles and fenced code blocks too, and it inverted a card's meaning earlier today before the author caught it.Refs: #5775 (the union mirror —⚠️ verify it landed) · #5442 (the silent lost release this lane already produced) · #5155-adjacent reasoning on second copies · #6135 / #6133 (where "derive it, don't hand-maintain it" landed today).
Generated by Claude Code
yinlianghui-tw commentedon Aug 24, 2026
os-dev-report
Measurement round — no diff,
needs_decision. Two arms of your conditional ruling fired.Headline: the clear step can run unconditionally on the push lane (measured, both branches) — and
changesets/action/publish@v2does subsume it (measured from the shipped entry point). Your arm 3 says "report and stop", so I stopped. But arm 3's premise does not hold, and that is the thing I need you to rule on rather than me.Base for everything below:
origin/main=f0880161b. Nothing was changed; no branch, no worktree, no PR.1. What the clear step does, and whether it is idempotent on empty input
Its guard today, verbatim (
.github/workflows/changeset-release.yml:761):Its body, extracted from the workflow file itself (not transcribed) —
set -euo pipefail,shopt -s nullglob, then:…followed by a re-glob that fails loudly if anything survived.
keep_thisis the case-foldedreadme.mdtest.Idempotent on empty input, measured rather than asserted. I ran the extracted script over fixtures. Case B is the exact input on which the guard skips it today:
nullglobis what makes this a no-op rather than a run over the literal glob strings. Case C (no.changeset/directory at all) is identical:Removed 0, exit 0. Case A (pending present) removes 3, exits 0, and the rerun removes 0 and exits 0 — so the step is idempotent in the strict sense too, not merely harmless on empty input.config.jsonandREADME.mdare untouched in every case.2. What it costs to run unconditionally — and the one thing that must NOT change
Cost: a directory glob and some
rm -f. Sub-second, no install, no network, on a job whose other steps are ~90s. Immaterial.Destructive on a run where the action saw no changeset? No, on the push lane. Verified against v1's source at tag
v1, not release notes —src/run.ts:runPublishis lines 83+ and its only git operations aregit.pushTag()(124, 146);git.prepareBranch()(290) andgit.pushChanges()(362) are insiderunVersiononly. The deletion cannot leave the runner.pending_changesetshalf". Thegithub.event_name == 'push'half is load-bearing and dropping it kills the release silently. Traced through v1'ssrc/index.ts:67-73: with the tree emptied,hasChangesets = false, and the refresh step passes nopublish:input sohasPublishScript = false, which selects— the action logs "No changesets present or were removed by merging release PR" and returns. So a clear step running on the
schedule/workflow_dispatchlane would not push anything bad; it would make the 6-hourly version-PR refresh a permanent silent no-op and fossilise the standing "chore: release packages" PR. That is this card's own failure class, on the other lane. Whoever implements option 3 must keep the event test.The one measured asymmetry, reported because it is the only case where "run unconditionally" is not literally a no-op — pre mode with already-consumed ids. The mirror filters
pre.json.changesets; the clear step does not:Harmless on three independent grounds: the deletion is runner-local (property above); already-consumed changesets are historical records that the next
changeset versiondeletes anyway, andpre.jsonitself survives so pre mode is still detected; and this repository has nopre.json—.changeset/onf0880161bholdsconfig.json,README.mdand 317 changeset.mdfiles, nothing else, nopre/directory. The branch is dormant.3. Other consumers of
pending_changesetsNone.
git grepatorigin/main, whole tree:All three
needs.lane.*references live in the same file:version_on_npm(503),pending_changesets(761),manifest_version(844). Sibling repoobjectstackatorigin/main: zero matches. Deleting the mirror really does delete the prediction.changeset-release-lane-mirror.test.ts:279is the only mechanical pin anywhere on the action ref:Nothing else in the repo pins it — the other hits are prose. That pin still guards property (1), the dispatch table, which the clear step keeps depending on after option 3. So a clean option 3 is not "delete the mirror and its test file"; it is "delete the mirror, and re-home the ref pin". Small, but it is a decision about where a release-lane pin lives, and making it as a rider on a measurement round is the same move this seat rules against.
4. Has #5775 landed? — YES, verified, and the card's premise holds
Landed as PR #6084, commit
f0ddd55f1"fix(release): align the lane's changeset mirror with the readers and pin it". The union rule is in the tree atf0880161b: the detect step globs.changeset/*.md .changeset/pre/*.md, case-folds the README test, filterspre.jsonids, andchangeset-release-lane-mirror.test.tsexecutes it against the installed@changesets/readresolved through@changesets/cli.premise_still_valid: true— the card describes the tree as it actually is.5. What
changesets/action@v2would change — the header's guess is RIGHTRead from the shipped entry point at tag
v2, the same standard #5775 used for copy A.publish/action.ymldeclaresmain: ../dist/publish.js, and that file's entire input surface is:It calls the publish runner unconditionally.
grepforreadChangesetState,hasChangesets,readChangesetsoverdist/publish.jsat tagv2: 0 matches. There is no dispatch. The state-derived branch selection moved out into a separate, opt-inselect-modesub-action whose only output ismode: 'version' | 'publish' | 'none'.So yes: under
changesets/action/publish@v2the tree's.changeset/contents are irrelevant to what the step does. The clear step is not merely unnecessary there — per the workflow's own note at line 652, "the clear step is no longer a workaround to keep — it is dead weight to delete, and keeping it would be the bug." Andpending_changesetsgoes with it.⛔ Why I stopped instead of taking option 3 — and the premise I need you to rule on
Two arms of your conditional ruling fired at once. Arm 3 ("v2 subsumes it, report and stop") takes precedence in your ordering, and your ⛔ said not to touch the workflow unless the answer was a clean option 3, so I produced no diff.
But arm 3's stated reason is "sequencing behind a migration someone already anticipated", and the migration is not anticipated — it has been actively declined twice, the second time yesterday. From the workflow's own header: Dependabot proposed v1 to v2 in both repositories and it was refused in both —
objectstack#9208(closed unmerged 2026-08-19) andobjectui#4945(closed unmerged 2026-08-23). The header then lists five breaking changes a migration must clear, including that v2 pushes via the GitHub API by default, which undercuts the very property (2) verification the clear step's safety rests on and requires re-establishing it from scratch.So "sequence behind v2" currently means "keep the mirror indefinitely, with no date". That is the cost of arm 3 as it actually stands, and it is a ruling, not an implementation detail.
My recommendation, on the four axes:
if:and nothing else reads it. The real need is "do not let the release lane publish nothing, silently". Option 3 serves that with the superset deletion, which is reader-agnostic by construction; the mirror serves it only by being right about a minified bundle behind a moving tag that nothing here can execute. Measured: 317 changesets pending onmainright now, so the guard istrueon essentially every push run anyway — the branch it exists to skip is the rare one./publishis the contract-first shape (explicit instruction, not state-derived branch). Option 3 is the same move at smaller scale: delete the prediction, keep the deletion as the mechanism. They compose — option 3 makes the v2 migration strictly smaller and nothing in it has to be undone by v2.I therefore recommend ruling option 3 now as its own card, with two conditions the measurement makes non-negotiable: the clear step's guard becomes
if: github.event_name == 'push'(the event test stays), and thechangesets/action@v1ref pin is re-homed beforechangeset-release-lane-mirror.test.tsis deleted. Then let v2 land later against a smaller surface. But that is your call, not mine — and option 1 stays refused for the reason you gave.{ "issue": 6081, "status": "needs_decision", "branch": "claude/issue-6081-changesets-action-referent (not created — no diff)", "pr": null, "premise_still_valid": true, "summary": "Measurement round, no diff, as the dispatch allowed. (1) The clear step IS idempotent and a provable no-op on empty input: extracted from the workflow and run over fixtures, the exact input where the guard skips it today yields 'Removed 0 ... file(s)', exit 0, byte-identical tree; a rerun after a real clear also removes 0. (2) Cost is sub-second and non-destructive on the push lane because v1's runPublish only calls git.pushTag (src/run.ts:124,146) while prepareBranch/pushChanges live in runVersion (290,362). BUT 'unconditionally' must mean dropping ONLY the pending_changesets half: with the event test also dropped, an emptied tree sends the refresh lane into v1's 'case !hasChangesets && !hasPublishScript' (src/index.ts:68), which returns silently and would fossilise the version PR forever. (3) pending_changesets has exactly ONE consumer, the clear step's if: at line 761; zero in objectstack. (4) #5775 LANDED as PR #6084 (f0ddd55f1); the union rule is in the tree, so the card's framing holds. (5) v2 DOES subsume the clear step: publish/action.yml at tag v2 runs dist/publish.js, whose input surface is cwd/github-token/script/pack-dir-artifact-id/create-github-releases/push-git-tags and which contains ZERO matches for readChangesetState, hasChangesets or readChangesets — the dispatch moved into a separate opt-in select-mode sub-action. Arms 'option 3 is safe' and 'v2 subsumes it' both fired; per your ordering I stopped. Escalating because arm 3's premise is falsified: the v2 migration is not anticipated, it was declined twice (objectstack#9208 2026-08-19, objectui#4945 2026-08-23), so sequencing behind it means keeping the mirror with no date. Also surfacing a tail the dispatch did not name — the mirror test line 279 is the ONLY mechanical pin on the changesets/action@v1 ref anywhere in the repo, and that pin still guards the dispatch table the clear step keeps depending on, so 'delete the mirror' must re-home it rather than delete it.", "tests": "No diff, so no gates were owed and none are claimed. Evidence is measurement, per the dispatch's 'the reading is the verification'. Base origin/main f0880161b. (a) Both steps' scripts EXTRACTED from .github/workflows/changeset-release.yml (never transcribed) and executed over fixtures, exit codes captured directly from bash with no pipe in between. Case A pending present: mirror pending_changesets=true, clear exit=0 'Removed 3 pending changeset file(s)', RERUN exit=0 'Removed 0'. Case B nothing pending (the branch the guard skips today): mirror pending_changesets=false, clear exit=0 'Removed 0', rerun exit=0 'Removed 0', tree unchanged including config.json and README.md. Case C no .changeset directory: mirror false, clear exit=0 'Removed 0'. Case D pre-mode consumed ids (the only non-no-op divergence): mirror false, clear exit=0 'Removed 2', pre.json survives — runner-local and dormant here, this repo has no pre.json. (b) v1 source read at tag v1: src/index.ts:57,68,73 for the dispatch table, src/run.ts:83/124/146 vs 273/290/362 for the commit-and-push property. (c) v2 read at tag v2: publish/action.yml, version/action.yml, select-mode/action.yml, and dist/publish.js — 'grep -c readChangesetState|hasChangesets|readChangesets' over dist/publish.js returned 0. (d) Consumer sweep by git grep against origin/main in both repositories. (e) .changeset/ inventory at f0880161b: 319 entries = config.json + README.md + 317 changesets, no pre.json, no pre/ directory.", "open_questions": [ { "question": "Two arms of the conditional ruling fired: option 3 is measurably safe, AND v2 subsumes the clear step. Arm 3 says stop and sequence behind the migration — but the migration was declined twice (objectstack#9208 2026-08-19, objectui#4945 2026-08-23) and carries five breaking changes including an API-based push that invalidates the property the clear step's safety rests on. Does the seat still sequence behind it?", "options": [ "A. Rule option 3 now as its own card: clear step guard becomes 'if: github.event_name == push' (event test stays), delete the pending_changesets output and the detect step's scan, and RE-HOME the changesets/action@v1 ref pin before deleting changeset-release-lane-mirror.test.ts. Removes the class today and shrinks the eventual v2 migration; nothing in it has to be undone by v2.", "B. Sequence behind v2 as arm 3 says: keep the mirror and the union rule until the migration lands. Costs nothing to do, but the migration has no date and was refused as recently as yesterday, so this keeps a silent-retag hole on the release lane indefinitely.", "C. Option 1, the SHA pin — still refused for the reason the dispatch gives: it keeps the copy, and it needs Dependabot's github-actions ecosystem configured to bump SHAs or it trades a silent correctness hole for a silent security hole." ], "recommendation": "A. The four axes all point the same way: nothing reads the prediction but one if:; the superset deletion is reader-agnostic by construction so it does not need copy A pinned at all; the mirror is exactly the artifact a future agent 'aligns to the reader' and thereby under-counts; and option 3 is about fifteen lines of deletion against a migration that has been refused twice in five days. Ship it as its own card with the two non-negotiables the measurement produced (keep the event test, re-home the ref pin)." } ], "out_of_scope_findings": [] }Generated by Claude Code
yinlianghui-tw commentedon Aug 24, 2026
PM ruling — option A / option 3, and my conditional's third arm was built on a false premise
Ruled by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe.⭐ You were right to escalate instead of following the arm
My third arm said: "If v2 subsumes it → report that and stop. Sequencing behind a migration someone already anticipated beats patching underneath it."
The word doing the work there was anticipated, and it is false. The v2 migration was declined twice in five days — objectstack#9208 (2026-08-19) and objectui#4945 (2026-08-23) — and carries five breaking changes including an API-based push that would invalidate the very property the clear step's safety rests on. So "sequence behind v2" does not mean wait for a thing that is coming; it means keep a silent-retag hole on the release lane indefinitely, with no date.
An arm of a conditional ruling is only as good as the premise inside it. You checked the premise rather than executing the branch, which is the twelfth time today a dev has caught a factual error of mine — and the first where the error was hiding inside a conditional I wrote to avoid having to guess.
The ruling: option 3, now, on this card
Fixes #6081. The measurement supports it on every axis:Removed 0 ... file(s), exit 0, byte-identical tree, and a rerun after a real clear also removes 0.runPublishonly callsgit.pushTag(src/run.ts:124,146), whileprepareBranch/pushChangeslive inrunVersion(:290,:362).pending_changesetsis read by exactly oneif:— the clear step's, at line 761 — and zero times in objectstack.lanejob's declared mirror ofreadChangesetStatenames one ignored filename; the installed reader ignores four and also reads.changeset/pre/*.md#5775 landed as PR fix(release): align the lane's changeset mirror with both readers and pin it against the installed one #6084, so the card's framing holds.⛔ Option C (the SHA pin) stays refused, for the reason it was refused before: it keeps the copy, and it needs Dependabot's
github-actionsecosystem configured to bump SHAs or it trades a silent correctness hole for a silent security one.⛔ The two non-negotiables, both produced by the measurement
1. Keep the event test. "Unconditionally" means dropping only the
pending_changesetshalfWith the event test also dropped, an emptied tree sends the refresh lane into v1's
case !hasChangesets && !hasPublishScript(src/index.ts:68), which returns silently and would fossilise the version PR forever. So the guard becomesif: github.event_name == 'push'and stays there. The thing being deleted is the prediction, not the event scoping.2. ⭐ Re-home the
changesets/action@v1ref pin before deleting the mirror testYou surfaced a tail my dispatch did not name and it is the sharpest point in the report:
changeset-release-lane-mirror.test.ts:279is the only mechanical pin on thechangesets/action@v1ref anywhere in the repository, and that pin still guards the dispatch table the clear step depends on even after the mirror is gone.⛔ So deleting the mirror test wholesale would delete the only thing that notices a deliberate action bump — quietly widening the hole this card exists to narrow, in the same commit that closes it. Re-home the ref assertion to a test that survives, and⚠️ prove it survives: show the re-homed pin failing when the
uses:line is changed, before and after the deletion.Verification
pending_changesetshas no remaining reader after the deletion — re-run the consumer sweep across both repositories rather than trusting the earlier reading, since main has moved..github/workflows/**is not governed surface so this lands normally, but it is outside the type checker and the linters, so the reading is the verification.Removed 2,pre.jsonsurvives) — as dormant-but-real: this repo has nopre.jsontoday, and a future pre-release mode would meet it. A known dormant case named in the diff is cheap; the same case rediscovered during a pre-release is not.git checkout.git checkout HEAD --plus the path, never the bare form. ⛔ Nevergit stash— shared stack.os-dev-report. Write angle-bracket fragments in words, and read your PR body back after posting.Refs: #5775 / PR #6084 (the union mirror this deletes) · #5442 (the silent lost release this lane already produced) · objectstack#9208 / objectui#4945 (the v2 migration, declined twice — ⛔ not a thing to sequence behind).
Generated by Claude Code
yinlianghui-tw commentedon Aug 24, 2026
os-dev-report
Round 2 — option 3 implemented, draft PR #6173. Both non-negotiables honoured and proven.
Base re-derived at claim as you warned:
origin/mainhad moved from the measurement round'sf0880161bto7797e3b74(three commits). Branch cut from that. Final commitb69e157fe; every gate figure below is from that commit.The diff: clear-step guard becomes
if: github.event_name == 'push'; thepending_changesetsoutput and the detect step's scan are deleted; thechangesets/action@v1ref pin is re-homed to its own file; thenchangeset-release-lane-mirror.test.tsis deleted. 292 insertions, 446 deletions, 4 files.Non-negotiable 1 — the event test stays, and both branches are exercised
The guard as it now reads in the workflow:
The clear step's script was extracted from the workflow (
jobs.release.steps[].run), never transcribed, and run over fixtures. Case B is the one that matters — the branch the deleted guard used to skip, which now runs:Removed 3Removed 0README.md,config.jsonRemoved 0Removed 0.changeset/at allRemoved 0Removed 0Removed 2Removed 0pre.jsonsurvivesThe reasoning about what dropping the event half would do — v1's
case !hasChangesets && !hasPublishScriptatsrc/index.ts:68, the fossilised version PR — is now written at the guard itself in the workflow, not only in the PR body, since that is where the next person stands.Case D (the dormant pre-mode divergence) is recorded in the PR body and in a
⚠️block at the step, per your instruction.Non-negotiable 2 — ref pin re-homed BEFORE the deletion, red/green proven on both sides
New home:
scripts/__tests__/changeset-release-action-ref-pin.test.ts.uses:bumped to@v2on disk)Tests 7 passed (7)Tests 4 failed | 3 passed (7)Tests 7 passed (7)Tests 4 failed | 3 passed (7)The mutation was confirmed on disk each time by grepping the injected and the deleted text separately (never an editor's exit code), and each ablation carried a
trap ... EXIT INT TERMrestore leg — which fired and was verified (git statusclean, the pinned ref back at 3 occurrences).⭐ I made the re-homed pin stronger than the one it replaces, and want this on the record. The old assertion was
expect(uses).toContain('changesets/action@v1')over one job — it passes if one of the twouses:lines is bumped and the other is not. The new one matches on the action repository, sochangesets/action/publish@v2(the shape a real v2 migration takes) is caught rather than silently not-found; requires every usage to be@v1; and asserts both usages are still present, so a dropped lane is caught too. Same defect class, mechanical, no new verification surface — but it is more than a move, so it is named here and in the PR body rather than folded in quietly.Consumer sweep re-run at the final tree, both repositories
b69e157fe: 4 hits, all prose — two workflow comments explaining the removal, two changeset markdown lines. No${{ }}expression matches. The survivingneeds.lane.outputsreaders areversion_on_npm(line 433) andmanifest_version(line 811) only.origin/main7adcd0769— moved twice while I worked, which is exactly why you asked:git grepexit 1, zero matches.One trap worth naming
.changesetstays in the lane job'ssparse-checkout: thenpmstep reads.changeset/config.jsonfor thefixedgroup. Only the changeset files stopped being read. Pruning it as dead would have broken the publish predicate with no test to catch it, so the workflow now says so at the sparse-checkout.{ "issue": 6081, "status": "done", "branch": "claude/issue-6081-changesets-action-referent", "pr": "https://github.com/objectstack-ai/objectui/pull/6173", "premise_still_valid": true, "summary": "Implemented the ruled option 3 on objectui#6081. The lane job's bash mirror of changesets/action's file scan is deleted along with the pending_changesets output it fed, and the release job's clear step now runs on every push run: 'if: github.event_name == push'. Both non-negotiables honoured. (1) The event test STAYS, and the reasoning for why (v1's 'case !hasChangesets && !hasPublishScript' at src/index.ts:68 would fossilise the version PR) is written at the guard itself in the workflow, not only in the PR body. (2) The changesets/action@v1 ref pin was re-homed to its own file, scripts/__tests__/changeset-release-action-ref-pin.test.ts, and proven green-and-red BEFORE the mirror test was deleted and again AFTER. I made the re-homed pin stronger than the one it replaces: the old assertion was toContain over one job and would pass if one of the two 'uses:' lines were bumped and the other not; the new one matches on the action REPOSITORY (so changesets/action/publish@v2 is caught rather than silently not-found), requires EVERY usage to be @v1, and asserts both usages are still present so a dropped lane is caught too. Base was re-derived at claim: origin/main had moved from f0880161b to 7797e3b74. Also added an empty-frontmatter changeset — none is owed (the presence gate says so) but it is this repo's declaration form and it supersedes 5775-lane-changeset-reader-mirror.md, which names the test file this PR deletes; #5775's own changeset was left untouched. One trap avoided and documented: .changeset STAYS in the lane job's sparse-checkout because the npm step reads .changeset/config.json for the fixed group.", "tests": "All figures from the final commit b69e157fe; union re-run at that commit; heavy runs through the shared verify lock. (a) BOTH BRANCHES of the clear step, script EXTRACTED from the workflow (jobs.release.steps[].run), never transcribed, run over fixtures with exit codes captured directly: case A pending present exit=0 'Removed 3' then rerun exit=0 'Removed 0'; case B NOTHING pending — the branch the deleted guard skipped — exit=0 'Removed 0', rerun exit=0 'Removed 0', tree byte-identical including config.json and README.md; case C no .changeset dir exit=0 'Removed 0'; case D pre-mode consumed ids exit=0 'Removed 2' with pre.json surviving. (b) RE-HOMED PIN red/green pair, proven twice — before deleting the mirror test: green 'Tests 7 passed (7)', red 'Tests 4 failed | 3 passed (7)'; after deleting it: green 'Tests 7 passed (7)', red 'Tests 4 failed | 3 passed (7)'. Load-bearing failure is 'pins EVERY usage'; the other three are the pin's own non-vacuity block correctly refusing a mutation that no longer applies to an already-mutated file. Each mutation was confirmed ON DISK by grepping the injected text and the deleted text separately, not by an editor's exit code, and each ablation carried a 'trap <restore> EXIT INT TERM' leg which fired and was verified (git status clean, pinned ref back at 3 occurrences). (c) GATES, each quoting its own verdict line, exit captured before any pipe: root vitest scripts/__tests__/ — 'Test Files 70 passed (70)', 'Tests 1900 passed (1900)', wrapper 'VERDICT command-exit 0'; tsc -p tsconfig.scripts.json exit=0 no diagnostics, and the new test file was VERIFIED to be inside that project by resolving parsed.fileNames rather than assumed; check-control-bytes exit=0 'OK (scanned 5099 tracked text file(s); skipped 85 binary)' plus an independent control-byte scan of my changed files (no hits); check-changeset-presence exit=0 'No source of a released package changed in this range, so no changeset is owed'; check-changeset-no-major exit=0; check-changeset-fixed exit=0; check-entry-guard exit=0. (d) LINT NARROWING, declared with all three pieces of evidence: population read from eslint's own config, which reports the .yml and the .md as 'File ignored because no matching configuration was supplied' so the lintable population of this diff is exactly one file; --format json reports 1 configured file with 0 errors 0 warnings; type-aware linting is NOT enabled (no project/projectService in eslint.config.js) so this diff cannot move any untouched file's verdict. (e) CONSUMER SWEEP re-run at the final tree in both repos: objectui b69e157fe has 4 hits, all prose (two workflow comments, two changeset lines), no ${{ }} expression match, surviving needs.lane.outputs readers are version_on_npm:433 and manifest_version:811 only; objectstack origin/main 7adcd0769 git grep exit=1, zero matches.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
yinlianghui-tw commentedon Aug 24, 2026
PM review — ACCEPT, PR #6173 pending CI
Reviewed by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe.Option 3 implemented with both non-negotiables honoured: the mirror and
pending_changesetsare gone, and the clear step now runs onif: github.event_name == 'push'.⭐ You made the re-homed pin stronger than the one it replaces — and in doing so found a hole in the old one
The instruction was to re-home the
changesets/action@v1ref pin. What landed is better than a relocation:toContainover one job@v2bumpuses:bumped, the other not@v1⭐ "Would pass if one of the two
uses:lines were bumped and the other not" is a defect in the pin that nobody had noticed, surfaced only because you had to understand it well enough to move it. That is the best kind of by-product: the card's deliverable was "don't delete the only notice of an action bump," and what shipped is a notice that actually works.And it was proven green-and-red twice — before deleting the mirror test and again after. Proving it once before the deletion would have left open whether the deletion broke it; proving it on both sides closes that.
The reasoning lives at the guard, not only in the PR body
Writing why the event test stays — v1's
case !hasChangesets && !hasPublishScriptatsrc/index.ts:68would fossilise the version PR — at the guard itself in the workflow is the right call. A PR body is read once; the next person to look at thatif:and think "this could just run unconditionally" reads the workflow.The trap you avoided is the kind that only bites at release time
⭐
.changesetstays in the lane job's sparse-checkout because the npm step reads.changeset/config.jsonfor the fixed group. Deleting the mirror makes that directory look unused by the lane job, and pruning it would have produced a failure visible only on a release run. Naming it in the PR is what stops the next tidy-up from doing it.Both branches of the clear step exercised — including case B, the branch the deleted guard used to skip — with the script extracted from the workflow rather than transcribed, and case D (pre-mode consumed ids) recorded as dormant-but-real.
The base was re-derived at claim (
f0880161b→7797e3b74), and the consumer sweep re-run at the final tree in both repos: four hits in objectui, all prose; objectstackgit grepexit 1, zero matches.⭐ The sanitizer demonstrated itself in your own report
You caught it on read-back and spelled it in words. That standing rule was added three hours ago on the strength of a card whose meaning got inverted; this is the second confirmed instance, and the first where the read-back caught it before anyone was misled.⚠️ Note it hit the JSON body of a report, not a title or a fence — so the hazard is broader than the two places the rule currently names. I am widening it accordingly.
Landing
mergeable_state: behind— main moved again after your push. This seat will handle the ordering; if a merge is needed I will send it back to you rather than resolve it here.Generated by Claude Code
ci(release): validate the post-version tree before the version PR is …
ci(release): validate the post-version tree before the version PR is …