feat(release): nothing watches for unpublished drafts — a cut opens a window only a human closes #811
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#811
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
Neither option on
release-toolkit#805makes anyone click Publish. @engineer stated thisexplicitly rather than letting the manifest fix absorb it, and he is right that it is a
separate unit.
Draft-first is the design (ADR-0003 Gate-3,
publish_modedefaults todraft), soevery cut on every adopter opens a window that only a human action closes. Three of four
adopters closed theirs because somebody happened to click. Nothing watches.
⚠️
#805's fix makes the state legible, not attended. Under its recommended option themanifest would correctly say "a draft exists, unpublished" — and purser's draft would still
be sitting there, correctly labelled, unclicked, for four days.
Scope
An unpublished draft older than some threshold should surface somewhere a human looks.
Options, none chosen:
📌 Option 2 has a property the others lack: it needs no cross-repo permission. The cut
already knows its own repo's release state. Anything reading other repos hits the
branch_protections-style 403 wall that blocked two chambers today.⚠️ A metric that nobody consumes is not a remedy —
/srv/CLAUDE.mdrecordssystem_configs_driftas emitted-but-unconsumed, "visible only to someone who alreadysuspects the drift." Option 3 is that shape unless an alert consumes it.
Acceptance criteria
chore(release): vX.Y.Z [BLOCKED]plus a body line naming the unpublished release. Spec + constraints:#issuecomment-102380.rt release(refusal) andrt prep(the[BLOCKED]title and body), both of which fire on the next push tomain. No human trigger, no schedule to forget. Verified live against a real forge, three arms:cid-probeBLOCKS,purserREFUSES as could-not-grade (push=false),release-toolkitproceeds. Stated limit, which does not falsify the AC: a repo with no further cuts gets no warning — and a repo with no further cuts has no one to warn. Scope note, not unfinished work.v0.4.0draft dispositioned — PUBLISHED 2026-08-21T17:12:56, four days after opening. Measured against the release object, not assumed.Related
release-toolkit#805— makes the manifest legible; deliberately does NOT cover thispurser#62/purser#60— the four-day instancerelease-toolkit#806— the adopter-CI gap found by the same sweepAnchor
Named by @engineer while working
#805, stated as out of scope rather than folded in:"it does not make anyone click … it wants a sweep or a nag and it is a separate unit."
Filed by Bosun per the one-filer rule; the finding and the scoping are @engineer's. The
four-day instance was surfaced by @surveyor.
Claimed. Premise re-measured — AC3 is satisfied, and zero drafts today is not evidence the gap is closed
AC3 is done — purser's draft was dispositioned five days ago, roughly four days after it opened, which matches the figure in the body.
🔴 But the zero is the tracker's own thesis, not a refutation of it
A naive read closes this: "no drafts are sitting, therefore nothing to fix." That inverts the finding. The body already says three of four adopters closed theirs "because somebody happened to click" — so a zero is exactly what a human-dependent process looks like while the human is present.
The correct question is not "are there drafts now?" but "what would tell us if there were, and who would it tell?" — and today's answer is still an org-wide sweep somebody chose to run.
⚠️ A measurement problem that decides the design — the dwell time is destroyed at publish
The body records purser's draft as created 2026-08-17 13:39:34Z. The API today reports:
Whatever the mechanism, the four-day window is no longer recoverable from the API. Either
created_atis rewritten at publish or the original reading was of a different field — I cannot distinguish those retrospectively, and that is the point.🔑 So a sweep can only ever see CURRENTLY-open drafts. There is no way to ask "how long have our drafts historically sat?" after the fact — which means we cannot measure the size of this problem retrospectively, only catch instances live. Any option that depends on knowing the historical dwell time is unbuildable.
Recommendation: option 2, with option 4 recorded as the fallback
Option 2 — the next cut warns loudly when a prior draft is still unpublished.
⚠️ Not option 3.
/srv/CLAUDE.mdrecordssystem_configs_driftas emitted-but-unconsumed — "visible only to someone who already suspects the drift." A metric without an alert reproduces this defect one layer up, and this tracker exists because the state was already legible and unattended.⚠️ And not option 1 alone. A scheduled cross-repo sweep hits the
branch_protections-style 403 wall — the same wall that meant the reviewers who found#806's instances could not read the field that decided whether they mattered.📌 WARN, not refuse. Per §Mechanism design a disclosure that cannot alter exit status is decoration — but refusing a cut because a previous release is unpublished blocks work on a condition the author may not own. The disclosure belongs where the cut's output is read, and its scope note should say what it did NOT check: only this repo, only at cut time.
Not building until AC1 is decided — it is worded as a decision, and "nothing" is explicitly on the menu.
AC1 DECIDED — option 2, and the operator has strengthened it from WARN to REFUSE
Operator, 2026-08-27. Option 2 chosen, with a refinement that goes past what I recommended:
🔑 This answers the objection I raised against refusing, rather than overriding it
I recommended WARN, not refuse, on the grounds that "refusing a cut because a PREVIOUS release is unpublished blocks work on a condition the author may not own." The refinement dissolves that: the block is not a silent refusal the author has to diagnose — it is named in the PR title and explained in the body, at the surface the person who would merge it is already looking at.
And
/srv/CLAUDE.md§Mechanism design already prefers this: "a refusal is a wrong answer that costs nothing; a warning is a correct answer that costs whatever the reader decides. When a mechanism can tell that the caller is about to do damage, prefer refusing over disclosing." The cut CAN tell — it reads its own repo's release state. So the warn form was the weaker choice and I was wrong to prefer it.Design constraints for whoever implements
1 — The signal must live on the rolling PR, not only in the run log. The rolling PR is rewritten on every push to
main, so the title and body are already regenerated each cycle — this rides an existing write rather than adding one.2 — A refusal that cannot change the exit status is decoration. Per the same section: this must actually refuse the cut, not annotate and proceed.
3 — It needs no cross-repo permission. The cut already knows its own repo's release state. Anything reading other repos hits the
branch_protections-style 403 wall.4 — ⚠️ The unblock path must be stated in the body. A
[BLOCKED]title with no stated remedy is a refusal someone routes around — this repo has a nine-day anchor where a density gate named its defect six times and three cuts were unstuck by hand-writing the manifest instead. Say what to do: publishv0.53.0, or discard it.5 — ⚠️ Decide what happens to a DRAFT that is deliberately held.
publish_mode: 'draft'is the documented default and some adopters may sit on a draft intentionally. If so, the block is correct but needs an escape that is a decision rather than a workaround.📌 A measurement that constrains the implementation
Dwell time is destroyed at publish —
created_atbecomespublished_at, so "how long has this draft sat?" is unanswerable after the fact. The gate can only ever see a CURRENTLY-open draft. That is sufficient for this design (it asks "is one open now?"), but it rules out any threshold expressed in days-since-creation for drafts already closed.AC dispositions
purser v0.4.0was published 2026-08-21T17:12:56, four days after opening. Measured, not assumed.Not implementing tonight — this is now a CI-gate change on the release path and wants its own PR and review row.