Compare commits

..

16 commits

Author SHA1 Message Date
github-actions[bot]
84bb1a424d chore: bump main to 0.4.1-dev — a dev install must not impersonate 0.4.0
Some checks are pending
CI / test (push) Waiting to run
CI / release-exercise (push) Waiting to run
CI / self-guards (push) Waiting to run
CI / action-exercise (push) Waiting to run
CI / docs-sync-exercise (push) Waiting to run
release / release (push) Has been skipped
2026-07-29 12:27:50 +00:00
Daniel Marin
0a54e31972
Merge pull request #207 from codex-bot-andresmgsl/build/206-release-0-4-0
release: cut 0.4.0
2026-07-29 13:27:32 +01:00
codex-bot-andresmgsl
98c8dc2eb4 docs: record 0.4.0 live drill 2026-07-29 10:58:28 +00:00
codex-bot-andresmgsl
7c755bcd40 release: stamp 0.4.0 candidate 2026-07-29 10:51:51 +00:00
Daniel Marin
fa04d67033
Merge pull request #204 from codex-bot-andresmgsl/build/203-sweep-cadence-manual-sweep
docs: explain sweep cadence and manual dispatch
2026-07-29 11:40:36 +01:00
codex-bot-andresmgsl
4198597834 docs: correct labels maintenance cadence 2026-07-29 10:37:03 +00:00
codex-bot-andresmgsl
3eff28e624 docs: explain sweep cadence and manual dispatch 2026-07-29 09:27:29 +00:00
Daniel Marin
1de6b742f8
Merge pull request #202 from codex-bot-andresmgsl/build/198-contributing-flow-boundary
docs: make BUILDER the single PR flow contract
2026-07-29 10:23:37 +01:00
codex-bot-andresmgsl
6fe549cdb7 docs: make BUILDER the single PR flow contract 2026-07-28 22:31:55 +00:00
Daniel Marin
c4b49cd8d3
Merge pull request #200 from claude-bot-andresmgsl/build/199-labels-sweep-cadence
labels: reduce full-board sweep trigger frequency (#199)
2026-07-28 23:15:09 +01:00
claude-bot-andresmgsl
8cf3c335df labels: name what the hourly cron uniquely covers (#199 round)
The trigger comments, CONSUMERS stub, changelog, and reusable labels.yml
comment said "events carry every real state change in seconds; the cron
only backstops a forgotten handoff." That is backwards: no subscribed
event wakes this sweep for a review verdict landing (no pull_request_review
trigger), blocker:ci-red set/cleared, blocker:conflict when another PR
merges, or the time-based stale/48h-reclaim. The hourly cron is the sole
discovery path for those four classes, not a forgotten-handoff net —
so the comments now name them and warn against deleting the cron (AC5).

codex-2 / kimi-2 (both blocking, round 2 @0a812c4b).
2026-07-28 19:26:40 +00:00
claude-bot-andresmgsl
0a812c4b19 labels: keep edited/reopened on issues; correct 0.3.0 adoption prose (#199 round)
Round fixes on #200.

codex-1 (blocking): the issues narrowing dropped `edited`/`reopened`, but both
carry a queue-state change an event uniquely carries — `edited` a body rewrite
of the `Blocked by #N` declaration the sweep parses
(issueflow-reconcile.sh:179), `reopened` a closed issue re-entering the queue.
Dropping them tripped #199's must-fail. Narrow to
`[opened, closed, edited, reopened]`, dropping only the churn/validation
actions labeled/unlabeled/assigned/unassigned. Trigger tests now pin
edited/reopened present and the four dropped; labels.test.sh exact-list updated.

kimi (blocking): the "supersedes unreleased #144" prose was false — #144's
edited/reopened shipped in 0.3.0. Dissolved: we now keep them. CONSUMERS prose
rewritten to the real version history (0.2.0 #32 / 0.3.0 #144 / #199 narrows),
and the #137 review-request line corrected from "unreleased" to shipped-in-0.3.0.

kimi (non-blocking): reusable labels.yml comment no longer cites */15.

codex-2 (AC1 after-measurement / closing) escalated to triage on #199 — held,
not guessed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 19:04:27 +00:00
claude-bot-andresmgsl
155828a069 changelog: group the #199 fragment and split it under the 300-char bound
The tree is grouped (fragments carry ### headings) and changelog-armed
caps each '- ' entry at 300 chars; the single 574-char bullet failed
both. Rewrite as three '### Changed' bullets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:24:23 +00:00
claude-bot-andresmgsl
f8ad0b34c7 test: guard the #199 trigger surface; update #144 parity to [opened, closed]
Add test/labels-triggers.test.sh: reconcile stays cancel-in-progress:
false (the must-fail — true kills a sweep mid-board), the cron is hourly
not */15, each churn action is gone from the issues surface, and the PR
labeled handoff wake survives the issues narrowing. Update the #137/#144
parity block in labels.test.sh to the narrowed [opened, closed] contract
and replace its fragile inline pull_request_target scan (the #199 prose
comments name the trigger keys) with the anchored event_types reader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:21:48 +00:00
claude-bot-andresmgsl
12887cc9de docs: narrow CONSUMERS stub issues triggers, relax cron to hourly (#199)
Bring the published caller stub to the #199 shape in lockstep with
self-labels.yml (preserving the stub<->caller parity #144 established):
hourly cron, issues: [opened, closed], per-trigger why-comments. Rewrite
the adoption prose — 0.2.0 shipped the broad set; #199 narrows it at its
pin bump and supersedes the unreleased edited/reopened additions (#144).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:13:15 +00:00
claude-bot-andresmgsl
dfafeedeec labels: relax self caller cron to hourly, narrow issues triggers (#199)
The advisory */15 cron fired a full-board sweep four times an hour at
GitHub's 1-minute billing floor; events carry real state changes in
seconds, so the cron only backstops a forgotten handoff. Relax it to
hourly and narrow the issues: trigger to [opened, closed] — the two
actions with promptness-critical reconcile behavior — dropping the
labeled/unlabeled churn source. Per-trigger why-comments added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:10:23 +00:00
24 changed files with 388 additions and 196 deletions

View file

@ -10,11 +10,14 @@ name: labels
# and reconcile checks out the BASE branch only. Keep it that way. # and reconcile checks out the BASE branch only. Keep it that way.
# #
# There is no pull_request_review_target, so a review landing cannot wake this # There is no pull_request_review_target, so a review landing cannot wake this
# workflow directly — and the */15 cron is advisory: GitHub deprioritises # workflow directly — which is why the caller's cron is load-bearing, not a
# short intervals hard enough that a quiet repo goes hours between ticks. The # safety net (#199 relaxed it from */15 to hourly, but did NOT drop it). The
# handoff wakes the sweep itself: the author sets state:needs-human, and the # cron is the sweep's only discovery path for every transition no subscribed
# caller's `labeled` event confirms or corrects that optimistic write within # event carries: a verdict landing, blocker:ci-red set/cleared, a
# seconds. The cron stays as the last resort for a forgotten handoff. # blocker:conflict when another PR merges under this one, and the time-based
# stale / 48h claim-reclaim. Where an event IS subscribed the wake is direct —
# the handoff sets state:needs-human and the caller's `labeled` event confirms
# or corrects that optimistic write within seconds.
# #
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not # This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a # create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a
@ -25,7 +28,7 @@ on:
env: env:
# A called workflow arrives without its repository. Keep this literal pin # A called workflow arrives without its repository. Keep this literal pin
# aligned with the ceremony release consumed by callers (issue #9 D3). # aligned with the ceremony release consumed by callers (issue #9 D3).
CEREMONY_SELF_REF: "0.3.0" CEREMONY_SELF_REF: "0.4.0"
jobs: jobs:
scope: scope:

View file

@ -129,7 +129,7 @@ env:
# `ref:` accepts ${{ env }}; `uses:` strings do not — which is why the # `ref:` accepts ${{ env }}; `uses:` strings do not — which is why the
# shared logic arrives as script files via checkout, not as inner `uses:` # shared logic arrives as script files via checkout, not as inner `uses:`
# references. # references.
CEREMONY_SELF_REF: "0.3.0" CEREMONY_SELF_REF: "0.4.0"
VERSION_SOURCE: ${{ inputs.version-source }} VERSION_SOURCE: ${{ inputs.version-source }}
jobs: jobs:

View file

@ -5,11 +5,40 @@ name: labels
# unpinned — correct only for the repo that IS the source). Consumers write: # unpinned — correct only for the repo that IS the source). Consumers write:
# uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag> # uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
on: on:
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake # The consumer owns this cadence (#203). Hourly is the recommended default
workflow_dispatch: # bootstraps missing labels on a fresh repo # when no other engine drives board state: the cron is then the sweep's ONLY
# wake for four transition classes — a review verdict landing (there is no
# pull_request_review trigger here), blocker:ci-red set or cleared (no
# check_suite/check_run/workflow_run), a blocker:conflict when ANOTHER PR
# merges under this one, and the time-based stale / 48h claim-reclaim. The
# events below carry the rest in seconds. Hourly trades ≤1h of latency on
# those four while cutting nominal scheduled sweeps from four an hour to one
# at GitHub's 1-minute billing floor. Do not delete the cron: it is their
# discovery path. If another engine writes some of those transitions, only
# the classes with no other writer bound the cadence; relax it only as that
# list shrinks.
schedule: [{cron: "0 * * * *"}]
# A manual full-board sweep, including taxonomy bootstrap on a fresh repo.
workflow_dispatch:
# Narrowed (#199) to the actions that carry a queue-state change the hourly
# cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
# unassigned, which feed validation and the 48h claim clock (caught within one
# cadence) and were the dominant issues-churn source. Kept: `opened` (the
# mint→needs-triage check, issueflow's opened-only path), `closed` (the
# blocker-closes→ready self-heal, crew#96/#98), `edited` (a body rewrite of the
# `Blocked by #N` declaration the sweep parses — issueflow-reconcile.sh:179),
# `reopened` (a closed issue re-entering the queue wearing labels derived when
# it closed). The must-fail in #199 is exactly "a queue-state transition waits
# on the schedule when an event could have carried it", so edited/reopened stay
# on events. The PR handoff wake is pull_request_target:labeled, NOT issues, so
# this does not touch the handoff.
issues: issues:
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened] types: [opened, closed, edited, reopened]
pull_request_target: pull_request_target:
# Every PR arrives from a fork, so these carry the head/draft/review facts
# the sweep derives state:* from. labeled/unlabeled are the handoff wake —
# the author's optimistic state:needs-human write, confirmed or corrected
# here in seconds (#11); synchronize re-derives on every push;
# review_requested/review_request_removed wake the sweep that clears (or # review_requested/review_request_removed wake the sweep that clears (or
# restores) blocker:unrequested — without them the one event that makes # restores) blocker:unrequested — without them the one event that makes
# the label false could not clear it, and a quiet repo wore the red flag # the label false could not clear it, and a quiet repo wore the red flag

View file

@ -208,9 +208,9 @@ triage bug, and the move is to say so on the issue, not to guess.
## The review round ## The review round
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo: the (If you are reading this as `.ceremony/BUILDER.md` in a governed repo:
panel roster and any repo-specific flow notes live in that repo's own repo-specific facts such as the panel roster live in that repo's own
CONTRIBUTING; everything below is the shared flow.) CONTRIBUTING; the shared flow lives here and is not restated there.)
1. Mark ready-for-review; request **the whole panel**. The panel is the roster 1. Mark ready-for-review; request **the whole panel**. The panel is the roster
of the repo the **PR** is in, minus you — never the roster of the repo the of the repo the **PR** is in, minus you — never the roster of the repo the

View file

@ -9,6 +9,102 @@ 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 an edit to this file — and the release PR assembles them into the next
section here (`bin/changelog-assemble`, #112). section here (`bin/changelog-assemble`, #112).
## 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 ## 0.3.0 — 2026-07-24
- Make `changelog-armed` reject fragment shape drift on the PR that introduces it. - Make `changelog-armed` reject fragment shape drift on the PR that introduces it.

View file

@ -35,45 +35,9 @@ Who may set which label is [LABELS.md](LABELS.md)'s contract.
## The PR flow ## The PR flow
The same flow the sibling repos run, and the part of this pipeline that is PRs move through review rounds that builders answer whole, and only a human
already proven: merges. [BUILDER.md](BUILDER.md) is the shared flow contract; this file names
only ceremony-specific facts such as the roster and code conventions.
1. **One issue, one PR**, opened as a **draft** while building, with
`Closes #N` in the body — its exceptions (cross-repo work, a post-merge
criterion) live in [BUILDER.md](BUILDER.md). Drafts are invisible to the
reviewer panel on purpose. Every behavior change writes one fragment,
`changelog.d/<issue>.md` — the exact prose to publish, nothing else
(cross-repo work names it `<repo>-<issue>.md`; a grouped repo puts its
`### Added` / `### Changed` / `### Fixed` headings inside the fragment).
Never edit `CHANGELOG.md` for an entry — the release PR assembles the
section from the fragments (#112).
The sole exception is the release PR: it writes no fragment. It consumes
the directory and stamps the section, so a fragment it created would be
absent from
[`changelog-assembled`](https://github.com/heavy-duty/ceremony/blob/a602fd0/actions/changelog-assembled/changelog-assembled.sh)'s
merge-base replay if consumed, or refused by
[`changelog-armed`](https://github.com/heavy-duty/ceremony/blob/a602fd0/actions/changelog-armed/changelog-armed.sh)
if left to survive into the next release. A change that must ship inside
the release PR therefore ships without an entry. If it can wait and wants
an entry, land it as an ordinary PR before the release PR, then rebase and
re-assemble the release.
2. **When it's ready**: mark ready-for-review and request the whole panel.
3. **Rounds are answered whole.** Wait until every reviewer has a verdict in,
then answer the entire round in a **single reply**, push the fixes, and
re-request the reviewers that didn't approve. Prefer verification over
argument: a test settles what a comment thread can't.
4. **Reviews end in a verdict** — approve or request-changes, never a bare
comment. The verdict carries blockingness only; the body carries the
feedback. ([REVIEWER.md](REVIEWER.md) for why a comment-only review stalls
the machine.)
5. **Handoff**: when the round passes — every panel verdict is an approval of
the current head and no `blocker:*` label stands — the engine requests the
human's review, sets `state:needs-human`, and posts the handoff facts
(approvals at the current head, head SHA, and Round-log pointer) on the
author's behalf. The label write is optimistic; the reconciler validates
it within seconds.
6. **A human merges.** Nothing else merges.
### Roster ### Roster

View file

@ -3,7 +3,8 @@
The taxonomy shared across the heavy-duty repos. Only the `scope:` set The taxonomy shared across the heavy-duty repos. Only the `scope:` set
differs per repo (each repo's `.github/labels.conf` names its actual differs per repo (each repo's `.github/labels.conf` names its actual
surfaces); everything else below is core and identical everywhere, created by surfaces); everything else below is core and identical everywhere, created by
the labels workflow's bootstrap dispatch (issue #10). the labels workflow's dispatch (which is also the operator's manual
full-board reconcile sweep; issue #10).
Two state machines share the taxonomy: the **PR machine** (proven in Two state machines share the taxonomy: the **PR machine** (proven in
box/rig/cast, reconciled by machinery) and the **issue flow** (the box/rig/cast, reconciled by machinery) and the **issue flow** (the
@ -211,10 +212,13 @@ on a PR would say the same thing twice and drift.
## Maintenance ## Maintenance
The labels workflow (issue #10) recomputes PR state statelessly on PR events The labels workflow (issue #10) recomputes PR state statelessly on subscribed
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on events plus a consumer-owned scheduled discovery sweep. Hourly is the
manual dispatch. The sweep warns when the core taxonomy declares a label the recommended default when no other engine drives board state; relax it only as
repository lacks. The same workflow reconciles issue-flow labels on issue the transition classes with no other writer shrink. Manual dispatch both
bootstraps this taxonomy idempotently and runs the operator's on-demand
full-board reconcile. The sweep warns when the core taxonomy declares a label
the repository lacks. The same workflow reconciles issue-flow labels on issue
events and during the scheduled sweep. Default GitHub labels (`duplicate`, events and during the scheduled sweep. Default GitHub labels (`duplicate`,
`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are `invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
deleted at bootstrap — a `question` is a discussion, not an issue. deleted at bootstrap — a `question` is a discussion, not an issue.

View file

@ -1 +1 @@
0.3.1-dev 0.4.1-dev

View file

@ -1,7 +0,0 @@
### Added
- `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).

View file

@ -1,3 +0,0 @@
### Changed
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).

View file

@ -1,3 +0,0 @@
### Added
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.

View file

@ -1,5 +0,0 @@
### Changed
- 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).

View file

@ -1,5 +0,0 @@
### Fixed
- `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).

View file

@ -1,10 +0,0 @@
### 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).
### Changed
- Ceremony's changelog is grouped from this release forward: the pending
fragments carry `### ` headings under a `grouped` sentinel (#182).

View file

@ -1,6 +0,0 @@
### Fixed
- `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).

View file

@ -1,13 +0,0 @@
### 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).
### Changed
- 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).

View file

@ -1,27 +0,0 @@
### Changed
- `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).

View file

@ -1,5 +0,0 @@
### Changed
- `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).

View file

@ -1,5 +0,0 @@
### Changed
- `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).

View file

@ -1,5 +0,0 @@
### Changed
- 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).

View file

@ -122,7 +122,8 @@ the machinery at all:
[Labels automation](#labels-automation), plus `.github/labels.conf` [Labels automation](#labels-automation), plus `.github/labels.conf`
(panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the (panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the
path→scope globs). Run `workflow_dispatch` once — **this bootstraps path→scope globs). Run `workflow_dispatch` once — **this bootstraps
the taxonomy, `release` label included**. the taxonomy, `release` label included** — and use it again whenever an
operator needs a full-board sweep immediately.
7. **The artifact hook** (optional): `.github/actions/release-artifact/` 7. **The artifact hook** (optional): `.github/actions/release-artifact/`
per [The artifact hook](#the-artifact-hook). No hook → the source per [The artifact hook](#the-artifact-hook). No hook → the source
tarball is the package. tarball is the package.
@ -296,20 +297,39 @@ The complete caller is:
```yaml ```yaml
name: labels name: labels
on: on:
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake # The consumer owns this cadence (#203). Hourly is the recommended default
workflow_dispatch: # bootstraps missing labels on a fresh repo # when no other engine drives board state: the cron is then the sweep's only
# wake for four transition classes — a review verdict landing (no
# pull_request_review trigger), blocker:ci-red set/cleared, blocker:conflict
# when another PR merges under this one, and time-based stale / 48h
# claim-reclaim. Events below carry the rest in seconds. Hourly trades ≤1h of
# latency on those four while cutting nominal scheduled sweeps from four an
# hour to one at GitHub's 1-minute floor. Do not delete the cron: it is their
# discovery path. If another engine writes some of those transitions, only
# the classes with no other writer bound the cadence; relax it only as that
# list shrinks.
schedule: [{cron: "0 * * * *"}]
# A manual full-board sweep, including taxonomy bootstrap on a fresh repo.
workflow_dispatch:
pull_request_target: pull_request_target:
# review_requested/review_request_removed are unreleased — not in 0.2.0; # Fork PRs; these carry the head/draft/review facts state:* derives from.
# add them with the pin bump to the first tag carrying ceremony#137. They # labeled/unlabeled are the handoff wake (state:needs-human confirmed here);
# wake the sweep that clears blocker:unrequested when the panel is asked. # synchronize re-derives on every push. review_requested/review_request_removed
# (shipped in 0.3.0, ceremony#137) wake the sweep that clears
# blocker:unrequested when the panel is asked.
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed] types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
# Available at 0.2.0 and later (the first tag carrying ceremony#32); a # Available at 0.2.0 and later (the first tag carrying ceremony#32); a
# consumer pinned to 0.1.0 omits this block. # consumer pinned to 0.1.0 omits this block.
issues: issues:
# edited/reopened are unreleased — not in 0.2.0; add them with the pin # Narrowed (#199) to the actions carrying a queue-state change the hourly
# bump to the first tag carrying ceremony#144, the same bump as the # cron cannot wait one cadence for: opened → the mint→needs-triage check,
# pull_request_target additions above. # closed → the blocker-closes→ready self-heal, edited → a body rewrite of the
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened] # `Blocked by #N` declaration the sweep parses, reopened → a closed issue
# re-entering the queue. Dropped: labeled/unlabeled/assigned/unassigned —
# validation + the 48h claim clock, caught within one cadence, and
# labeled/unlabeled were the issues-churn source. The handoff wake is
# pull_request_target:labeled, not issues, so this leaves it intact.
types: [opened, closed, edited, reopened]
permissions: permissions:
contents: read contents: read
checks: read # mergeability/check-rollup read for PR state checks: read # mergeability/check-rollup read for PR state
@ -330,23 +350,32 @@ needs all three explicit reads above; without them the failure appears as an emp
The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the
first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt
it only by bumping every ceremony reference to `0.2.0` or later; never mix it only by bumping every ceremony reference to `0.2.0` or later; never mix
refs to adopt it early. At `0.2.0` the type list stops at `closed`: `edited` refs to adopt it early. The type list has grown then narrowed across tags:
and `reopened` are newer, part of the pending stub edit below. `0.2.0` (ceremony#32) shipped `[opened, labeled, unlabeled, assigned,
unassigned, closed]`; `0.3.0` (ceremony#144) added `edited` and `reopened`;
ceremony#199 narrows it to `[opened, closed, edited, reopened]` and relaxes the
cron to hourly, so a consumer picks up the smaller trigger surface at the pin
bump to the first tag carrying ceremony#199. The narrowing drops
`labeled`/`unlabeled`/`assigned`/`unassigned` — validation and the 48h claim
clock, which the hourly cron catches within one cadence, and `labeled`/
`unlabeled` were the issues-churn source — while **keeping** #144's `edited`/
`reopened`: those carry a queue-state change an event uniquely carries (a body
rewrite of `Blocked by #N`, and a closed issue re-entering the queue), so the
must-fail in ceremony#199 keeps them on events. `opened` drives the
mint→`needs-triage` check and `closed` the blocker-closes→`ready` self-heal;
the stub and ceremony's own caller stay byte-for-byte identical, the parity
#144 established.
`pull_request_target` is intentional: fork PRs need the base repository's `pull_request_target` is intentional: fork PRs need the base repository's
token to write labels. The reusable workflow executes no PR code. It checks token to write labels. The reusable workflow executes no PR code. It checks
out only the consumer's base branch and the pinned ceremony implementation. out only the consumer's base branch and the pinned ceremony implementation.
The #52 ruling invariants ride exactly these triggers — but the caller above The #52 ruling invariants ride exactly these triggers — but the caller above
is no longer the #18 shape, so adopting current triggers is a stub edit, not is no longer the #18 shape, so adopting current triggers is a stub edit, not
a bare pin bump. The pending edits are `review_requested` and a bare pin bump. `review_requested` and `review_request_removed` on
`review_request_removed` on `pull_request_target:` (#137) — the wake that `pull_request_target:` shipped in `0.3.0` (ceremony#137) — the wake that
clears `blocker:unrequested` the moment the panel is asked, without which a clears `blocker:unrequested` the moment the panel is asked, without which a
quiet repo wears that flag until the advisory cron — and `edited` and quiet repo wears that flag until the backstop cron; a consumer picks them up
`reopened` on `issues:` (#144): the wakes for a body edit rewriting the by pinning `0.3.0` or later, never through mixed refs.
`Blocked by #N` declaration the reconcile sweep parses, and for a closed
issue re-entering the queue wearing labels derived when it closed. Make both
edits together, with the pin bump to the first tag carrying ceremony#137 and
ceremony#144 — never before it and never through mixed refs.
`.github/labels.conf` has one mandatory panel setting, one mandatory `.github/labels.conf` has one mandatory panel setting, one mandatory
`triage-actors` setting, and then zero or more scope rows: `triage-actors` setting, and then zero or more scope rows:
@ -375,11 +404,20 @@ Core state, blocker, work-queue, and release labels come from ceremony. Scope
rows remain consumer-owned because paths and surfaces differ by repository. rows remain consumer-owned because paths and surfaces differ by repository.
After adding the caller and configuration, run `workflow_dispatch` once to After adding the caller and configuration, run `workflow_dispatch` once to
bootstrap labels on a fresh repository. Scheduled and PR-triggered runs only bootstrap labels on a fresh repository. It is also the operator's general
reconcile; they do not repeatedly upsert the taxonomy. When a ceremony pin manual full-board sweep — the answer when the board looks wrong now rather
bump adds a core label, bump the pin first and then re-dispatch than after the next scheduled cadence:
`workflow_dispatch`; the scheduled sweep warns when the pinned taxonomy
declares a core label the repository lacks. ```sh
gh workflow run labels.yml -R <owner>/<repo>
```
Ceremony dogfoods the caller under the filename `self-labels.yml`, so the
equivalent command in this repository substitutes that filename. Scheduled
and PR-triggered runs only reconcile; they do not repeatedly upsert the
taxonomy. When a ceremony pin bump adds a core label, bump the pin first and
then re-dispatch `workflow_dispatch`; the scheduled sweep warns when the
pinned taxonomy declares a core label the repository lacks.
## Doctrine mirror ## Doctrine mirror

50
drills/0.4.0.md Normal file
View file

@ -0,0 +1,50 @@
# 0.4.0 — drill record
Run 2026-07-29 by `codex-bot-andresmgsl` against release PR #207 head
`7c755bcd402ba7f9a38ecd406a025c149c77aa57`.
Where: disposable private repo
`codex-bot-andresmgsl/ceremony-drill-0.4.0`, carrying the
`docs/CONSUMERS.md` release caller and a fragment-mode fixture armed at
`0.4.0-dev`. The fixture had `changelog.d/README.md`, one release fragment,
and a non-blank drill record. The repository is archived, pending the
operator's delete.
## Candidate-ref deviation
The pure consumer path cannot resolve `CEREMONY_SELF_REF: "0.4.0"` before
the candidate creates that tag. No `0.4.0` branch was created in
`heavy-duty/ceremony`. The scratch caller instead used
`codex-bot-andresmgsl/ceremony@drill/0.4.0`, whose parent is PR #207 head
`7c755bcd402ba7f9a38ecd406a025c149c77aa57` and whose only additional
commit rewrites both `CEREMONY_SELF_REF` carriers to that same canonical
candidate SHA. All runtime machinery was therefore fetched from the 0.4.0
candidate tree.
## Probes
| # | probe | run | result |
|---|---|---|---|
| 1 | merge-door ceremony | 30445585532 (attempt 1) | ✅ one `0.4.0` release; tag equals merge commit; main re-armed to `0.4.1-dev` with only `changelog.d/README.md` |
| 2 | mislabeled ordinary PR | 30445473977 | ✅ green NOTICE no-op; no tag or release |
| 3 | bare-version PR without `release` | 30445513860 | ✅ refused at decide; no tag or release |
| 4 | re-run completed ceremony | 30445585532 (attempt 2) | ✅ refused because the tag/release already existed; release count stayed one |
| 5 | manual matching tag | 30445658952 | ✅ `0.5.0` published with its changelog section; main untouched |
| 6 | mismatched tag | 30445684268 | ✅ refused before publication; no `9.9.9` release, and the probe tag was removed afterward |
The merge-door `0.4.0` tag and PR #3 merge commit were both
`2b2d592ffcc5c376e55bd5fcf2dd5ffbdd692d64`. Its release body was exactly:
```text
- Fragment mode is exercised by the 0.4.0 drill.
```
## Setup
The armed fixture was committed before the caller so the first workflow run
had a real parent version to inspect. Installing the caller then produced
green baseline run 30445432039.
After the unlabeled bare-version refusal, its merge commit was reverted to
restore the armed fixture. That setup correction produced green no-op run
30445544068 before the merge-door probe began.

View file

@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -u
# The labels TRIGGER SURFACE is a cost lever (#199): a full-board sweep is
# billed a 1-minute minimum every time a trigger fires, so how OFTEN it fires
# is what exhausted the fleet's shared Actions allotment. These assertions
# pin the reductions #199 made and the guard it must not trade away — none of
# them touch the reconciler's LOGIC, which its own fixtures cover.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
source "$ROOT/test/harness.sh"
REUSABLE="$ROOT/.github/workflows/labels.yml"
SELF="$ROOT/.github/workflows/self-labels.yml"
STUB="$ROOT/docs/CONSUMERS.md" # the published caller stub, a fenced yaml block
# The `cancel-in-progress:` value of a named top-level job, read from the first
# such line inside that job's block. Job keys sit at two-space indent.
job_cancel_in_progress() { # $1 = file, $2 = job name
awk -v job="^ $2:\$" '
$0 ~ job { f = 1; next }
f && /^ [a-z]/ { exit } # next job — stop before leaking into it
f && /cancel-in-progress:/ { sub(/.*cancel-in-progress:[[:space:]]*/, ""); print; exit }
' "$1"
}
# The `types:` list of a trigger key (issues:, pull_request_target:), read from
# the first `types:` line after the bare key. The key is bare (nothing after
# the colon) so it never collides with `issues: write` in the permissions block.
trigger_types() { # $1 = file, $2 = trigger key
awk -v key="^ $2:\$" '
$0 ~ key { f = 1; next }
f && /^ types:/ { sub(/^ types:[[:space:]]*/, ""); print; exit }
f && /^ [a-z]/ { exit }
' "$1"
}
# ---- the guard the cost fix must never trade away (#199 test plan must-fail) --
# cancel-in-progress: true on reconcile kills a sweep mid-board, the exact race
# the shared concurrency group exists to prevent. It WOULD cut run count — by
# trading correctness for minutes — so it stays false, forever.
check "reconcile serializes, never cancels mid-board" 0 "false" \
job_cancel_in_progress "$REUSABLE" reconcile
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
check "reconcile is never cancel-in-progress: true" 1 "" \
bash -c 'job_cancel_in_progress() {
awk -v job="^ reconcile:\$" "\$0 ~ job{f=1;next} f&&/^ [a-z]/{exit} f&&/cancel-in-progress:/{sub(/.*cancel-in-progress:[[:space:]]*/,\"\");print;exit}" "$1"
}; [ "$(job_cancel_in_progress "$1")" = true ]' _ "$REUSABLE"
# scope MAY cancel — it is per-PR and additive, so a superseded run is waste,
# not a lost sweep. This asserts the must-fail above is scoped to reconcile.
check "scope stays cancel-in-progress: true (per-PR, additive)" 0 "true" \
job_cancel_in_progress "$REUSABLE" scope
# ---- the cron is a backstop, relaxed to hourly (#199 candidate 1) -----------
# Scope the */15 assertion to the cron LINE — the prose comments cite */15 by
# name to explain the change, and must not re-red their own documentation.
check "self caller cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$SELF"
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
check "self caller cron line no longer fires */15" 1 "" \
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$SELF"
check "stub cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$STUB"
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
check "stub cron line no longer fires */15" 1 "" \
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$STUB"
# ---- issues: is narrowed to the queue-state-changing actions (#199) ----------
# Kept because each carries a queue-state change an event uniquely carries, so
# dropping it would trip #199's must-fail (a transition waiting on the schedule
# when an event could have carried it): opened → mint→needs-triage; closed →
# blocker-closes→ready self-heal; edited → a body rewrite of the `Blocked by #N`
# declaration the sweep parses; reopened → a closed issue re-entering the queue.
# (labels.test.sh owns the exact-list and caller<->stub parity assertions.)
for keep in opened closed edited reopened; do
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
check "self caller issues surface keeps '$keep'" 0 "" \
bash -c 'trigger_types() {
awk -v key="^ issues:\$" "\$0 ~ key{f=1;next} f&&/^ types:/{sub(/^ types:[[:space:]]*/,\"\");print;exit} f&&/^ [a-z]/{exit}" "$1"
}; trigger_types "$1" | grep -qw "$2"' _ "$SELF" "$keep"
done
# The churn actions must not reappear on the issues surface without a fresh why.
# labeled/unlabeled were the dominant issues-churn source; assigned/unassigned
# only feed validation and the 48h claim clock, caught within one cadence.
for churn in labeled unlabeled assigned unassigned; do
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
check "self caller issues surface drops '$churn'" 1 "" \
bash -c 'trigger_types() {
awk -v key="^ issues:\$" "\$0 ~ key{f=1;next} f&&/^ types:/{sub(/^ types:[[:space:]]*/,\"\");print;exit} f&&/^ [a-z]/{exit}" "$1"
}; trigger_types "$1" | grep -qw "$2"' _ "$SELF" "$churn"
done
# ---- the PR handoff wake is NOT collateral of the issues narrowing ----------
# The handoff (state:needs-human, confirmed by the caller's labeled event) rides
# pull_request_target, not issues. A future edit that strips it there re-reds.
check "pull_request_target keeps the labeled handoff wake" 0 "labeled" \
trigger_types "$SELF" pull_request_target
summary

View file

@ -69,15 +69,16 @@ check "LABELS.md enumerates no repo's scope labels" 1 "0" \
# same lists. review_requested/review_request_removed are the wake that # same lists. review_requested/review_request_removed are the wake that
# clears blocker:unrequested — the label sat false for as long as a quiet # clears blocker:unrequested — the label sat false for as long as a quiet
# repo stayed quiet because the one event that falsifies it was never # repo stayed quiet because the one event that falsifies it was never
# listed (#137). edited/reopened are the wakes for the two events that # listed (#137). The issues list narrowed to [opened, closed, edited, reopened]
# falsify issue labels silently — an edited body rewrites the `Blocked by # (#199): each carries a queue-state change the hourly cron cannot wait one
# #N` declaration the reconcile sweep parses, and a reopened issue # cadence for — opened drives mint→needs-triage, closed the blocker-closes→ready
# re-enters the queue wearing labels derived at close; PR #32 widened the # self-heal, edited a body rewrite of the `Blocked by #N` line the sweep parses,
# caller by both and the stub never followed (#144). The stub is prose, so # reopened a closed issue re-entering the queue — while the churn/validation
# nothing but these rows keeps the lists from drifting: a type in one file # actions (labeled/unlabeled/assigned/unassigned) came off. The stub is prose, so nothing but these rows
# only is a wake that fires at home and nowhere in the fleet, or the # keeps the lists from drifting: a type in one file only is a wake that fires
# reverse. The NF guard keeps `issues: write` under permissions: from # at home and nowhere in the fleet, or the reverse — the drift #144 caught.
# matching the issues: trigger key. # The NF guard keeps `issues: write` under permissions: from matching the
# issues: trigger key.
event_types() { # $1 = file, $2 = trigger key → that trigger's types line, unindented event_types() { # $1 = file, $2 = trigger key → that trigger's types line, unindented
awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1" awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
} }
@ -88,17 +89,20 @@ types_in_sync() { # $1 = trigger key, $2 = caller, $3 = stub → 0 when both lis
} }
CALLER="$ROOT/.github/workflows/self-labels.yml" CALLER="$ROOT/.github/workflows/self-labels.yml"
STUB="$ROOT/docs/CONSUMERS.md" STUB="$ROOT/docs/CONSUMERS.md"
# event_types anchors on the bare trigger key (NF == 1), so it reads the real
# types line even though the #199 comments name pull_request_target: and
# issues: in prose above the keys — an inline /pull_request_target:/ scan would
# latch onto the first mention and read the wrong list.
pr_has_both_review_wakes() {
event_types "$CALLER" pull_request_target | grep -F review_requested | grep -qF review_request_removed
}
check "caller and stub pull_request_target lists are identical" 0 "" \ check "caller and stub pull_request_target lists are identical" 0 "" \
types_in_sync pull_request_target "$CALLER" "$STUB" types_in_sync pull_request_target "$CALLER" "$STUB"
# shellcheck disable=SC2016 # expansion belongs to the nested bash check "the caller lists both review-request wakes" 0 "" pr_has_both_review_wakes
check "the caller lists both review-request wakes" 0 "" bash -c \
'awk "/pull_request_target:/{f=1; next} f && /types: /{print; exit}" "$1" |
grep -F review_requested | grep -qF review_request_removed' _ "$CALLER"
check "caller and stub issues lists are identical" 0 "" \ check "caller and stub issues lists are identical" 0 "" \
types_in_sync issues "$CALLER" "$STUB" types_in_sync issues "$CALLER" "$STUB"
check "the caller still lists all eight issue types" 0 \ check "the caller lists exactly the queue-state-changing issue types" 0 \
"types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]" \ "types: [opened, closed, edited, reopened]" event_types "$CALLER" issues
event_types "$CALLER" issues
# the failing cases: drop a type from either file, or reorder one list only, # the failing cases: drop a type from either file, or reorder one list only,
# and the identity rows above go red — exercised here on mutated copies # and the identity rows above go red — exercised here on mutated copies
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md" mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"
@ -112,13 +116,13 @@ sed 's/review_requested, review_request_removed/review_request_removed, review_r
"$STUB" >"$mut_stub" "$STUB" >"$mut_stub"
check "a reorder in one list only goes red" 1 "" \ check "a reorder in one list only goes red" 1 "" \
types_in_sync pull_request_target "$CALLER" "$mut_stub" types_in_sync pull_request_target "$CALLER" "$mut_stub"
sed 's/, edited//' "$CALLER" >"$mut_caller" sed 's/, closed//' "$CALLER" >"$mut_caller"
check "an issue type dropped from the caller goes red" 1 "" \ check "an issue type dropped from the caller goes red" 1 "" \
types_in_sync issues "$mut_caller" "$STUB" types_in_sync issues "$mut_caller" "$STUB"
sed 's/, edited//' "$STUB" >"$mut_stub" sed 's/, closed//' "$STUB" >"$mut_stub"
check "an issue type dropped from the stub goes red" 1 "" \ check "an issue type dropped from the stub goes red" 1 "" \
types_in_sync issues "$CALLER" "$mut_stub" types_in_sync issues "$CALLER" "$mut_stub"
sed 's/closed, reopened/reopened, closed/' "$STUB" >"$mut_stub" sed 's/opened, closed/closed, opened/' "$STUB" >"$mut_stub"
check "an issue-list reorder in one file only goes red" 1 "" \ check "an issue-list reorder in one file only goes red" 1 "" \
types_in_sync issues "$CALLER" "$mut_stub" types_in_sync issues "$CALLER" "$mut_stub"