forked from heavy-duty/ceremony
984 lines
59 KiB
Markdown
984 lines
59 KiB
Markdown
# Changelog
|
||
|
||
The curated history of the ceremony itself. Each release's section is
|
||
published verbatim as that release's body (lib/changelog.sh extracts it),
|
||
so entries say what changed, cite the issue, and stop — at most 300
|
||
characters each, guard-enforced on the PR that writes the fragment (#167);
|
||
a genuinely long change ships several short entries, never one long one.
|
||
The citation is guard-enforced too, and it closes the entry: one `(#N)`
|
||
group, then the final `.` and nothing after it (#262). Sections published
|
||
before that rule keep their prose; the guard reads fragments only.
|
||
Entries arrive as fragments — one `changelog.d/<issue>.md` per PR, never
|
||
an edit to this file — and the release PR assembles them into the next
|
||
section here (`bin/changelog-assemble`, #112).
|
||
|
||
This tree is `heavy-duty/ceremony` on
|
||
`forgejo.heavyduty.builders`, and it tracks the upstream tree's version
|
||
numbers (#197 D2). Two trees therefore answer to the same number, differing
|
||
by the forge-compatibility delta. **This tree carries upstream through
|
||
`8c3a4d1`** (upstream `0.6.0`, merged by #198); the `0.4.1` section below is
|
||
this forge's own release, not upstream's, and upstream `0.6.1` through `0.6.3`
|
||
were adopted by port rather than merge. Upstream's separate `0.4.1` section
|
||
is deliberately not carried — the tag published here is the one this section
|
||
is the body of. Each sync updates this line (docs/UPSTREAM-SYNC.md, #200).
|
||
|
||
## 0.6.2 — 2026-08-24
|
||
|
||
### Changed
|
||
|
||
- `upstream-0.6.1` was ported in #229: CONTRIBUTING routes the vendored set
|
||
through `docs/VENDORED.txt` — upstream#316 and upstream#311; BUILDER.md
|
||
scopes the green-check precondition to its act, upstream#330; RELEASES.md
|
||
adds the post-merge gate-member split, upstream#329 (#246).
|
||
|
||
- `upstream-0.6.2` was ported in #229: BUILDER.md orders parked claims so an
|
||
operator-owned remainder parks the claim, never the handoff — upstream#336
|
||
(#246).
|
||
|
||
- `upstream-0.6.3` was ported in #230: release windows read `## Members` with
|
||
no gate fallback, carriers leave their own gates, stale board flags stay
|
||
silent — upstream#343 and upstream#327 — and the parser accepts CommonMark
|
||
rows (#246).
|
||
|
||
- Upstream logic was ported onto this forge's Forgejo-adapted issue-flow
|
||
reconciler, test, and CONTRIBUTING; those files were never overwritten with
|
||
upstream bytes (#246).
|
||
|
||
- Upstream's drill-record fixes and the upstream `0.7.0`–`0.7.4` line are
|
||
deferred to the next sync campaign (#246).
|
||
|
||
- No upstream ancestry moves in this release: `.upstream-ref` remains
|
||
`8c3a4d1` from upstream `0.6.0`, merged by #198; `upstream-0.6.3` is the
|
||
content baseline, not a merge-base (#246).
|
||
- Release windows now read membership from a dedicated `## Members` record, with CommonMark-bounded rows and no fallback to predecessor gates (#230).
|
||
- Forge consumers now receive the upstream 0.6.1 and 0.6.2 doctrine for vendored-set routing, review-round signalling, operator-owned remainders, producer liveness, and post-merge release edges (#229).
|
||
- `docs/RUNNER-PROBES.md` records the delivered 0.6.1 consumer exercise in the standing Forgejo runner venue (#217).
|
||
|
||
### Fixed
|
||
|
||
- Forgejo drafts and fast-forward conflict-check windows no longer surface as merge conflicts when the API has not distinguished one (#236).
|
||
- Forgejo review requests no longer count as verdicts, while its blocking and comment states now grade like their GitHub equivalents (#235).
|
||
|
||
## 0.6.1 — 2026-08-09
|
||
|
||
### Added
|
||
|
||
- `test/labels-bootstrap.test.sh` pins the bridge at every hop: the declared
|
||
boundary, both gate sites as the identity, no expression reading
|
||
`github.event.inputs`, and the caller and stub pass-throughs
|
||
byte-exact (#215).
|
||
|
||
- The same test drives the four value paths — schedule-empty, `no`, `yes`,
|
||
invalid — through the shipped expressions into the action's real
|
||
validator (#215).
|
||
|
||
- The taxonomy bootstrap keys on the `BOOTSTRAP` input, never the event name.
|
||
It tested `GITHUB_EVENT_NAME = workflow_dispatch` — correct while an
|
||
operator's manual dispatch was the only dispatch there was, inert-by-
|
||
construction from #209 on, when every machine wake became a dispatch
|
||
event (#215).
|
||
|
||
- The venue drill caught that: with the bridge delivering `no` perfectly,
|
||
drill runs 16/17 still bootstrapped, because the script never read the
|
||
input the whole chain existed to deliver (#215).
|
||
|
||
- `test/labels-reconcile.test.sh` pins the regression pair exactly: a
|
||
`workflow_dispatch` event with `BOOTSTRAP=no` (or unset) creates and
|
||
deletes nothing; only `BOOTSTRAP=yes` bootstraps (#215).
|
||
- A gather-level case drives the real board read against a Forgejo-shaped
|
||
fixture — every entry carrying the key. The existing discriminator cases
|
||
assert `jq` expressions in isolation and passed throughout this regression
|
||
(#210).
|
||
|
||
- A source pin forbids `has("pull_request")` on this surface, because the rule
|
||
was stated in a comment and violated forty lines below it. It strips comments,
|
||
so the #188 warning that explains the trap is allowed to stay (#210).
|
||
|
||
- All three sites are covered behaviourally, not only by the pin: the board
|
||
gather, the release-body gather through an observable window flag, and the
|
||
per-issue payload check (#210).
|
||
- `test/forge-backends.test.sh` pins each backend's path **and** field, because
|
||
a stubbed `forge_api` cannot catch a wrong path — which is how this shipped
|
||
and why a live sweep was what found it (#209).
|
||
- `test/labels-dispatch.test.sh` extracts the shipped step and executes it
|
||
against a recording stub, asserting the method, endpoint, ref and
|
||
`inputs.bootstrap` actually sent (#205).
|
||
|
||
- That test also drives the failure path: any non-204 still fails the job, so
|
||
the misconfiguration alarm the trigger exists to be cannot decay into a
|
||
warning (#205).
|
||
|
||
- An unset `GITHUB_API_URL` refuses before any request instead of defaulting
|
||
to `api.github.com`. Guessing sent this forge's dispatch to GitHub and
|
||
reported success — the "Never 'probably github'" rule, and the same
|
||
unset-environment refusal #201 established for docs-sync (#205).
|
||
|
||
- A dispatch that never reaches the forge names the failure. Letting `set -e`
|
||
carry curl's exit code out did fail the job, but with a bare status and no
|
||
sentence (#205).
|
||
|
||
- `docs/CONSUMERS.md` and both caller comments describe the REST dispatch, and
|
||
the manual bootstrap command carries a forge-neutral form beside the `gh`
|
||
one — a cross-forge runbook that directs this forge to a missing binary is
|
||
wrong even where the surrounding prose is right (#205).
|
||
- `docs/RUNNER-PROBES.md` documents the standing runner-probe venue,
|
||
`heavy-duty/ceremony-runner-probe` — the place runner-only facts are measured
|
||
on demand, ruled as option A by the operator (#202).
|
||
|
||
- `drills/README.md` cross-links it beside the disposal rule, so the exception
|
||
is visible where the dangerous habit lives (#202).
|
||
|
||
- The runbook states that the drill disposal rule does **not** apply to it.
|
||
Archiving it defeats its purpose, and that is exactly how the three existing
|
||
drill repos each became unavailable (#202).
|
||
|
||
- It records that a probe must run as an Actions job under the workflow token:
|
||
the same call answers 500 there and 204 under a PAT, so a probe run any other
|
||
way produces a confident wrong answer (#202).
|
||
|
||
- Creating the repo is recorded as the operator's step, measured rather than
|
||
assumed: a fleet identity gets 403 on org repo creation and 201 in its own
|
||
namespace (#202).
|
||
|
||
- It carries an executable two-layer arming procedure: an immutable candidate
|
||
code SHA and an armed workflow commit on top of it. A single layer is
|
||
self-referential — rewriting a workflow makes a new commit, and a commit
|
||
cannot contain its own object ID (#202).
|
||
|
||
- Callers are pinned by layer: composite actions to the candidate code SHA,
|
||
reusable workflows to the armed SHA, which is the only revision whose inner
|
||
checkout points at the fork (#202).
|
||
|
||
- The arming gate asserts what each carrier IS, not only that the old literal
|
||
is gone: every `repository:` equals the fork, every `CEREMONY_SELF_REF` value
|
||
equal the candidate code SHA, and callers match the layer they belong to
|
||
(#202).
|
||
|
||
- It enumerates the carriers from the tree rather than encoding a count, and
|
||
distinguishes ceremony's internal self-checkouts from the consumer checkouts
|
||
that must stay `${{ github.repository }}` (#202).
|
||
|
||
- Both published snippets are ShellCheck-clean when extracted and linted
|
||
directly, not merely as part of the repository sweep (#202).
|
||
|
||
- The checker validates the MANIFEST against the target it was given, so a
|
||
manifest that describes a wrong arming consistently — wrong fork, or the
|
||
armed SHA where the candidate belongs — refuses instead of matching a tree
|
||
rewritten to the same wrong value (#202).
|
||
|
||
- The manifest is generated from the PRE-arming tree, which is the only order
|
||
that enumerates the carriers that must change (#202).
|
||
|
||
- Both published snippets were driven against a constructed candidate/probe
|
||
pair: deletion, both role swaps, wrong owner, wrong
|
||
SHA, wrong path, a deleted caller class and an extra carrier all refuse, and
|
||
the armed control passes (#202).
|
||
|
||
- The manifest records complete caller coordinates, so a path swapped under the
|
||
right owner and SHA is caught (#202).
|
||
|
||
- Generator and checker share one domain — ceremony callers — so a third-party
|
||
`actions/checkout` is neither manifested nor reported as unrecognised (#202).
|
||
|
||
- Probe results are written to an issue in the probe repo and carried to the
|
||
ceremony issue by a human, so the probe holds no path that can write to the
|
||
live board (#202).
|
||
- `test/docs-sync.test.sh` drives the fetch path, which had no coverage at all:
|
||
every existing row passes `--source`, which overrides the fetch entirely
|
||
(#201).
|
||
|
||
- A stubbed `curl` records the requested URL and serves a tarball, so which
|
||
forge a pin resolves against is a tested decision rather than plumbing (#201).
|
||
- `docs/UPSTREAM-SYNC.md` — the recurring upstream sync as a runbook: the
|
||
standing resolutions, which side wins each and the issue that decided it
|
||
(#200).
|
||
|
||
- It names the step the 0.6.0 sync nearly shipped without: auditing what the
|
||
merge brought in that did **not** conflict. `git merge` asks no question
|
||
about a function upstream added to a file this tree owns (#200).
|
||
|
||
- It records that the same mechanic applies to state, not just to call sites: a
|
||
resolved region can silently remove a producer whose consumers auto-merged,
|
||
and every one of those consumers degrades to empty rather than erroring
|
||
(#200).
|
||
|
||
- It says to verify with the runner's tooling, because "green locally" was
|
||
wrong three times in one sync — untracked files, a pinned linter, and a
|
||
pinned `jq` whose empty-input exit code differs (#200).
|
||
|
||
- It says every branch open across a sync is stale afterwards — Forgejo never
|
||
re-tests an open PR when main moves, so a prior approval is evidence about a
|
||
tree that no longer exists (#200).
|
||
|
||
- It says to audit post-merge runs by executed steps rather than colour, and to
|
||
inventory what the sync changed about workflow triggers and jobs first (#200).
|
||
|
||
- `.upstream-ref` records the upstream commit this tree carries, in
|
||
machine-readable form beside the CHANGELOG's prose (#200).
|
||
|
||
- `test/upstream-delta.test.sh` fails the PR that scatters a forge decision
|
||
into a file the inventory does not name. Discovery is derived from the tree,
|
||
so a composite `action.yml` or a `.yaml` workflow is seen without anyone
|
||
remembering to add a glob (#200).
|
||
|
||
- Discovery is git's, not the filesystem's: `ls-files`, so the tarballs `ci.yml`
|
||
extracts into the checkout and any developer cache are not parsed as source
|
||
(#200).
|
||
|
||
- It refuses when the recorded commit is missing, absent from the object store,
|
||
or not an ancestor — three distinct refusals, none of them a skip. `ci.yml`
|
||
fetches that exact object so the test reads local evidence without CI
|
||
omitting it (#200).
|
||
|
||
- Its mutation cases drive the real check against a constructed tree, so
|
||
replacing the guard with `return 0` reds five of them (#200).
|
||
|
||
- `docs/CONSUMERS.md` states that two ceremonies answer to the same version
|
||
number, and how a consumer says which one it pinned (#200).
|
||
- `lib/issue_references.sh` — the LOCAL/CROSS classifier, moved out of
|
||
`actions/issueflow-reconcile`'s executable so a second caller can use it
|
||
without sourcing a reconciler, which would run one (#199, #61).
|
||
|
||
- `test/refs-not-closing.test.sh` drives the action's boundary on **both**
|
||
backends with stubs at the transport, proving one fixture yields the same
|
||
verdict on each — including a closing keyword that appears only in a commit
|
||
message (#199).
|
||
- This tree carries upstream ceremony through `8c3a4d1` (upstream `0.6.0`):
|
||
`lib/attention.sh`, `lib/read.sh`, `actions/refs-not-closing`, the guarded
|
||
reads, and the ruling and window rules (#198).
|
||
|
||
- `test/no-runtime-gh.test.sh` — the forge-portability guard: no runtime `gh`
|
||
outside `lib/forge-github.sh` unless the file declares
|
||
`CEREMONY_FORGE_CLIENT=gh` (#198).
|
||
|
||
- `CHANGELOG.md` names the upstream commit this tree carries, so a drill
|
||
record can say which `0.6.0` it exercised (#197, #198).
|
||
- `test/labels.test.sh` holds the conf's roster and `CONTRIBUTING.md`'s roster
|
||
table to the same set, in both directions, so a roster edit that touches one
|
||
file and not the other goes red instead of drifting quietly (#195).
|
||
- `test/forge-backends.test.sh` pins the replacement contract: preserve
|
||
unrelated labels across a combined add+remove, an absent removal as a
|
||
successful no-op, the empty set as a full clear, and `forge_labels_add`
|
||
still `POST`-only, per ceremony#128 (#192).
|
||
|
||
- `test/labels-reconcile.test.sh` drives a failing write through `main()` — the
|
||
swallow was in the loop, where a fixture-level probe cannot reach (#192).
|
||
|
||
### Changed
|
||
|
||
- The review panel restores `kimi-reviewer-andresmgsl` alongside GLM. The
|
||
four-identity panel now requires three cross-vendor approvals after the PR
|
||
author recuses (#224).
|
||
- The review panel names `glm-reviewer-andresmgsl` in place of
|
||
`kimi-reviewer-andresmgsl` while that identity is unavailable. The panel
|
||
stays three, so a PR still requires two cross-vendor approvals (#222).
|
||
|
||
- `test/labels.test.sh`'s table-side roster mutation names an identity the
|
||
table actually carries. Naming a departed one would mutate nothing and the
|
||
case would pass while proving nothing — #195's rot class one layer down
|
||
(#222).
|
||
- This forge's release line runs `0.4.1 → 0.6.1`: versions 0.5.0 and 0.6.0
|
||
arrived here by merge from the read-only upstream and were never released
|
||
on this forge (#220).
|
||
|
||
- The `## 0.6.0` section this changelog carries is upstream's — its entries
|
||
describe upstream's work under upstream's issue numbers. The forge port's
|
||
own work ships first in 0.6.1 (#220).
|
||
- `docs/RUNNER-PROBES.md` records the venue's first delivered drills — the
|
||
#192 asymmetry re-observed on demand under the workflow token, the dispatch
|
||
route's 204 under both identities, and #215's boundary finding — each with
|
||
the probe-issue URL it is recorded in (#202).
|
||
|
||
- Two venue lessons join the runbook where the next probe author will look:
|
||
findings must be written to issues because the venue's log route 404s for
|
||
non-admin reads, and report content sent to the forge must never contain a
|
||
credential expression or value (#202).
|
||
|
||
### Fixed
|
||
|
||
- The sweep's `bootstrap` value crosses the `workflow_call` boundary as a
|
||
declared input, explicitly passed by the caller — the one channel measured
|
||
to work; the called workflow did not see the caller's event inputs as an
|
||
implicit substitute on this instance (#215).
|
||
|
||
- Before the bridge, `github.event.inputs` was empty inside the called
|
||
workflow, so every dispatch-woken sweep bootstrapped: ~20 label upserts on
|
||
each board event (#215).
|
||
|
||
- The caller maps an empty top-level value to `no` explicitly, so a
|
||
cron-woken sweep can never bootstrap; the declared input also defaults to
|
||
`no`, so a consumer that passes nothing gets the safe path (#215).
|
||
|
||
- The gate feeds the declared input to `labels-reconcile` unchanged, so an
|
||
invalid value meets the action's own `yes|no` refusal instead of being
|
||
silently coerced (#215).
|
||
|
||
- `docs/CONSUMERS.md`'s published sweep stub carries the same pass-through —
|
||
without it every consumer inherits the defect ceremony fixed for
|
||
itself (#215).
|
||
- `issueflow-reconcile` sees this forge's issues again. The board gather used
|
||
`has("pull_request")`, and every Forgejo entry carries that key — so it
|
||
selected zero rows on every sweep while printing `reconciled.` (#210).
|
||
|
||
- Three sites take `.pull_request == null`, the discriminator the file's own
|
||
comment already specified and that one of its four call sites already used
|
||
(#210).
|
||
|
||
- `post-merge` transitions can fire again: they could not, because the sweep
|
||
saw no issues to transition (#210).
|
||
- `blocker:unrequested` is judged on this forge again. The head-commit date was
|
||
read from `repos/{o}/{r}/commits/{sha}`, which Forgejo answers **404** — so
|
||
every sweep degraded and left the blocker unjudged (#209).
|
||
|
||
- `forge_commit_at` is a verb on both backends: GitHub serves a single commit at
|
||
the bare path with the date nested, Forgejo at `git/commits/{sha}` with it
|
||
under `.created`. The caller asks for one timestamp and knows neither shape
|
||
(#209).
|
||
- `.github/workflows/labels.yml` wakes the sweep over REST instead of
|
||
`gh workflow run`, so a board event reconciles within seconds on any forge
|
||
rather than waiting up to an hour for the scheduled sweep (#205).
|
||
|
||
- The workflow-dispatch endpoint has the same shape on both forges, so that
|
||
step no longer decides one: the `CEREMONY_FORGE_CLIENT=gh` declaration and
|
||
both inline refusals are gone rather than ported (#205).
|
||
|
||
- The dispatch supplies its `ref` explicitly, because REST has no default
|
||
branch where `gh workflow run` had one, and refuses without it (#205).
|
||
|
||
- It takes that ref from the repository, never from `GITHUB_REF_NAME` — on a
|
||
`pull_request_target` run that is `<n>/merge`, which is not a branch (#205).
|
||
|
||
- A failed dispatch names the endpoint, the ref and the status, and says that
|
||
an empty `500` body from Forgejo means the workflow name or the ref did not
|
||
resolve — a bare status sends the reader after a server fault that is not
|
||
there (#205).
|
||
- `actions/docs-sync` fetches the doctrine mirror from the forge named by
|
||
`GITHUB_SERVER_URL` instead of a hard-coded `github.com` (#201).
|
||
|
||
- The same pin ref names a different tree on each forge, so a consumer's mirror
|
||
was verified against a tree it never pinned — and with HTTP 200, so `--check`
|
||
reported drift that could not be fixed (#201).
|
||
|
||
- A fetch that cannot name its forge now refuses instead of guessing: no
|
||
`GITHUB_SERVER_URL` and no `--source` exits naming the variable, having
|
||
reached for no network (#201).
|
||
|
||
- A failed fetch names the URL it actually tried, and asks whether the ref
|
||
exists on that forge rather than in the abstract (#201).
|
||
- `actions/refs-not-closing` gathers over REST through the forge shim instead
|
||
of one GraphQL query, so it produces a real verdict on Forgejo — which
|
||
serves no GraphQL surface at all — rather than refusing (#199).
|
||
|
||
- The closing set is parsed by `lib/closes_references.sh` over the PR body
|
||
**and** every commit message, unioned. Forgejo honours closing keywords in
|
||
commit messages, so a body-only port would miss a PR that closes an issue
|
||
from a commit subject (#199).
|
||
|
||
- The `hasNextPage` refusal is carried onto the paginated commit read: an
|
||
incomplete gather refuses instead of returning a partial verdict, reusing
|
||
the backend's `x-total-count` completeness proof (#199).
|
||
|
||
- A failed read never reaches the parser. An unread body parses to an empty
|
||
closing set, which is a passing verdict the action never earned (#199).
|
||
|
||
- `.github/workflows/refs-guard.yml` no longer gates the job on the forge.
|
||
A portable action behind a github-only gate is a guard that passes by never
|
||
running (#199, #198).
|
||
|
||
- The `CEREMONY_FORGE_CLIENT=gh` declaration is gone, and the contract test
|
||
asserts its absence: an opt-out with no `gh` behind it is a standing
|
||
permission slip (#199).
|
||
- Eight runtime `gh` call sites arrived with the merge outside every conflict
|
||
hunk, in functions upstream added to files this tree already owned. Seven
|
||
are ported onto the shim; the eighth is named with its reason (#198).
|
||
|
||
- The open-PR gather reads `Refs`, not only closing keywords. Reading one side
|
||
for closing links and the other for `Refs` is what released a live claim in
|
||
crew#321, and this tree carried that shape (#198).
|
||
|
||
- The merged record gains `merged_at`, so `post_merge_pr_for_issue` answers
|
||
the PR that merged last rather than the highest-numbered one. Without the
|
||
column every sort key ties and the old order returns silently (#198).
|
||
|
||
- The open gather feeds `open_pr_issues` one record per physical body line. A
|
||
whole decoded body as one record loses every declaration including the
|
||
first, and reclaims a claim a live PR was holding (#198).
|
||
|
||
- The post-merge nudge links the issue on the forge in play rather than a
|
||
hard-coded `github.com` (#198).
|
||
|
||
- `actions/refs-not-closing` reports and skips on a forge it cannot speak,
|
||
naming the client and #199, instead of standing red on every PR. It reaches
|
||
the forge zero times, so no verdict is produced either way (#198).
|
||
|
||
- `.github/workflows/labels.yml`'s sweep dispatch declares the client it
|
||
speaks and decides the FORGE before the binary, so a Forgejo runner that
|
||
happens to ship `gh` cannot dispatch against a forge that cannot serve it.
|
||
#205 ports it to REST (#198).
|
||
|
||
- `actions/refs-not-closing` fails closed on a forge it cannot speak, and
|
||
`.github/workflows/refs-guard.yml` carries the scheduling decision — the
|
||
action never reports a success it did not earn (#198).
|
||
|
||
- `issue_payload_valid` refuses an empty payload on jq 1.6 as well as 1.7.
|
||
`jq -e` exits 4 on empty input under 1.7 and **0** under 1.6, and this
|
||
instance's runner carries 1.6 — so the guard #247 D3 added to refuse an
|
||
unreadable read was accepting one here (#198).
|
||
|
||
- The post-merge nudge strips a trailing slash from the server URL, so a forge
|
||
URL carrying one does not render `//owner/repo` (#198).
|
||
|
||
- `.github/scripts/release-path.sh` names `lib/forge.sh`: #191 put the shim on
|
||
the release doors' executable path here, so a doors-unchanged record that
|
||
omitted it was measuring the wrong set (#198).
|
||
- `.github/labels.conf` names identities that exist on the forge this repo
|
||
lives on. All five it named before were absent, so `panel=` could never
|
||
converge a review round and `triage-actors=` made every issue a stray mint
|
||
nobody could normalize (#195).
|
||
|
||
- `CONTRIBUTING.md`'s roster table matches the conf: three identities, the
|
||
human row is `andres`, and the approval count states what panel-minus-author
|
||
actually resolves to on this roster rather than a stale three (#195).
|
||
- Label removal on Forgejo is a full-set `PUT`, not a per-label `DELETE`. The
|
||
workflow token gets HTTP 500 on every `DELETE .../labels/{id}` on this
|
||
instance, so the state machine could only ever ADD labels (#192).
|
||
|
||
- Every `state:*` transition that needs the previous state cleared, and every
|
||
`blocker:*` that should lift, can now actually clear. They were inert (#192).
|
||
|
||
- A label edit that fails is fatal to `labels-reconcile`, matching
|
||
`issueflow-reconcile`. One cause had two contradictory policies (#192).
|
||
|
||
- A failed write reaches the sweep's exit code: per-PR tolerance is kept for
|
||
READS, but a sweep that could not write exits non-zero and its output carries
|
||
no `reconciled.` token at all (#192).
|
||
|
||
- Every label mutation goes through one checked helper, so clearing
|
||
`merge-next` or either `stale` edit fails the sweep too — not only the
|
||
primary state edit (#192).
|
||
|
||
- A preserved label keeps the id the issue payload already carried, so
|
||
preservation does not depend on a repository-wide list that has nothing to do
|
||
with the issue (#192).
|
||
|
||
- A removal that changes nothing writes nothing, rather than replacing the set
|
||
with itself and opening a race for no state change (#192).
|
||
|
||
- Every failure diagnostic on the forgejo backend names the verb as well as the
|
||
path and the status. A read used to say `HTTP 500 from 'repos/…'`, which
|
||
cannot be told from a failed write of the same path (#192).
|
||
|
||
- The diagnostic names what was attempted and that it did not happen, instead
|
||
of blaming a missing label and telling the operator to bootstrap — a cause it
|
||
had not established (#192, #101).
|
||
|
||
- An add-label the repo does not carry refuses before any write, so a
|
||
replacement `PUT` can never drop a label nobody asked to remove (#192).
|
||
|
||
## 0.6.0 — 2026-08-05
|
||
|
||
### Added
|
||
|
||
- The issue-flow sweep's `claimed`-branch ruling pre-read is pinned: an
|
||
unassigned claim under `needs-ruling` must draw its board diagnostic and
|
||
its ruling nudge in one sweep, so a read that drifts below the diagnostic
|
||
reds instead of silently costing the escalation 7 days (#284, #307).
|
||
- The issue-flow sweep now flags a collision the board never declared: two
|
||
open, unblocked issues whose titles name one deliverable draw a comment
|
||
naming the newer's owed `Blocked by` edge. Keys normalize, so
|
||
`actions/x` and `x` are one deliverable (#288).
|
||
- The sweep now flags an unblocked non-member during a standing release
|
||
window, naming the window's invariant. `claimed` counts, PR in flight or
|
||
not. The gate is read from the release issue's own `Blocked by`
|
||
declarations, and an emptied gate leaves it dormant (#292).
|
||
- Both flags are advisory: comments only, no label write and no state
|
||
change, deduped against each family's last word on the thread so a
|
||
standing state re-sweeps silently (#293).
|
||
- The fragment guard now requires each entry to end with its issue
|
||
citation: one `(#N)` group — local, `repo#N` or `owner/repo#N`
|
||
references separated by `, ` — then the final `.` and nothing after it
|
||
(#262).
|
||
- The refusal distinguishes an entry carrying no reference at all from one
|
||
whose reference is present but not terminal, and names the shape to
|
||
write in both (#262).
|
||
- The 300-character bound still outranks the citation across the whole
|
||
fragment, and the outranked problem stays out of the message it lost
|
||
to: one fragment, one diagnosis, wherever in the file it sits (#262).
|
||
- BUILDER.md now describes a fix round that rides a draft: the draft phase
|
||
stays the builder's, ready-for-review is the builder's own act, and where a
|
||
draft suppressed the checks green is proven at the flip (#258).
|
||
- REVIEWER.md now reads a draft carrying `state:addressing` as a fix round in
|
||
progress rather than abandonment (#258).
|
||
- A `post-merge` item with no comment for 7 days now draws one nudge from the
|
||
issue sweep: the wake evidence is owed. A starving criterion used to be
|
||
found only when someone happened to run the right read (#254).
|
||
- Label churn does not reset that clock, and neither does an assignment: on
|
||
`post-merge` an assignee is an invalid composition, not activity, and it
|
||
must not buy the item another 7 days of silence (#254).
|
||
- The nudge names the triage actor from `triage-actors=`, not the human
|
||
reviewer: `post-merge` is triage's completion queue, so the starved wake
|
||
condition is triage's to answer (#254).
|
||
- It links the item and parses nothing from the body — which criterion
|
||
starved is prose, and the machine never judges prose (#254).
|
||
- Like the ruling nudge it carries no idempotency marker on purpose: the
|
||
comment is itself activity, so the rule self-rate-limits to one nudge per 7
|
||
quiet days. Comment-only — no path here writes a label (#254).
|
||
- Release epics now announce release initialization when their declared dependency gates clear (#253).
|
||
- The issue sweep now echoes an issue's parsed `Blocked by` set as a comment
|
||
whenever that set changes, so a readable-but-wrong declaration is visible in
|
||
one sweep instead of days later, when a human happens to run the parser by
|
||
hand (#252).
|
||
- The echo's marker carries the parsed set itself: an unchanged parse never
|
||
re-posts on a 15-minute cron, and a changed one always speaks. Comment-only
|
||
— no path here writes a label (#252).
|
||
- CI now refuses a root `*.md` declared in neither `docs/VENDORED.txt` nor the
|
||
guard's short exemption list, so a new doctrine file can no longer reach a
|
||
tag undeclared and stay invisible to every consumer's `docs-sync` (#251).
|
||
- The same guard reads the manifest the other way: every entry must resolve to
|
||
a regular, non-empty, tracked file — no symlink, no directory, no `../`
|
||
escape (#251).
|
||
- Document the optional, operator-ruled release-epic flow for governed repositories. (#248).
|
||
- Guard documentation availability markers against missing issue citations
|
||
and release candidates that already ship the cited work (#238).
|
||
- The label and issue-flow sweeps now comment once per episode when
|
||
`attention` targets a pull request or an unassigned issue, without
|
||
retargeting the demand or changing labels or assignees (#232).
|
||
- Pull requests that promise `Refs #N` now fail a read-only, body-edit-aware
|
||
guard if GitHub would close N through a keyword or sidebar link (#218).
|
||
|
||
### Changed
|
||
|
||
- `README.md` is rewritten whole from the current tree: the front page names
|
||
the governance repo ceremony now is, routes to `docs/CONSUMERS.md`,
|
||
`AGENTS.md`, `LABELS.md` and `RELEASES.md` rather than restating them, and
|
||
keeps the operator's release runbook as its core, re-measured (#311).
|
||
- Standing release windows are dependency DAGs: every mint is placed in the window or behind it, and only current sources are `ready` (#292).
|
||
- TRIAGE.md now requires unconditional collision-edge chains when open issues
|
||
carry the same deliverable, keeping the ready queue concurrently claimable
|
||
(#288).
|
||
- TRIAGE.md now states its rules with bare record cites: the label-race and
|
||
lifted-hold incident narratives leave the normative text while their
|
||
operational rules remain complete (#282).
|
||
- `BUILDER.md` states its rules and cites their record bare: the incident
|
||
narratives, the links into issue comments and the cross-repo issue cites
|
||
leave the normative text, which no rule leaves with them (#281).
|
||
- CONTRIBUTING.md now keeps vendored doctrine self-contained: state the rule,
|
||
retain at most one sentence of why, cite the local record bare, and leave the
|
||
incident narrative in that record (#280).
|
||
- BUILDER.md's green ruled term now says which entry to read before it says
|
||
what an entry means: a check's word at a head is its newest entry by start
|
||
time, and a cancelled entry is not that word while the same check carries a
|
||
non-cancelled one at that head (#276).
|
||
- A check whose every entry at the head is cancelled is unchanged — nothing
|
||
survived to be its word, so it never reported and is not green — and the
|
||
collapse mirrors `checks_state`'s carve-out rather than adding a class
|
||
(#276).
|
||
- BUILDER.md's step 1 now rules the checkless head: no checks configured is
|
||
nothing to wait for, and the request goes out straight away — stated once,
|
||
in the ruled-term paragraph, with the draft-round restatement removed
|
||
(#272).
|
||
- `README.md` and `RELEASES.md` derive `scope:docs`, and the
|
||
`changelog-assembled`, `docs-sync` and `runner-isolated` actions and tests
|
||
derive `scope:guards`; all five were mapped nowhere. The docs block matched
|
||
a literal `README`, which this tree does not carry (#267).
|
||
- `lib/read.sh` and `lib/ruling.sh` derive `scope:labels` beside
|
||
`scope:release-flow`. Both reconcilers share them, and a mixed file wears
|
||
both labels rather than `lib/**` being re-carved into a row per file (#267).
|
||
- TRIAGE.md now tells every epic author to put its progress checklist under
|
||
the literal `## Task list` heading, because any other heading is silently
|
||
invisible to the completion sweep (#266).
|
||
- TRIAGE.md now scopes the no-assignee board bug to flagging an unassigned
|
||
issue, while still directing triage to repair ownership instead (#264).
|
||
- `BUILDER.md` and `CHANGELOG.md` state the citation as guard-enforced
|
||
rather than as house style, beside the 300-character bound it now sits
|
||
next to (#262).
|
||
- Four fragments in flight gained a terminal citation; published sections
|
||
are untouched, so no shipped prose is re-opened (#262).
|
||
- BUILDER.md's green ruled term now names its field: greenness is read from
|
||
each check's `conclusion`, never its `status`, and *stale* means a check
|
||
of a superseded head — not a same-head node whose `status` lags its own
|
||
conclusion (#260).
|
||
- Consumer guidance: re-vendor tooling reads the pin's `docs/VENDORED.txt`,
|
||
never a hardcoded list, so a new doctrine file propagates at the next
|
||
ordinary pin bump with zero list edits (#251).
|
||
- Define the doors-unchanged drill record and an executable release-path list,
|
||
so a release may reuse live evidence only when its door bytes are unchanged
|
||
since the last rehearsed tag (#237).
|
||
|
||
### Fixed
|
||
|
||
- A roster edit no longer reds the whole suite: the labels-reconcile
|
||
state-machine fixtures name their own panel instead of binding
|
||
`.github/labels.conf` by slot (#304).
|
||
- Shrinking `panel=` to three had left that binding's third slot unbound, and
|
||
`set -u` aborted the file before its first assertion — 217 assertions
|
||
became 0, on `main` and on every branch cut from it (#304).
|
||
- The one case still reading the shipped roster asserts a property, not a
|
||
size: it parses, and each member is recused from its own panel. Any
|
||
`panel=` of one or more members leaves `test/run.sh` green (#304).
|
||
- `lib/attention.sh` locates as label machinery beside its two shelf-mates —
|
||
`[scope:release-flow]` alone was a wrong answer of the class #267 measured
|
||
— and the map learns the sweep workflow pair, the shared-lib tests, and
|
||
seven enumerated test/guard surfaces (#302).
|
||
- Claiming a `needs-ruling` issue no longer buys its escalation another 7
|
||
quiet days: the issue-side ruling clock reads comments alone — an
|
||
assignment is the claim clock's fact — and LABELS.md now names what each
|
||
surface's clock reads (#284).
|
||
- `scope:release-flow` no longer rides every pull request: `changelog.d/**`
|
||
is out of its path map. Doctrine makes every behavior change write a
|
||
fragment, so the glob labelled 20 of the last 20 PRs while 3 touched a
|
||
release surface. `CHANGELOG.md` stays, as only the release PR edits it
|
||
(#267).
|
||
- The issue-flow reconciler and its test now derive `scope:labels`, the scope
|
||
that already names the taxonomy they reconcile (#267).
|
||
- Abort issue-flow reconciliation when the board read fails instead of reporting a complete pass over an empty or partial result (#257).
|
||
- The issue sweep no longer derives label writes from a read that failed. An
|
||
HTTP 504 whose body is GitHub's JSON error object passed every guard and
|
||
emptied the label set, so a healthy epic was written `needs-triage` and the
|
||
pass reported success (#247).
|
||
- A failed comments read no longer reclaims a live claim. Swallowed, it dated
|
||
the issue by `created_at` and unassigned the builder under a comment
|
||
asserting 48 hours of silence about an issue commented on seconds earlier
|
||
(#247).
|
||
- A failed comments read no longer reads as "no marker", which re-posted the
|
||
comment the marker exists to suppress (#247).
|
||
- Every read inside the per-issue subshell is checked explicitly, on its
|
||
status and on its payload shape; the issue is left exactly as it is and the
|
||
sweep continues. A partial pass names its skipped issues after
|
||
`reconciled.` (#247).
|
||
- A per-issue pass is now atomic: its writes and its log lines commit only
|
||
once the pass completes. A skip could previously land after an earlier
|
||
mutation, reporting an issue as untouched when a label had already been
|
||
written or removed (#247).
|
||
- The issue-flow sweep now reads an issue's deliverable as the `Refs` PR that
|
||
merged last, not the one numbered highest — merge order is not number order,
|
||
and the old rule spent the transition marker on the wrong PR (#242).
|
||
- Preserve active claims when an open local pull request links them with `Refs #N`. (#241).
|
||
- `blocker:unrequested` no longer fires while a head's checks are pending or
|
||
red: the review round forbids requesting there, so the one blocker that
|
||
demanded an act flagged builders for complying. Pending is CI's move, red is
|
||
`blocker:ci-red`'s (#236).
|
||
- `blocker:unrequested` now waits for the round to settle — the head and the
|
||
newest verdict must have stood for `RECONCILE_UNREQUESTED_GRACE` (default
|
||
300s) — so a sweep landing between a push and its re-request no longer flags
|
||
a round in motion (#236).
|
||
- LABELS.md no longer claims nothing in `actions/` clears or reads
|
||
`attention`: the reconciler has done both since the derived `claimed` →
|
||
`post-merge` transition shipped. The amended text keeps the hand-set rule
|
||
and admits the one clear and the diagnostic read (#231).
|
||
- Triage now puts `attention` on the assigned issue that owns a claim, never
|
||
on its pull request, and treats an unassigned issue as a board bug rather
|
||
than a demand (#230).
|
||
|
||
## 0.5.0 — 2026-08-03
|
||
|
||
### Added
|
||
|
||
- `labels.conf` accepts optional `panel[<login>]=` rows: the required set for
|
||
a PR authored by that login is the row minus the author; other authors keep
|
||
`panel=`. Consumers gain the row at their next pin bump — adding it before
|
||
that bump is a parse failure that takes the label board down (#224).
|
||
|
||
### Changed
|
||
|
||
- Doctrine: third-party actions never hold a write-capable token by default —
|
||
repo-owned scripts in write-capable jobs, established publisher plus
|
||
full-SHA pin for the exception, SHA pins everywhere. Canonical in
|
||
REVIEWER.md, short form in BUILDER.md; consumers adopt at the pin bump
|
||
(#216).
|
||
|
||
### Fixed
|
||
|
||
- `ruling_escalation_row` selects the setter's best-shaped in-window comment,
|
||
ties broken to the earliest, instead of the earliest outright — a whole-round
|
||
reply landing seconds before the escalation is no longer graded in its place
|
||
(crew#293).
|
||
- The escalation selector and `ruling_shape_decision` share one field-presence
|
||
matcher, and an undecodable body column scores 0 instead of erroring the
|
||
sweep.
|
||
- Five stale **unreleased** markers in `docs/CONSUMERS.md` now name their
|
||
tags: fragment mode, `changelog-assembled` and `runner-isolated` at
|
||
`0.2.0`; the additive labeler at `0.3.0`; the two-caller split at `0.4.1`
|
||
(#221).
|
||
- The marker convention now names its clearing owner: the release PR that
|
||
ships machinery clears, in that same PR, every marker its assembled
|
||
section makes false (#221).
|
||
- A standing non-approving verdict now outranks draft in `decide_state`: a
|
||
re-drafted PR mid-round reads `state:addressing`, a live panel request on a
|
||
draft surfaces as `state:bots-reviewing`, and a draft with no round history
|
||
still reads `state:building` (#205).
|
||
|
||
## 0.4.1 — 2026-08-04
|
||
|
||
### Added
|
||
|
||
- `forge_release_exists`, `forge_commit_pulls`, `forge_tag_create`,
|
||
`forge_release_create` and `forge_pr_create` on both backends, so the
|
||
release path names no client (#191).
|
||
|
||
- The forgejo backend serves one PR object at `/commits/{sha}/pull` where
|
||
GitHub serves an array at `/pulls`; both verbs emit the array shape, so
|
||
the call site carries one expression (#191).
|
||
|
||
- Forgejo creates tags at `POST /tags` — it serves `/git/refs` GET-only,
|
||
so GitHub's ref-POST would have 404'd there forever (#191).
|
||
|
||
- `forgejo_api_base` refuses when `REPO` is empty. Every verb interpolates
|
||
it and every call reaches the network through there, so `repos//…` —
|
||
whose 404 reads as "no release" and "no PRs" — is now impossible (#191).
|
||
|
||
- Release asset names are percent-encoded. The hook contract permits any
|
||
filename, and the name travels as a query value: a space made curl reject
|
||
the URL and `&`/`#`/`+`/`%` silently renamed the asset (#191).
|
||
- `lib/forge.sh` — the forge selector: `forge_detect` names the forge from
|
||
the runner's own environment, `forge_client` names the client it needs, and
|
||
`forge_preflight` refuses loudly before any sweep when the two disagree
|
||
(#188).
|
||
- The reconcilers and `labels-scope` run that preflight first, so a
|
||
GitHub-shaped client on a Forgejo instance is a named refusal instead of a
|
||
sweep that reads nothing and reports success (#188).
|
||
- `lib/closes_references.sh` — the closing-keyword parser, sibling of
|
||
`refs_references`, so "which issues does this PR close" is answered from a
|
||
PR body rather than from GitHub's GraphQL API (#188).
|
||
- `lib/forge-github.sh` and `lib/forge-forgejo.sh` — one call surface, two
|
||
backends, selected by `forge_select`; no forge branching at the call sites
|
||
(#188).
|
||
- The forgejo backend proves each paginated gather complete against the
|
||
server's `x-total-count` and refuses loudly when it cannot — a missing
|
||
header is a refusal, not a pass (#188).
|
||
|
||
### Changed
|
||
|
||
- `docs/CONSUMERS.md`'s artifact-hook recovery no longer tells operators to
|
||
run `gh release create` by hand — on a Forgejo runner there is no `gh`.
|
||
It names the forge-neutral tag-door path first, with both clients shown
|
||
(#191).
|
||
- `issueflow-reconcile` gathers open and merged PRs over REST instead of
|
||
`gh api graphql`. Forgejo serves no GraphQL at all, so the two queries were
|
||
replaced rather than translated; both forges return `number` and `body`
|
||
from `/pulls` in the same shape (#188).
|
||
|
||
- `forge_api` owns the page size, because each forge silently ignores the
|
||
other's parameter: `per_page=100` reads 30 items on Forgejo and `limit=100`
|
||
reads 30 on GitHub, both HTTP 200. No call site names one (#188).
|
||
|
||
- Outstanding review requests are derived from the reviews on the current head
|
||
rather than from `requested_reviewers`, which Forgejo never clears — read
|
||
raw there, a PR would sit at `state:bots-reviewing` forever (#188).
|
||
|
||
### Fixed
|
||
|
||
- The release doors run on a Forgejo consumer. `lib/facts.sh` and
|
||
`release.yml` gathered and published through `gh`, which the runner image
|
||
does not ship, so the merge door read `labeled=no` for a correctly
|
||
labeled ceremony PR and the tag door died at the publish (#191).
|
||
|
||
- A release fact that could not be read is no longer reported as a definite
|
||
`no`. A completed read finding no label is still `no` and still
|
||
fail-closed; a read that did not complete refuses and emits no fact
|
||
(#191).
|
||
- `labels-scope` no longer fails to compile its jq program on jq 1.6, which
|
||
the Forgejo runner image ships: `label` is a reserved word in jq's grammar,
|
||
so `$label` is a syntax error there and every scope derivation died before
|
||
reading the config (#188).
|
||
|
||
- `labels-reconcile` and `labels-scope` no longer exit 0 on a Forgejo
|
||
consumer having read zero facts — measured on `heavy-duty/rig`, where the
|
||
sweep printed `reconciled.` over an empty PR list and scope reported "no
|
||
labeler.yml" for a file that exists (#188).
|
||
|
||
- `forge_timeline` projects Forgejo's label events (`.type` / `.body` /
|
||
`.user.login`) into the GitHub shape (`.event` / `.actor.login`) so the
|
||
ruling ladder reads the same board on both forges (#188).
|
||
|
||
- `forge_pr_activity` no longer calls `/pulls/{n}/comments` on Forgejo
|
||
(HTTP 404); inline review comments come from reviews with
|
||
`comments_count > 0` (#188).
|
||
|
||
- CI installs shellcheck before linting, matching actionlint — the Forgejo
|
||
runner image does not ship it (#188).
|
||
|
||
## 0.4.0 — 2026-07-29
|
||
|
||
### Added
|
||
|
||
- `changelog.d/shape` — an optional one-line sentinel, `flat` or `grouped`,
|
||
that pins the fragment set's shape and outranks the newest-published-section
|
||
inference; absent, the inference binds unchanged (#182).
|
||
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.
|
||
- `changelog_fragment_problem` bounds every entry at 300 normalized
|
||
characters, red on the PR that writes the fragment; the armed guard and
|
||
the assembler inherit the one definition (#167).
|
||
- BUILDER.md and CHANGELOG.md state the bound and the split rule: a long
|
||
change ships several short entries, never one long one (#167).
|
||
|
||
### Changed
|
||
|
||
- Labels automation docs now make sweep cadence a consumer-owned tradeoff,
|
||
retain hourly as the engine-less default, and document manual dispatch as
|
||
the operator's immediate full-board sweep (#203).
|
||
- `labels` — the reconcile cron relaxes from `*/15` to hourly (#199), cutting a
|
||
private consumer's schedule-triggered full-board sweeps ~4× at GitHub's
|
||
1-minute billing floor.
|
||
- `labels` — the hourly cron is the sweep's only wake for transitions no
|
||
subscribed event carries — a verdict landing, blocker:ci-red, a
|
||
blocker:conflict when another PR merges, the time-based stale/reclaim — so it
|
||
bounds their latency to ≤1h, delaying no event-carried transition (#199).
|
||
- `labels` — the caller's `issues:` trigger narrows to
|
||
`[opened, closed, edited, reopened]` (#199), the actions that carry a
|
||
queue-state change the cron cannot wait a cadence for. The churn/validation
|
||
actions — labeled/unlabeled/assigned/unassigned — come off; the PR handoff
|
||
wake is unaffected.
|
||
- `labels` — each caller trigger now carries a comment saying why it is
|
||
subscribed, and reconcile keeps `cancel-in-progress: false` (#199) —
|
||
cancelling a sweep mid-board is the race that guard exists to prevent.
|
||
- `CONTRIBUTING.md` now points to `BUILDER.md` for the shared PR flow instead
|
||
of restating doctrine that can drift, while retaining ceremony's roster and
|
||
other repo-specific facts (#198).
|
||
- Builder doctrine makes each whole-round reply the durable Round log record
|
||
mirrored by the engine, leaving handoff as a mechanical facts-only step
|
||
instead of a newly composed summary (#196).
|
||
- `FLEET.md` removes its duplicate bench roster, records crew as a general
|
||
operator-configured tool, and advances its whole-file audit stamp to
|
||
`crew@eaeb302` with every surviving crew link re-pinned (#193).
|
||
- `FLEET.md` keeps the registry's authorization rule and its crew#16/crew#66
|
||
provenance, while replacing duplicated mechanism and path claims with a
|
||
pinned pointer to crew's registry header (#192).
|
||
- `BUILDER.md` gates both review-request points on a green check at the
|
||
head, carries crew#45's argued exception for failures outside the PR,
|
||
and states the ruled classification: cancelled and stale are not a
|
||
green head; skipped and neutral are (#189).
|
||
- `BUILDER.md` documents CI-red recovery in pickup precedence: a red head
|
||
of your own PR is picked up before claiming another issue, is never a
|
||
parked claim, and follows crew#17's recovery path (#189).
|
||
- `FLEET.md` writes the ci-red wake into the duty order between resume
|
||
and build, now as deployed engine rather than on paper: the
|
||
reconciliation stamp advances to the crew SHA carrying crew#64 (#189).
|
||
- `FLEET.md` describes the build wake's check gate as the engine
|
||
implements it: a green head, or one with no checks configured, opens a
|
||
round; a red head and an unfinished one are held and reported
|
||
separately (#189).
|
||
- `FLEET.md` corrects the attention wake to the crew#66 ruling: the query
|
||
is cross-repo, the action is registry-bounded, and an out-of-scope
|
||
demand is reported and escalated to the operator rather than worked. It
|
||
no longer claims attention is exempt from the registry (#189).
|
||
- `FLEET.md` distinguishes an attention session that dies before acking,
|
||
which relaunches, from one that completes without acking, which is a
|
||
decline a ledger keeps from re-firing (#189).
|
||
- `BUILDER.md` re-requests by head, not by verdict: a push while
|
||
answering a round stales every approval, so every panelist is
|
||
re-requested; only an unchanged head re-requests the non-approvers
|
||
alone (#190).
|
||
- FLEET.md's duty-loop mechanism is a pointer to crew's shared engine; the
|
||
wake lists follow the engine's duty order, the roster keeps the as-built
|
||
bench beside `fleet.roster`'s target, and the reconciliation stamp names
|
||
crew@`01fb49c` (#187).
|
||
- Ceremony's changelog is grouped from this release forward: the pending
|
||
fragments carry `### ` headings under a `grouped` sentinel (#182).
|
||
- BUILDER.md: a park declaration stands until its facts change — a
|
||
nothing-changed resumption posts nothing; only a no-open-PR park owes a
|
||
refresh, inside the 48-hour reclaim window (#178).
|
||
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).
|
||
|
||
### Fixed
|
||
|
||
- FLEET.md no longer says a review request outside the registry is
|
||
authorization: `repos.txt` is the scope for the review queue, out-of-scope
|
||
requests are logged and never acted on, and the attention wake is stated
|
||
as the one registry-independent exception, by design (#187).
|
||
- `blocked_reference_records` unions every `Blocked by` clause in the body
|
||
instead of binding to the first marker occurrence — a repeated declaration
|
||
no longer promotes on its first sentence alone, and earlier prose that
|
||
merely mentions being blocked no longer hijacks the parse (#184).
|
||
- `decide_state()` refuses `state:needs-human` while the hand-set `blocked`
|
||
label stands — the PR falls to `state:addressing`, exactly parallel to the
|
||
`needs-ruling` exclusion; never emitted by `blockers()` (#180).
|
||
|
||
## 0.3.0 — 2026-07-24
|
||
|
||
- Make `changelog-armed` reject fragment shape drift on the PR that introduces it.
|
||
- A directive hold now has a written ending, not just a beginning: BUILDER.md's shape 5 says the hold ends where it began — on the labels — with the hold owner's most recent queue-label event governing over any stale prose, the timeline read (`gh api .../issues/{n}/timeline`) named as the move before standing down or up on a hold, a claim against stale prose required to cite the events it read, and a refused claim given its two exits. TRIAGE.md now requires re-reading label events before asserting label-borne state in prose, and makes correcting a lifted hold's stale body header triage's move in the same tick. On 2026-07-24 the unranked signals split two builders reading one board (#149, #151); both acted defensibly — the doctrine, not the builders, lacked the rule (#154).
|
||
- Doctrine names the second `Closes #N` exception: a same-repo PR whose
|
||
authorizing issue marks an acceptance criterion post-merge uses `Refs #N`,
|
||
and triage closes the issue by hand on the evidence — merging #143
|
||
auto-closed #137 with exactly such a criterion unmet, and no role had been
|
||
told otherwise. TRIAGE.md now requires a post-merge criterion to carry its
|
||
own mechanism (post-merge, triage closes, `Refs #N`), REVIEWER.md lists
|
||
`Refs #N` beside `Closes #N` and `Part of <owner>/<repo>#N` and stops
|
||
treating the reference-only PR as a defect, and CONTRIBUTING.md points at
|
||
BUILDER.md as the rule's one home (#151).
|
||
- FLEET.md — the Reviewers wake describes the deployed sweep, not the `gh search` trigger the bench replaced: the pulls-API `requested_reviewers` sweep across the org plus the named bot forks is source 1, the `repos.txt`/search poll an adds-only backstop, and the two are merged and deduplicated by (repo, PR) before acting. Only the notifier's `needs-ruling` queue remains on paper; `repos.txt` is the registry only on the triage box; and the Status block now stamps the crew ref the file was last reconciled against (#149).
|
||
- REVIEWER.md now carries the review mechanics every box had been re-deriving from an incident: the queue comes from the API and not the search index, every write is one-shot per (reviewer, PR, head), heads are reviewed in throwaway checkouts, a pinned consumer's config is verified at its pin, and a verdict names the checks its box could not run (#145).
|
||
- The `docs/CONSUMERS.md` labels-caller stub lists the same `issues:` types
|
||
as ceremony's own caller — `edited` and `reopened` included — so a consumer
|
||
adopting the stub wakes when an issue body's `Blocked by #N` declaration is
|
||
edited, and when a closed issue re-enters the queue wearing labels derived
|
||
at close. The two lists drifted apart inside PR #32; a parity test now pins
|
||
them together, red if either file drops a type or the lists diverge.
|
||
Adopting the widened list is a stub edit riding the pin bump to the first
|
||
tag carrying this change (#144).
|
||
- `labels-reconcile` — a queue-cancelled duplicate check is discarded when its context holds a real verdict, so a sibling PR's eviction no longer reds a green PR; an all-cancelled context still blocks (#139).
|
||
- `blocker:unrequested` now clears the moment the panel is asked: the labels
|
||
caller (and the `docs/CONSUMERS.md` stub) listens on `review_requested` and
|
||
`review_request_removed`, so the one event that falsifies the label — or
|
||
makes it true again — wakes the reconcile sweep instead of waiting for an
|
||
unrelated push or the advisory cron. The `scope` job skips both events:
|
||
they change no paths, and running the labeler on them widens the #130
|
||
clobber window. Adopting the new triggers is a stub edit riding the pin
|
||
bump to the first tag carrying this change (#137).
|
||
- `drills/README.md` no longer tells the builder to delete the scratch repo —
|
||
a step no fleet identity can perform, because `delete_repo` is deliberately
|
||
absent from bot tokens. The builder's end state is **archive**
|
||
(`archived: true`, inside the `repo` scope); the delete is the operator's,
|
||
and cleanup gates nothing — not ready-for-review, not the panel, not the
|
||
merge. The drill record now names the scratch repo by `owner/name` and
|
||
states the disposal its author actually observed, never one that has not
|
||
happened: both 0.2.0 drills hit the missing-scope wall independently, one
|
||
stalling a release draft on an impossible 403, the other shipping a record
|
||
asserting a delete that never ran (#135).
|
||
- `lib/facts.sh` — a repository's first push to `main` (a root commit with no first parent) now reads `base_ver=(none)` and lets decide's table govern, instead of dying at exit 128 before establishing a fact; the no-base path skips the base fetch and `git show`, and an unresolvable head still fails loudly (#134).
|
||
- The changelog rule now explains why release PRs write no fragment and how entry-worthy changes land instead (#131).
|
||
- `actions/labels-scope` replaces `actions/labeler@v5` in the labels workflow's scope job: labeler wrote the whole label set (`PUT`) even under `sync-labels: false`, silently removing any label applied while it ran — #128 lost its `release` that way — so the scope job now derives from the same `.github/labeler.yml` mapping (the `changed-files`/`any-glob-to-any-file` shape, block or flow; anything else refuses loudly) and its only write is an additive `POST`. The reconcile sweep also warns — never sets — when a non-draft PR is release-shaped (bare version differing from its base) but carries no `release` label (#130).
|
||
|
||
## 0.2.0 — 2026-07-24
|
||
|
||
- `test/changelog-assembled.test.sh` — keep the trio interaction aligned with fragment mode: a dropped entry makes armed red too, while a hand-edited section leaves assembled as the sole red (#126).
|
||
- `actions/changelog-assembled` — a release PR's stamped section must be byte-for-byte what the fragments it consumed assemble to, replayed from the merge base; inapplicable trees pass with a NOTICE (#116).
|
||
- `changelog-armed` — treat `changelog.d/` as the arming, validate every development fragment, and require bare releases to consume the directory into their exact publishable section (#115).
|
||
- `lib/changelog.sh` + `bin/changelog-assemble` — read the `changelog.d/` fragments, assemble one release section (canonical group order, one shape per repo), and consume exactly what was published (#114).
|
||
- BUILDER.md — the directed hold is the parked claim's fifth shape, its attention demand is acknowledged in the declaration comment, and its board bookkeeping covers in-flight work; TRIAGE.md no longer excludes it (#113).
|
||
- Ceremony adopts `changelog.d/` — a PR writes one fragment per issue instead of editing `CHANGELOG.md`, the release PR assembles the section, and `## Unreleased` is gone (#112).
|
||
- BUILDER.md — the handed-off PR is the parked claim's fourth shape, its handoff is its declaration, and shape 2 covers the round awaiting its first verdicts (#109).
|
||
- `labels-reconcile` — warn once per sweep when a repository lacks labels declared by the pinned core taxonomy (#105).
|
||
- `LABELS.md` — drop the vendored scope-table enumeration; the per-repo set lives in `.github/labels.conf` and the repo's own CONTRIBUTING (#104).
|
||
- `labels-reconcile` — a degraded mergeability/checks read now logs gh's actual stderr (collapsed, bounded) beside the byte-identical counted line, and the blind-sweep warning leads with the observed reason instead of asserting the permissions cause (#101).
|
||
- Changelog publication — count entries instead of bytes, refuse dangling grouped headings, and seed grouped re-arms with Added/Changed/Fixed (#98).
|
||
- `labels-reconcile` — grant callers private-repo check reads and warn when an entire PR sweep is blind (#95).
|
||
- `labels-reconcile` — the bootstrap now retires the six GitHub defaults `LABELS.md` publishes as deleted, tolerating both an already-absent label and a refused delete (#93).
|
||
- `issueflow-reconcile` — a triage-authored issue arrival stands down with exit 0 instead of killing the run before the sweep (#91).
|
||
- FLEET.md — the assignee's `attention` wake: one role-independent trigger ahead of every per-role list, one acked session per demand; a spec on paper until `duty.sh` polls it (#86).
|
||
- `attention` doctrine — define its assignee-owned pickup, ack, queue and clock semantics across labels, triage, and builder roles (#85).
|
||
- `attention` — add the issue-only, hand-set assignee-demand flag to the core label taxonomy (#84).
|
||
- One issue at a time counts build work in flight: the parked claim's three shapes, its declared-never-inferred comment, and triage's duty to name a directed hold as a park (#77).
|
||
- FLEET.md — the operator notifier's `needs-ruling` queue (one tracked message per item, edited in place across the rungs) and triage's past-24h wake condition; a spec on paper until an operator updates the box (#74).
|
||
- The sweep observes the escalation contract: a malformed escalation is named field-by-field, and the ladder's 12h/24h rungs each draw one comment to the flag-setter — comment-only, per-episode, both surfaces (#73).
|
||
- Ruling doctrine — define every human-owned trigger, the fixed escalation shape, and the 0–24h builder-to-triage ladder (#72).
|
||
- `issueflow-reconcile` — nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69).
|
||
- `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68).
|
||
- `issueflow-reconcile` — keep cross-repo references out of local dependency decisions and require triage to resolve cross-repo blockers by hand (#61).
|
||
- `actions/runner-isolated` — a `pull_request`-triggered job may never run on a self-hosted runner (#58).
|
||
- Cross-repo doctrine: the panel is the PR's repo's roster, a review request is authorization but not panel membership, and `Part of <repo>#N` replaces the `Closes #N` that cannot cross repos (#57).
|
||
- The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52).
|
||
- `needs-ruling` — the cross-cutting flag for a pending human decision, excluded from `state:needs-human` and from the staleness sweep (#51).
|
||
|
||
## 0.1.0 — 2026-07-22
|
||
|
||
- `lib/version.sh` — one version abstraction, `file` and `package-json` backends (#3).
|
||
- `lib/changelog.sh` + `bin/changelog-section` — the one canonical changelog-section extractor (#4).
|
||
- `actions/changelog-armed` — the version-keyed arming guard (#5).
|
||
- `actions/changelog-monotonic` — shipped release headings are append-only: no deletion, no duplication (#6).
|
||
- `actions/drill-recorded` — a release tree must carry its drill record (#7).
|
||
- `lib/decide.sh` — the merge door's five-state decision, pure and exhaustively tested (#8).
|
||
- `.github/workflows/release.yml` + `lib/facts.sh` — the reusable two-door release workflow (#9).
|
||
- `.github/workflows/labels.yml` + `actions/labels-reconcile` — label taxonomy bootstrap and PR-state reconciliation (#10).
|
||
- Ceremony adopts its own ceremony: `VERSION`, this changelog, the drill doctrine, the self-callers, and the self-guards in CI (#11).
|