Compare commits
33 commits
d9c5b92dd1
...
84bb1a424d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84bb1a424d | ||
|
|
0a54e31972 | ||
|
|
98c8dc2eb4 | ||
|
|
7c755bcd40 | ||
|
|
fa04d67033 | ||
|
|
4198597834 | ||
|
|
3eff28e624 | ||
|
|
1de6b742f8 | ||
|
|
6fe549cdb7 | ||
|
|
c4b49cd8d3 | ||
|
|
8cf3c335df | ||
|
|
0a812c4b19 | ||
|
|
155828a069 | ||
|
|
f8ad0b34c7 | ||
|
|
12887cc9de | ||
|
|
0e3d8de2a6 | ||
|
|
dfafeedeec | ||
|
|
0405d206ae | ||
|
|
ab77fa93ac | ||
|
|
0a9316f7e1 | ||
|
|
5757e24cd4 | ||
|
|
c96d2d8f4e | ||
|
|
7fc0425184 | ||
|
|
432dff0613 | ||
|
|
4f3fc2a20b | ||
|
|
45934b54a2 | ||
|
|
e3820dbf9a | ||
|
|
44ff7524af | ||
|
|
e9cf461ed3 | ||
|
|
e85a7d42f3 | ||
|
|
203907b0ea | ||
|
|
a414f3105b | ||
|
|
7a49de7efb |
21 changed files with 682 additions and 280 deletions
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
|
@ -16,5 +16,5 @@ to the issue for triage to amend, not silently unshipped. -->
|
||||||
|
|
||||||
## Round log
|
## Round log
|
||||||
|
|
||||||
<!-- Append each round's summary here: what changed, what was verified.
|
<!-- The engine appends each whole-round reply here, newest last: what
|
||||||
Rounds are answered whole — one reply covering every point. -->
|
changed and what was verified. Builders write the reply, not this section. -->
|
||||||
|
|
|
||||||
15
.github/workflows/labels.yml
vendored
15
.github/workflows/labels.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
35
.github/workflows/self-labels.yml
vendored
35
.github/workflows/self-labels.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
125
BUILDER.md
125
BUILDER.md
|
|
@ -11,6 +11,27 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
- Respect dependency order: inside an epic, take the earliest unblocked
|
- Respect dependency order: inside an epic, take the earliest unblocked
|
||||||
unclaimed child. Between epics and strays, prefer the issue that unblocks
|
unclaimed child. Between epics and strays, prefer the issue that unblocks
|
||||||
the most other work.
|
the most other work.
|
||||||
|
- **Your own red head outranks a new claim.** A failing check at the head
|
||||||
|
of a PR you authored is picked up **before claiming another issue** —
|
||||||
|
repairing your own red PR comes ahead of new work, which is why the
|
||||||
|
engine's duty order evaluates ci-red between resume and build (crew#17:
|
||||||
|
ceremony#163 sat with full-panel approvals at its head, mergeable, and
|
||||||
|
stranded on an HTTP 429 in a job that never ran the PR's code, because no
|
||||||
|
wake covered a red head that owed no round and had no conflict). Red and
|
||||||
|
green here are the ruled terms of the review round below: a cancelled or
|
||||||
|
stale check is not a green head; a skipped or neutral one is. The
|
||||||
|
recovery path (crew#17): inspect the check at the head and record the
|
||||||
|
failing check and its failure class; rerun a clearly retryable
|
||||||
|
infrastructure failure without changing code; when the failure belongs to
|
||||||
|
the branch, return to the normal fix-round and worklog discipline; leave
|
||||||
|
visible evidence when a rerun cannot be started or the cause is
|
||||||
|
uncertain; never repeatedly rerun a deterministic branch failure without
|
||||||
|
a corrective commit; and proceed to handoff once the check is green and
|
||||||
|
current-head approvals stand. A PR of yours with a red head is **not
|
||||||
|
parked** — the next move is yours, whatever the round's verdict state
|
||||||
|
says (shape 2 below carves this out explicitly). How the engine detects a red
|
||||||
|
head — its ledger, its quiet rules, the rollup's node shapes — is crew's
|
||||||
|
to describe, not this file's.
|
||||||
- **One build at a time.** You hold at most one issue on which you are
|
- **One build at a time.** You hold at most one issue on which you are
|
||||||
writing or revising a deliverable — finish or release that work before
|
writing or revising a deliverable — finish or release that work before
|
||||||
starting new work. The rule counts build work in flight, not claims: a
|
starting new work. The rule counts build work in flight, not claims: a
|
||||||
|
|
@ -20,9 +41,16 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
its `Blocked:` line stops the remaining work;
|
its `Blocked:` line stops the remaining work;
|
||||||
2. the deliverable is in a review round where every outstanding verdict
|
2. the deliverable is in a review round where every outstanding verdict
|
||||||
belongs to someone else — either the round is awaiting its first
|
belongs to someone else — either the round is awaiting its first
|
||||||
verdicts, or it was answered whole and the non-approvers re-requested
|
verdicts, or it was answered whole and the owed re-requests posted —
|
||||||
(the review round, steps 1–2). This is the *live* round; shape 4 is
|
by head, not by verdict: every panelist after a push, the
|
||||||
the *passed* one — they are sequential and do not overlap;
|
non-approvers alone at an unchanged head (the review round, steps
|
||||||
|
1–2). This is the *live* round; shape 4 is
|
||||||
|
the *passed* one — they are sequential and do not overlap. A red
|
||||||
|
check at the current head takes the deliverable **out of this
|
||||||
|
shape**: mid-round CI going red is exactly the state that reads as
|
||||||
|
"waiting on the panel" and is not — the next move is yours (the
|
||||||
|
red-head rule above), and reading it as parked is what strands the
|
||||||
|
PR;
|
||||||
3. every remaining acceptance criterion is operator-owned, stated as such
|
3. every remaining acceptance criterion is operator-owned, stated as such
|
||||||
by triage on the issue;
|
by triage on the issue;
|
||||||
4. the deliverable is **handed off** — the round passed, no `blocker:*`
|
4. the deliverable is **handed off** — the round passed, no `blocker:*`
|
||||||
|
|
@ -53,7 +81,8 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
that, or, if the events genuinely do not resolve it, say so on the
|
that, or, if the events genuinely do not resolve it, say so on the
|
||||||
issue and pick the next `ready` issue rather than idling on this one.
|
issue and pick the next `ready` issue rather than idling on this one.
|
||||||
Not parked — these are what the rule defends against: waiting on
|
Not parked — these are what the rule defends against: waiting on
|
||||||
yourself, waiting on CI, or waiting for a good moment. An issue you have
|
yourself, waiting on CI (a red head is your own work, above; a pending
|
||||||
|
one resolves without you), or waiting for a good moment. An issue you have
|
||||||
simply stopped working on is not parked either — that is abandonment,
|
simply stopped working on is not parked either — that is abandonment,
|
||||||
and its move is unchanged: unassign and restore `ready` (Claiming,
|
and its move is unchanged: unassign and restore `ready` (Claiming,
|
||||||
below).
|
below).
|
||||||
|
|
@ -78,12 +107,12 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
([#52](https://github.com/heavy-duty/ceremony/issues/52)) and `offsite`
|
([#52](https://github.com/heavy-duty/ceremony/issues/52)) and `offsite`
|
||||||
([#68](https://github.com/heavy-duty/ceremony/issues/68)) exemptions
|
([#68](https://github.com/heavy-duty/ceremony/issues/68)) exemptions
|
||||||
already guard — a parked claim nobody can name is an abandoned one.
|
already guard — a parked claim nobody can name is an abandoned one.
|
||||||
Shape 4 alone is exempt from the separate comment: the handoff round
|
Shape 4 alone is exempt from the separate comment: the factual handoff
|
||||||
summary plus the `state:needs-human` write *is* its declaration — both
|
comment plus the `state:needs-human` write *is* its declaration — both
|
||||||
halves are already there, what the claim waits on (the merge) and who
|
halves are already there, what the claim waits on (the merge) and who
|
||||||
owns the next move (the human), and both are visible to any scan as a
|
owns the next move (the human), and both are visible to any scan as a
|
||||||
`labeled` event with the summary beside it. No second comment is owed
|
`labeled` event with the comment beside it. No second comment is owed on
|
||||||
on the issue. Every other shape still declares as above.
|
the issue. Every other shape still declares as above.
|
||||||
Declared once, the declaration **stands** until the park's facts change:
|
Declared once, the declaration **stands** until the park's facts change:
|
||||||
a resumption that finds nothing changed posts nothing — the standing
|
a resumption that finds nothing changed posts nothing — the standing
|
||||||
declaration is the record, and silence while parked is compliant, not
|
declaration is the record, and silence while parked is compliant, not
|
||||||
|
|
@ -179,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
|
||||||
|
|
@ -194,9 +223,56 @@ CONTRIBUTING; everything below is the shared flow.)
|
||||||
does not become required. On rig#112 this distinction mattered: requesting
|
does not become required. On rig#112 this distinction mattered: requesting
|
||||||
codex and grok was correct for rig's panel even though ceremony's bench was
|
codex and grok was correct for rig's panel even though ceremony's bench was
|
||||||
larger, and the doctrine had not said which roster governed.
|
larger, and the doctrine had not said which roster governed.
|
||||||
|
**A review request requires a green check at the head.** A red check is
|
||||||
|
the author's own signal, not the panel's work: if the check is red, that
|
||||||
|
is your next task, not the panel's — fix it and push, then request. This
|
||||||
|
binds *you*, whether or not any engine enforces it. "My local suite
|
||||||
|
passed" is evidence about your machine; the check at the head is the
|
||||||
|
shared artifact the panel actually reads, and a reviewer's first act is
|
||||||
|
to read it. The one exception is a failure genuinely outside the PR — a
|
||||||
|
runner outage, a flaky dependency, a failure already present on the
|
||||||
|
default branch — and it is an exception only if the request says so
|
||||||
|
explicitly and names the evidence (e.g. "the same job fails identically
|
||||||
|
on `origin/main` at `<sha>`"). Silence about a red check is what is
|
||||||
|
prohibited; an argued exception shifts the burden to the author.
|
||||||
|
*Green* is a ruled term (operator, 2026-07-27): a **cancelled or
|
||||||
|
stale** check is not a green head — the rollup is scoped to the current
|
||||||
|
head, so what survives there is same-head cancellation, not
|
||||||
|
supersession by a newer push — while a **skipped or neutral** one *is*
|
||||||
|
green: those are deliberate "passed / not applicable" conclusions, and
|
||||||
|
reddening them would red every conditional job the fleet skips on
|
||||||
|
purpose. The costs behind the line are asymmetric: a false green spends
|
||||||
|
a three-reviewer round; a false red spends one author session.
|
||||||
2. **Wait for every verdict, then answer the round whole** — one reply
|
2. **Wait for every verdict, then answer the round whole** — one reply
|
||||||
covering every point, then push the fixes, then re-request exactly the
|
covering every point and stating what changed and what was verified.
|
||||||
reviewers who did not approve. Prefer verification over argument: when a
|
That reply is the written round record: the engine mirrors it under the
|
||||||
|
PR body's **Round log**, newest last, so the builder owes the reply and
|
||||||
|
no separate body edit. At re-request time the engine takes the author's
|
||||||
|
comments posted after the newest verdict in the round and appends them
|
||||||
|
with `<!-- round:<head-sha> -->`; an existing marker makes a retry a
|
||||||
|
no-op. If the builder posted no reply, the engine records that the round
|
||||||
|
passed without one and never blocks handoff on the omission. Then push
|
||||||
|
the fixes, then re-request **by head, not by verdict**: if answering the
|
||||||
|
round pushed any commit, every
|
||||||
|
panelist's approval is now stale — an approval is of a specific tree,
|
||||||
|
and the handoff predicate counts only approvals at the current head —
|
||||||
|
so **every panelist is re-requested, the approvers included**; a
|
||||||
|
panelist left un-re-requested after a push can never approve the tree
|
||||||
|
you shipped, and the PR sits looking finished with a full set of
|
||||||
|
verdicts and nothing owed by anyone, the same silent-stall shape as
|
||||||
|
[#26](https://github.com/heavy-duty/ceremony/issues/26)/[#39](https://github.com/heavy-duty/ceremony/issues/39).
|
||||||
|
Only when the head did not move — the round was answered with argument
|
||||||
|
or evidence and nothing was pushed — do you re-request just the
|
||||||
|
non-approvers: a standing approval already covers this exact head, and
|
||||||
|
the engine absorbs a re-request at an unchanged head (the re-request
|
||||||
|
rule, [#94](https://github.com/heavy-duty/ceremony/issues/94); its
|
||||||
|
mechanism is crew's to describe). **The re-request carries the same
|
||||||
|
green-check-at-head precondition as the first request**, argued
|
||||||
|
exception included. This is where the measured cost landed: crew#40
|
||||||
|
burned two consecutive heads and four reviewer-rounds, every one
|
||||||
|
relaying a CI failure already visible in the job log (crew#45). A fix
|
||||||
|
push whose check comes up red is not ready to go back to the panel; it
|
||||||
|
is your next fix. Prefer verification over argument: when a
|
||||||
reviewer doubts behavior, add the test that settles it.
|
reviewer doubts behavior, add the test that settles it.
|
||||||
3. Never dismiss a review, never merge, never mark your own work as passed.
|
3. Never dismiss a review, never merge, never mark your own work as passed.
|
||||||
A blocking point you disagree with is answered with evidence or escalated
|
A blocking point you disagree with is answered with evidence or escalated
|
||||||
|
|
@ -263,14 +339,19 @@ item to its flow in the same comment ([LABELS.md](LABELS.md)).
|
||||||
|
|
||||||
When the round passes — every panel verdict approves the **current head**,
|
When the round passes — every panel verdict approves the **current head**,
|
||||||
and no `blocker:*` stands (conflicts rebased, CI green, drill recorded if
|
and no `blocker:*` stands (conflicts rebased, CI green, drill recorded if
|
||||||
this is a release PR) — hand it to the human, in order:
|
this is a release PR) — the engine performs these mechanical steps on the
|
||||||
|
builder's behalf, in order:
|
||||||
|
|
||||||
1. post the round summary (what changed per round, what was verified);
|
1. request the human's review;
|
||||||
2. request the human's review;
|
2. set `state:needs-human`;
|
||||||
3. set `state:needs-human` yourself.
|
3. post the engine-rendered handoff comment: approvals at the current head,
|
||||||
|
the head SHA, and a pointer to the PR body's **Round log**.
|
||||||
|
|
||||||
The label write is optimistic — the reconciler validates it, and takes it
|
The builder composes no new summary at handoff: the authored record already
|
||||||
back if the PR is not actually mergeable-right-now. Then stop: the PR is the
|
lives in the Round log, mirrored mechanically from each whole-round reply as
|
||||||
human's. The claim is now parked as shape 4 (Picking, above) — the handoff
|
specified above. The label write is optimistic — the reconciler validates
|
||||||
you just posted is its declaration, and your build slot is free. Address
|
it, and takes it back if the PR is not actually mergeable-right-now. Then
|
||||||
what comes back (`state:addressing`) and re-hand-off the same way.
|
stop: the PR is the human's. The claim is now parked as shape 4 (Picking,
|
||||||
|
above) — the handoff you just posted is its declaration, and your build slot
|
||||||
|
is free. Address what comes back (`state:addressing`) and re-hand-off the
|
||||||
|
same way.
|
||||||
|
|
|
||||||
96
CHANGELOG.md
96
CHANGELOG.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -35,43 +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 author posts the
|
|
||||||
round summary, requests the human's review, and sets `state:needs-human`.
|
|
||||||
The label write is optimistic; the reconciler validates it within seconds.
|
|
||||||
6. **A human merges.** Nothing else merges.
|
|
||||||
|
|
||||||
### Roster
|
### Roster
|
||||||
|
|
||||||
|
|
|
||||||
312
FLEET.md
312
FLEET.md
|
|
@ -1,54 +1,67 @@
|
||||||
# FLEET.md — the roster, and how it actually runs
|
# FLEET.md — the fleet shape, and how it actually runs
|
||||||
|
|
||||||
> **Status:** descriptive snapshot, not doctrine. This file records how the
|
> **Status:** descriptive snapshot, not doctrine. This file records how the
|
||||||
> agent fleet that builds this repo is wired *today*, so the setup can later be
|
> heavy-duty operator fleet is wired *today*. It is **not** part of the
|
||||||
> solidified into a replicable fleet-management solution. It is **not** part of
|
> vendored doctrine set (`.ceremony/`) and is never mirrored to consumer
|
||||||
> the vendored doctrine set (`.ceremony/`) and is never mirrored to consumer
|
> repos. [Crew](https://github.com/heavy-duty/crew) is a general tool: its
|
||||||
> repos. The doctrine files (AGENTS.md, TRIAGE.md, BUILDER.md, REVIEWER.md,
|
> repository ships the engine, while the fleet definition belongs to the
|
||||||
> LABELS.md, CONTRIBUTING.md) say what roles *must* do; this file says how the
|
> operator; heavy-duty is one operator of it. Membership, repository scope,
|
||||||
> current bench *physically* does it. Last reconciled against the deployed
|
> agent-profile overrides and doctrine paths belong to that definition;
|
||||||
> duty scripts at
|
> membership itself lives outside every checkout. Crew's shipped defaults
|
||||||
> [`heavy-duty/crew@b2fd864`](https://github.com/heavy-duty/crew/tree/b2fd8642e7f7aa8dc9de6b44edadbe1dc557b140)
|
> name heavy-duty's AGENTS.md, TRIAGE.md, BUILDER.md and REVIEWER.md, but
|
||||||
> (private to the org; the fleet can read it), 2026-07-24 — a descriptive
|
> those are compatibility defaults, not vocabulary compiled into the engine
|
||||||
> file with no reconciliation stamp gives the next reader nothing to diff,
|
> — operator `doctrine.conf` values can replace them.
|
||||||
> which is exactly how the #149 drift went unnoticed.
|
> The *mechanism* lives with crew and this file points at it. Last reconciled
|
||||||
|
> against the merged engine at
|
||||||
|
> [`heavy-duty/crew@eaeb302`](https://github.com/heavy-duty/crew/tree/eaeb3022aa47d90e797f2b9e007b831df7ca8406),
|
||||||
|
> 2026-07-28 — a descriptive file with no reconciliation stamp gives the next
|
||||||
|
> reader nothing to diff, which is exactly how the #149 drift went unnoticed.
|
||||||
|
|
||||||
## The roster
|
## Fleet shape
|
||||||
|
|
||||||
One box (an isolated, disposable VM) per GitHub identity. Boxes are credential
|
One box (an isolated, disposable VM) per GitHub identity. Boxes are credential
|
||||||
boundaries; sessions inside a box are role boundaries. No box has an inbound
|
boundaries; sessions inside a box are role boundaries. No box has an inbound
|
||||||
network path — GitHub is the only queue.
|
network path — GitHub is the only queue. Fleet membership is the operator's
|
||||||
|
definition and lives outside every checkout; this file deliberately carries
|
||||||
|
no second roster.
|
||||||
|
|
||||||
| Identity | Box | CLI | Roles |
|
Review panel per PR = the governed repo's `.github/labels.conf` `panel=` line
|
||||||
|---|---|---|---|
|
minus the PR's author, as [REVIEWER.md](REVIEWER.md) specifies (recusal by
|
||||||
| `dan-claude-bot` | triage-box | Claude Code | **triage** — the only issue-minter |
|
|
||||||
| `claude-bot-andresmgsl` | claude-box | Claude Code | builder (hard machinery) + reviewer |
|
|
||||||
| `codex-bot-andresmgsl` | codex-box | Codex CLI | builder (mechanical) + reviewer |
|
|
||||||
| `grok-bot-andresmgsl` | grok-box | Grok CLI | reviewer |
|
|
||||||
| `kimi-bot-andresmgsl` | kimi-box | Kimi CLI | reviewer |
|
|
||||||
|
|
||||||
Review panel per PR = the reviewer bench minus the PR's author (recusal by
|
|
||||||
construction). Only humans merge — enforced as permissions (the agents team
|
construction). Only humans merge — enforced as permissions (the agents team
|
||||||
holds the triage role, not write), not as convention.
|
holds the triage role, not write), not as convention.
|
||||||
|
|
||||||
## Anatomy of a duty loop
|
## Anatomy of a duty loop
|
||||||
|
|
||||||
Every box runs the same skeleton, adapted to its CLI:
|
The mechanism is no longer described here. The five hand-rolled duty scripts
|
||||||
|
converged into crew's shared engine, and a prose mirror of running code in a
|
||||||
|
second repo is a second thing to keep true — this one drifted (it said cron
|
||||||
|
ran `duty.sh` directly and gave the hygiene sweep its own cron line; crew's
|
||||||
|
`duty.sh` records that separate line as the bug it fixed, sharing
|
||||||
|
`~/duty/work` unlocked). How a tick actually works — cron fires
|
||||||
|
[`bin/tick.sh`](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/shared/bin/tick.sh),
|
||||||
|
the only cron target, which wraps
|
||||||
|
[`bin/duty.sh`](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/shared/bin/duty.sh)
|
||||||
|
in a non-blocking `flock` with one evidence line per boundary; the boot gate
|
||||||
|
and crash recovery; the session runner; backlog hygiene self-scheduling
|
||||||
|
inside the duty tick under the same lock — lives with the code:
|
||||||
|
[`shared/README.md`](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/shared/README.md)
|
||||||
|
is the map, provenance table included. Sessions stay disposable: durable work
|
||||||
|
state lives on the board (issues, PRs, labels) and in git branches, while the
|
||||||
|
engine keeps only operational evidence and deduplication state under
|
||||||
|
`~/duty`; detection is the engine's, judgment is the session's.
|
||||||
|
|
||||||
- **Tick:** cron `*/5` runs `~/duty/duty.sh` under a non-blocking `flock`; the
|
What belongs here is what a wake *means*:
|
||||||
triage box adds an hourly hygiene sweep under its own lock. Holding the lock
|
|
||||||
is load-bearing: a tick that acquires it *knows* nothing else is running on
|
- **The registry is the scope.** A box acts only on repos its operator
|
||||||
this identity.
|
listed. Work it finds outside that scope is reported and never acted on;
|
||||||
- **Poll:** the script reads `~/duty/repos.txt` and queries GitHub with `gh`
|
the report is part of the boundary, because a bounded wake that goes quiet
|
||||||
for work matching the box's role. Whose registry that file is depends on
|
is indistinguishable from a broken one. Adding a repo is an **operator
|
||||||
the role: the triage box's `repos.txt` **is** its registry — adding a repo
|
decision**, never something a sweep makes by writing where nobody listed.
|
||||||
is adding a line — while a reviewer's registry is the org itself, and its
|
The 2026-07-25 scope ruling (crew#16) closed the org-wide review and
|
||||||
`repos.txt` is a backstop that cannot scope it (grok's copy says so in its
|
author-side write surface; the crew#66 attention ruling closed the last
|
||||||
own first line).
|
exemption. Crew's
|
||||||
- **Act:** when there is work, the script launches the box's CLI as a one-shot
|
[`examples/repos.txt` header](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/examples/repos.txt)
|
||||||
session with a role prompt; the session does the work via `gh` as the box's
|
is the pinned source for how that rule is implemented and reported.
|
||||||
own identity, then exits. Sessions are stateless and disposable — all state
|
|
||||||
lives on the board (issues, PRs, labels) and in git branches.
|
|
||||||
|
|
||||||
### Wake conditions
|
### Wake conditions
|
||||||
|
|
||||||
|
|
@ -62,9 +75,27 @@ sibling agent outranks self-directed continuation, and it is frequently the
|
||||||
very thing that unparks the work resume would otherwise pick up. The query is
|
very thing that unparks the work resume would otherwise pick up. The query is
|
||||||
the authenticated-user endpoint —
|
the authenticated-user endpoint —
|
||||||
`gh api "/issues?filter=assigned&state=open&labels=attention"` — one call, no
|
`gh api "/issues?filter=assigned&state=open&labels=attention"` — one call, no
|
||||||
search index (the reviewer trigger below already records that the index
|
search index (the review queue below already records that the index lags) —
|
||||||
lags), and like the review-request trigger it reaches repos `~/duty/repos.txt`
|
and it **sees** repos outside the operator's registry, because that endpoint
|
||||||
does not name.
|
takes no repo filter.
|
||||||
|
|
||||||
|
**Seeing is not acting, and that is a ruling** (crew#66, danmt, 2026-07-27).
|
||||||
|
The wake used to work every row it saw, which for a builder meant a clone and
|
||||||
|
the full worktree and round rule set against a repo no operator had listed —
|
||||||
|
write authority outside the registry, and the one hole left in the
|
||||||
|
containment story. Rows are now partitioned against the registry: inside it,
|
||||||
|
a session as before; outside it, reported and never acted on, exactly like an
|
||||||
|
out-of-scope review request or authored PR.
|
||||||
|
[`lib/duty-attention.sh`](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/shared/lib/duty-attention.sh)
|
||||||
|
implements the partition and states the ruling in its header.
|
||||||
|
|
||||||
|
The cost was argued before the ruling rather than discovered after it: an
|
||||||
|
assignment plus a label **is** a targeted authorization, so a cross-repo
|
||||||
|
handoff now waits on an operator adding the repo, and the box most likely to
|
||||||
|
be handed work outside its beat is the one that goes quiet. That is why an
|
||||||
|
out-of-scope demand does not only reach `duty.log` — it pings the operator
|
||||||
|
over the same channel the boot gate uses. A bounded wake that failed silently
|
||||||
|
would trade an unbounded write surface for a broken channel to the human.
|
||||||
|
|
||||||
Each demand gets **exactly one session, and the ack bounds it**: the
|
Each demand gets **exactly one session, and the ack bounds it**: the
|
||||||
session's first act, before any of the demanded work, is the pickup comment
|
session's first act, before any of the demanded work, is the pickup comment
|
||||||
|
|
@ -72,9 +103,13 @@ plus removing the label — [the `attention`
|
||||||
contract's](https://github.com/heavy-duty/ceremony/blob/bce09aa7648dbd74b8e91b1d4fbc2fa8d145f705/LABELS.md#L143-L149)
|
contract's](https://github.com/heavy-duty/ceremony/blob/bce09aa7648dbd74b8e91b1d4fbc2fa8d145f705/LABELS.md#L143-L149)
|
||||||
ack (#85), which here becomes the session's ack-then-act ordering.
|
ack (#85), which here becomes the session's ack-then-act ordering.
|
||||||
Then it acts on the thread and exits — short by construction. Until the label
|
Then it acts on the thread and exits — short by construction. Until the label
|
||||||
is removed the flag is still up, so a session that dies before acking is
|
is removed the flag is still up, so a session that **dies** before acking is
|
||||||
simply relaunched at the next tick; that is the whole crash-recovery story,
|
simply relaunched at the next tick — the same crash-only shape as resume
|
||||||
and it is the same crash-only shape as resume below.
|
below. A session that **completes** without acking is a different fact: that
|
||||||
|
is a decline, and a seen-ledger stops it re-firing until the issue moves.
|
||||||
|
Dying and declining used to look identical to the engine, which meant a
|
||||||
|
demand a session had considered and correctly left alone woke a new one every
|
||||||
|
tick forever.
|
||||||
|
|
||||||
The design this replaces was built and rejected: polling notifications for
|
The design this replaces was built and rejected: polling notifications for
|
||||||
`reason: mention` re-arms a thread on every comment, so ordinary round
|
`reason: mention` re-arms a thread on every comment, so ordinary round
|
||||||
|
|
@ -85,47 +120,91 @@ wake: [#16's 16:49Z
|
||||||
ruling](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198)
|
ruling](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198)
|
||||||
authorized the last open acceptance criterion on a `claimed` issue and sat
|
authorized the last open acceptance criterion on a `claimed` issue and sat
|
||||||
unowned for over an hour — the box answered every state signal that day and
|
unowned for over an hour — the box answered every state signal that day and
|
||||||
never saw the comment, and the eventual pickup ran on a manual bridge. Like
|
never saw the comment, and the eventual pickup ran on a manual bridge. The
|
||||||
the notifier's queue below, this wake is the spec for a box-side change only
|
wake is no longer on paper: `duty-attention.sh` is deployed engine, and
|
||||||
the operator can make; until `duty.sh` polls it, the wake exists on paper —
|
`duty.sh` runs it first on every box, whatever its roles.
|
||||||
though one consumer already polls for the label and no-ops while it is
|
|
||||||
absent, so the wiring can be verified live the day the row lands.
|
|
||||||
|
|
||||||
- **Triage:** new discussions to mint from, builder questions on issues, stray
|
The engine's duty order is fleet-standard
|
||||||
issues to reconcile, `@`-mentions, hourly hygiene (stale claims, label
|
([`bin/duty.sh`](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/shared/bin/duty.sh)):
|
||||||
invariants), and a `needs-ruling` standing **past 24h** — the ladder's last
|
**attention → triage signals → review queue → resume → ci-red → build →
|
||||||
rung makes the option triage's to pick, and this wake list is where triage
|
handoff → rebase → worktree hygiene → backlog hygiene (hourly)** — attention
|
||||||
learns such an item exists (see the notifier section below).
|
role-independent and first, then each duty family the box's roles enable.
|
||||||
- **Builders**, in priority order: **resume** (an open draft PR of mine, or a
|
Every position in that order is deployed engine at the stamped SHA:
|
||||||
claimed issue with my `build/*` branch but no PR — possible only if a
|
[crew#64](https://github.com/heavy-duty/crew/pull/64) merged ci-red between
|
||||||
previous session died mid-work), a `ready` issue to claim, a completed
|
resume and build, and `duty.sh`'s own header carries the same order.
|
||||||
review round on my PR (act on whole rounds, never single verdicts), my PR
|
The earlier form of this file folded handoff and rebase into the other
|
||||||
fully approved (write the closing summary, flip to `state:needs-human`,
|
builder wakes; they are duties of their own.
|
||||||
request the human), my PR `CONFLICTING` (rebase; never act on `UNKNOWN` —
|
|
||||||
post-merge flap).
|
- **Triage signals**, per registry repo: `needs-triage` issues,
|
||||||
- **Reviewers**, one candidate set from two merged sources. Source 1,
|
queue-unlabeled strays, discussions without triage's voice, unread
|
||||||
authoritative: every open PR in the `heavy-duty` org **plus the named bot
|
`@`-mentions (their own session), and `blocked` issues whose named blockers
|
||||||
forks** that lists me in `requested_reviewers`, enumerated straight from
|
have all landed — a lead the session verifies, never a label the engine
|
||||||
the pulls API — never `gh search`, whose index lags (cast#143,
|
flips. Backlog hygiene (stale claims, label invariants) runs hourly,
|
||||||
incubator#25 and box#164 each sat unreviewed behind it). A review request
|
self-scheduled inside the duty tick. A `needs-ruling` standing **past
|
||||||
is authorization, so no repo filter may gate it. Source 2, backstop: the
|
24h** is still triage's to pick up — the ladder's last rung makes the
|
||||||
`repos.txt` poll for an open PR by someone else whose head I have not yet
|
option triage's to choose — but, like the notifier queue below, that
|
||||||
reviewed — it only **adds** candidates the sweep may have missed (an
|
detection row is on paper only today.
|
||||||
org-enumeration failure, say) and never concludes "nothing to do". The
|
- **Review queue**: one candidate set, enumerated from the pulls pages of
|
||||||
sources are merged and deduplicated by (repo, PR) **before** acting, not
|
every registry repo — object endpoints, never the search index for the
|
||||||
run as sequential passes — the sequential shape double-announced on
|
queue itself, whose lag left cast#143, incubator#25 and box#164 sitting
|
||||||
ceremony#32, when the request sweep and the repo-list poll each acted on
|
unreviewed — filtered to PRs listing me in `requested_reviewers`, deduped
|
||||||
the same PR in one tick (operator protocol 2026-07-23) — and worked
|
by (repo, PR) before acting (the sequential shape double-announced on
|
||||||
oldest-first. Unchanged: one verdict per head, deduplicated against my
|
ceremony#32), and worked oldest-first. One search-backed **awareness pass**
|
||||||
own latest review's SHA.
|
per tick reports requests outside the registry and never acts on them —
|
||||||
|
the scope rule above. One verdict per head, deduplicated against my own
|
||||||
|
latest review's SHA; a re-request at an unchanged head is answered with an
|
||||||
|
auto-approve through the verdict gate rather than left as a stale blocker
|
||||||
|
(operator ruling 2026-07-23, ceremony#94).
|
||||||
|
- **Resume** (builders, checked before build): an open draft PR of mine, or
|
||||||
|
a `claimed` issue whose `build/*` branch exists on my fork with no open PR
|
||||||
|
— a session died between first push and PR creation. A branch whose PR
|
||||||
|
already **merged** is a post-merge wait, never resumed (#172,
|
||||||
|
incubator#55/#64).
|
||||||
|
- **ci-red** (builders): a non-draft PR of mine whose check at the current
|
||||||
|
head is failing. Evaluated before the build wake, so a red PR of mine
|
||||||
|
outranks a new claim — repairing my own red head comes ahead of new work
|
||||||
|
(ceremony#163: full-panel approvals at the head, mergeable, stranded on
|
||||||
|
a transient failure no wake covered). A round owed at a red head is
|
||||||
|
excluded from the build wake below but reported rather than silent, and
|
||||||
|
an unchanged red head goes quiet after one attempt, through the
|
||||||
|
`report_suppressed` path — suppressed, still said. A check that has not
|
||||||
|
finished is **not** a red head and wakes nothing here: nothing has failed
|
||||||
|
yet, so there is no investigation to launch. How a red head is detected
|
||||||
|
and kept quiet is the engine's mechanism, described in crew's
|
||||||
|
`shared/README.md`, not here.
|
||||||
|
- **Build**: a `ready` **unclaimed** issue (an assignee means mid-claim, not
|
||||||
|
pickable), or a completed review round on my PR — a changes-request with
|
||||||
|
no panel review request still outstanding; whole rounds, never single
|
||||||
|
verdicts, and never a round the check at its head does not support. The
|
||||||
|
wake admits a **green** head, and a head with **no checks configured** —
|
||||||
|
terminal, not transient, so holding there would retire the round rather
|
||||||
|
than delay it. It holds a **red** head (already woken ci-red above) and a
|
||||||
|
head whose check has **not finished** (opening the round there spends the
|
||||||
|
panel on a head that may go red — crew#45's measured cost — and it admits
|
||||||
|
itself a tick later once the check settles). Both holds are reported, not
|
||||||
|
swallowed, and they are reported *differently*: only one of them is the
|
||||||
|
author's own work to do.
|
||||||
|
- **Handoff**: a round of mine that converged — every panelist's latest
|
||||||
|
opinionated review approves the current head, no panel request
|
||||||
|
outstanding, mergeable right now, `state:needs-human` not already set.
|
||||||
|
Convergence is computed from `latestOpinionatedReviews`, never
|
||||||
|
`reviewDecision`, which stays empty without branch protection and silently
|
||||||
|
stalled rounds for a day (ceremony#26, #39).
|
||||||
|
- **Rebase**: my PR `CONFLICTING` — and only `CONFLICTING`; `UNKNOWN` is
|
||||||
|
GitHub's post-merge recompute flap and waits. A conflicting draft belongs
|
||||||
|
to resume.
|
||||||
|
- **Worktree hygiene**: a `build/*` worktree is removed only when its branch
|
||||||
|
has PR history and no PR on it remains open; a branch with no PR at all is
|
||||||
|
an in-flight claim and stays.
|
||||||
|
|
||||||
#### The operator notifier — the `needs-ruling` queue
|
#### The operator notifier — the `needs-ruling` queue
|
||||||
|
|
||||||
The operator notifier (`notify.sh`, on the triage box) watches open PRs
|
The operator notifier (`notify.sh`, a fleet singleton on the triage box; its
|
||||||
carrying `state:needs-human`. That poll never reads `needs-ruling`, which
|
mechanism is crew's too) watches open PRs carrying `state:needs-human`. That
|
||||||
lives mostly on *issues* — so an escalation waits invisibly on the very human
|
poll never reads `needs-ruling`, which lives mostly on *issues* — so an
|
||||||
it names. Not hypothetical: on 2026-07-23 alone, three escalations spent
|
escalation waits invisibly on the very human it names. Not hypothetical: on
|
||||||
their whole lives outside the operator's view — [#16's fork-PR-workflows
|
2026-07-23 alone, three escalations spent their whole lives outside the
|
||||||
|
operator's view — [#16's fork-PR-workflows
|
||||||
question](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5053302689)
|
question](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5053302689)
|
||||||
(raised 01:23Z, [ruled 09:24Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056705884)
|
(raised 01:23Z, [ruled 09:24Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056705884)
|
||||||
— eight hours in which the board showed a `claimed` issue indistinguishable
|
— eight hours in which the board showed a `claimed` issue indistinguishable
|
||||||
|
|
@ -135,11 +214,12 @@ and [epic #50's own 13:04Z
|
||||||
flag](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181),
|
flag](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181),
|
||||||
which surfaced only because a human happened to look. This file records how
|
which surfaced only because a human happened to look. This file records how
|
||||||
the fleet actually runs; that is why this wiring changed (#50 D16). The spec
|
the fleet actually runs; that is why this wiring changed (#50 D16). The spec
|
||||||
for the box-side update:
|
for the engine-side update:
|
||||||
|
|
||||||
- **The second query.** Alongside the `state:needs-human` PR poll, `notify.sh`
|
- **The second query.** Alongside the `state:needs-human` PR poll, `notify.sh`
|
||||||
polls **open issues and PRs labelled `needs-ruling`** across every repo in
|
polls **open issues and PRs labelled `needs-ruling`** across every repo in
|
||||||
`~/duty/repos.txt`.
|
`notify-repos.txt`, which is deliberately wider than the duty registry:
|
||||||
|
a cross-repo handoff is precisely what the operator cannot discover alone.
|
||||||
- **One tracked message per item, edited in place** — the same
|
- **One tracked message per item, edited in place** — the same
|
||||||
one-message-per-item discipline the PR poll already uses, so an aging
|
one-message-per-item discipline the PR poll already uses, so an aging
|
||||||
ruling reads as a **live queue**, not a feed. The message is removed when
|
ruling reads as a **live queue**, not a feed. The message is removed when
|
||||||
|
|
@ -164,54 +244,46 @@ Nothing box-side ever sets, clears, or decides `needs-ruling` (#50 D9, D15):
|
||||||
the notifier and triage's past-24h wake above *report and pick up* what the
|
the notifier and triage's past-24h wake above *report and pick up* what the
|
||||||
board already shows; the label itself moves only by the doctrine's hands.
|
board already shows; the label itself moves only by the doctrine's hands.
|
||||||
|
|
||||||
`~/duty/repos.txt` and the duty scripts live inside each box and are the
|
The duty engine is crew's shared tree, one source deployed to every box.
|
||||||
operator's to change; this descriptive edit is the spec for those box-side
|
Specs written in this file have a record of becoming engine: the attention
|
||||||
updates. The reviewers' request sweep is one such spec made real — deployed
|
wake and the reviewers' request sweep both started here as paper (the sweep's
|
||||||
on all four reviewer boxes since 2026-07-23 (the Reviewers wake above). The
|
org-wide form was then retired by the 2026-07-25 scope ruling), and the
|
||||||
notifier's `needs-ruling` queue is still on paper only: `notify.sh`'s one
|
builders' ci-red wake above is the latest: written here as paper while
|
||||||
label filter today is `state:needs-human`.
|
crew#64 was open, engine at the stamped SHA. Two rows are still on paper, and
|
||||||
|
both are `needs-ruling`: the notifier's queue — at that SHA, `notify.sh`'s
|
||||||
### Resilience
|
only label filter is `state:needs-human` — and triage's **past 24h**
|
||||||
|
detection row above, which the triage-signals bullet already marks. Earlier
|
||||||
- **Boot gate:** each tick compares the kernel boot id
|
counts here said "two" while silently excluding the second; naming them is
|
||||||
(`/proc/sys/kernel/random/boot_id`) to a stored marker. First tick after any
|
cheaper than a number that has to be recounted every time a wake lands.
|
||||||
reboot runs credential + disk probes; the marker is written only when auth
|
|
||||||
actually works, so a box with dead credentials re-checks loudly every tick
|
|
||||||
instead of silently skipping duty.
|
|
||||||
- **Crash-only resume:** there is no session state to restore. The recovery
|
|
||||||
path *is* the normal path: the resume wake condition reads the board, posts
|
|
||||||
`⟲ resuming from <sha>`, and continues from the worklog. Rebooting a box
|
|
||||||
never loses work that was pushed.
|
|
||||||
- **Checkpoint discipline (builders):** open the PR as draft at the first
|
|
||||||
commit with a `## Worklog` checkbox list; check off and push after every
|
|
||||||
step. The board and the branch are the only memory.
|
|
||||||
- **Worktree isolation:** builders build each PR in its own `git worktree`;
|
|
||||||
reviewers check out PR heads in throwaway detached worktrees and remove them
|
|
||||||
after the verdict. Main clones stay parked on the default branch, always
|
|
||||||
clean; stale worktrees are pruned by the boot gate.
|
|
||||||
|
|
||||||
### Conventions on the board
|
### Conventions on the board
|
||||||
|
|
||||||
- `🔎 reviewing head <sha>` — a reviewer announces work before starting, so
|
- `🔎 reviewing head <sha>` — a reviewer announces work before starting, so
|
||||||
liveness is visible instead of hoped for.
|
liveness is visible instead of hoped for.
|
||||||
- `⟲ resuming from <sha>` — a builder announces recovery after interruption.
|
- `⟲ resuming from <sha>` — a builder announces recovery after interruption;
|
||||||
|
there is no session state to restore, so the recovery path *is* the normal
|
||||||
|
path: read the board, continue from the worklog. Rebooting a box never
|
||||||
|
loses work that was pushed.
|
||||||
|
- Checkpoint discipline (builders): open the PR as draft at the first commit
|
||||||
|
with a `## Worklog` checkbox list; check off and push after every step.
|
||||||
|
The board and the branch are the only memory.
|
||||||
- Claim ritual: comment on the issue + self-assign + label flip, before any
|
- Claim ritual: comment on the issue + self-assign + label flip, before any
|
||||||
branch exists.
|
branch exists.
|
||||||
- Handoff: the author closes an approved PR's round with a summary comment,
|
- Handoff: the author closes an approved PR's round with a summary comment,
|
||||||
flips `state:needs-human`, and requests the human — merging is never the
|
flips `state:needs-human`, and requests the human — merging is never the
|
||||||
fleet's job.
|
fleet's job.
|
||||||
|
- Worktree isolation: builders build each PR in its own `git worktree`;
|
||||||
|
reviewers check out PR heads in throwaway detached worktrees and remove
|
||||||
|
them after the verdict. Main clones stay parked on the default branch,
|
||||||
|
always clean.
|
||||||
|
|
||||||
## Where this is going
|
## Where this is going
|
||||||
|
|
||||||
This wiring proved itself on day one (seven merged PRs, unanimous three-model
|
This wiring proved itself on day one (seven merged PRs, unanimous three-model
|
||||||
review convergence on #39, and a full-fleet crash recovery). The plan:
|
review convergence on #39, and a full-fleet crash recovery), and the plan it
|
||||||
|
carried has become **heavy-duty/crew** — a shared engine, CLI, operator
|
||||||
1. Once the ceremony machinery is complete and adopted, each agent will be
|
configuration model, real-host rehearsal and fixture tests — so standing up
|
||||||
asked to write a **detailed, replicable description of its own setup** —
|
a fleet is a bootstrap, not an archaeology dig. What remains is adoption:
|
||||||
cron lines, duty script, prompts, probes — as durable documentation.
|
crew#85 tracks the road to a `0.1.0` another operator can use without a fork.
|
||||||
2. Those five descriptions get converged into a **solidified fleet-management
|
Membership stays in the operator definition; this file remains the map of
|
||||||
solution** (duty loops as reusable templates, likely living alongside the
|
what a wake means, and crew is the map of how it runs.
|
||||||
rig templates registry), so standing up this roster on a new repo — or a
|
|
||||||
whole new fleet — is a bootstrap, not an archaeology dig.
|
|
||||||
|
|
||||||
Until then, this file is the map.
|
|
||||||
|
|
|
||||||
14
LABELS.md
14
LABELS.md
|
|
@ -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.
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.1-dev
|
0.4.1-dev
|
||||||
|
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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
50
drills/0.4.0.md
Normal 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.
|
||||||
98
test/labels-triggers.test.sh
Normal file
98
test/labels-triggers.test.sh
Normal 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
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue