Merge pull request #137 from codex-bot-andresmgsl/build/136-adopt-changelog-fragments
feat: adopt changelog fragments
This commit is contained in:
commit
5e70c0bb66
28 changed files with 535 additions and 96 deletions
|
|
@ -11,14 +11,98 @@ 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.
|
||||||
- **One issue at a time.** Finish or release your claim before taking
|
- **One build at a time.** You hold at most one issue on which you are
|
||||||
another.
|
writing or revising a deliverable — finish or release that work before
|
||||||
|
starting new work. The rule counts build work in flight, not claims: a
|
||||||
|
claim does not consume the slot while it is **parked**, meaning the next
|
||||||
|
move belongs to someone else. Exactly five shapes qualify:
|
||||||
|
1. the issue carries `needs-ruling`, its escalation names a decider, and
|
||||||
|
its `Blocked:` line stops the remaining work;
|
||||||
|
2. the deliverable is in a review round where every outstanding verdict
|
||||||
|
belongs to someone else — either the round is awaiting its first
|
||||||
|
verdicts, or it was answered whole and the non-approvers re-requested
|
||||||
|
(the review round, steps 1–2). This is the *live* round; shape 4 is
|
||||||
|
the *passed* one — they are sequential and do not overlap;
|
||||||
|
3. every remaining acceptance criterion is operator-owned, stated as such
|
||||||
|
by triage on the issue;
|
||||||
|
4. the deliverable is **handed off** — the round passed, no `blocker:*`
|
||||||
|
stands, and you set `state:needs-human` per Handoff (below). The
|
||||||
|
remaining move is the human's merge.
|
||||||
|
5. the claim is **held by directive** — triage or the operator has told
|
||||||
|
you to stop, the direction names what the hold waits on, and that thing
|
||||||
|
is not yours to move. This is not "waiting for a good moment": somebody
|
||||||
|
else has decided the work must not proceed, and only they end it.
|
||||||
|
And it ends the same way it started: **on the labels.** When the queue
|
||||||
|
labels and any prose — an issue body header, a triage comment, an
|
||||||
|
operator's comment — disagree about whether a hold stands, the most
|
||||||
|
recent queue-label event by the hold's owner governs, and the prose is
|
||||||
|
stale until someone corrects it. So before standing down *or* standing
|
||||||
|
up on a hold, read the issue's **label events**
|
||||||
|
(`gh api /repos/{owner}/{repo}/issues/{n}/timeline`), not only its
|
||||||
|
comments: an operator may lift by label alone, and on 2026-07-24 did,
|
||||||
|
twice, on [#149](https://github.com/heavy-duty/ceremony/issues/149)
|
||||||
|
and [#151](https://github.com/heavy-duty/ceremony/issues/151). Acting
|
||||||
|
on the labels against stale prose, say so in the claim — name the
|
||||||
|
events you read, their timestamps and their actor, and invite the
|
||||||
|
correction if the read is wrong;
|
||||||
|
[the 14:11:45Z claim on #149](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070781295)
|
||||||
|
is the exemplar. Refusing is not a resting place either:
|
||||||
|
[*"I am not claiming through that contradiction"*](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070776624)
|
||||||
|
was a correct instinct and an incomplete move — the next step is to
|
||||||
|
read the events, state what they say, and then claim or stand down on
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
simply stopped working on is not parked either — that is abandonment,
|
||||||
|
and its move is unchanged: unassign and restore `ready` (Claiming,
|
||||||
|
below).
|
||||||
|
The 2026-07-23 board is why the rule counts work and not claims: one
|
||||||
|
builder correctly held
|
||||||
|
[#15](https://github.com/heavy-duty/ceremony/issues/15) (`offsite`,
|
||||||
|
round answered whole, one verdict outstanding) and
|
||||||
|
[#16](https://github.com/heavy-duty/ceremony/issues/16) (`needs-ruling`
|
||||||
|
hard block, triage said hold) parked beside the one active build,
|
||||||
|
[#73](https://github.com/heavy-duty/ceremony/issues/73).
|
||||||
|
|
||||||
## Claiming
|
## Claiming
|
||||||
|
|
||||||
- Assign yourself, swap `ready` → `claimed`, and comment that you are
|
- Assign yourself, swap `ready` → `claimed`, and comment that you are
|
||||||
starting. The claim is a promise of a draft PR soon — a claim with no PR
|
starting. The claim is a promise of a draft PR soon — a claim with no PR
|
||||||
and no activity is what the staleness sweep reclaims.
|
and no activity is what the staleness sweep reclaims unless `offsite`
|
||||||
|
records that its PR lives in another repository.
|
||||||
|
- **A park is declared, never inferred.** When your claim enters a parked
|
||||||
|
shape (Picking, above), say so in a comment on that issue, naming what it
|
||||||
|
waits on and who owns the next move. No new label: the comment is
|
||||||
|
activity, so it feeds the same reclaim clock the `needs-ruling`
|
||||||
|
([#52](https://github.com/heavy-duty/ceremony/issues/52)) and `offsite`
|
||||||
|
([#68](https://github.com/heavy-duty/ceremony/issues/68)) exemptions
|
||||||
|
already guard — a parked claim nobody can name is an abandoned one.
|
||||||
|
Shape 4 alone is exempt from the separate comment: the handoff round
|
||||||
|
summary plus the `state:needs-human` write *is* its declaration — both
|
||||||
|
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
|
||||||
|
`labeled` event with the summary beside it. No second comment is owed
|
||||||
|
on the issue. Every other shape still declares as above.
|
||||||
|
- **Pick up `attention` before anything else.** On your claim, first post a
|
||||||
|
short pickup comment and remove `attention`; the removal is the ack. A
|
||||||
|
demand on a parked claim is usually its unpark, so take the slot back under
|
||||||
|
the existing rule below rather than leaving the demand parked. A demand
|
||||||
|
that *is* the park is different: the pickup comment is the declaration,
|
||||||
|
so one comment does both jobs, and the demand does not take the slot back.
|
||||||
|
- **A directed hold keeps its bookkeeping visible.** The PR carries `blocked`
|
||||||
|
with a comment naming what it waits on; the issue stays `claimed` and
|
||||||
|
carries `attention` until the builder acknowledges it. Nobody unassigns
|
||||||
|
the issue, and the 48-hour reclaim does not fire because the claim has an
|
||||||
|
open PR. Unparking follows the existing rule below.
|
||||||
|
- **Unparking is a claim like any other.** When the wait ends, the parked
|
||||||
|
issue is work again and takes the slot. If you are already active
|
||||||
|
elsewhere, finish or release that work first, and say which you did on
|
||||||
|
both issues — the slot is still one. Nothing counts claims per builder
|
||||||
|
and no reconciler path enforces any of this: `claim_decision()` sees one
|
||||||
|
issue at a time by construction, and no such machinery should be built
|
||||||
|
expecting it to have been specified here. The discipline is the
|
||||||
|
declaration, not a counter.
|
||||||
- **Abandoning is fine; ghosting is not.** If you stop, say where you got to,
|
- **Abandoning is fine; ghosting is not.** If you stop, say where you got to,
|
||||||
push the branch if it holds anything useful, unassign, and restore
|
push the branch if it holds anything useful, unassign, and restore
|
||||||
`ready`.
|
`ready`.
|
||||||
|
|
@ -26,15 +110,40 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
- Branch per issue; open the PR **as a draft early**, `Closes #N` in the
|
- Branch per issue; open the PR **as a draft early**, `Closes #N` in the
|
||||||
body. Drafts are invisible to the reviewer panel on purpose — the draft
|
body. `Closes #N` does not cross repos: when the PR is in a different repo
|
||||||
phase is yours.
|
from its authorizing issue, use `Part of <owner>/<repo>#N` instead, and
|
||||||
|
in the same step set `offsite` and comment on that issue with the draft PR
|
||||||
|
link as soon as the draft opens.
|
||||||
|
Triage closes the authorizing issue by hand when its acceptance criteria
|
||||||
|
are met; at that handoff the builder reports whether the cross-repo PR
|
||||||
|
merged or closed and clears `offsite` in the same comment. The cross-repo
|
||||||
|
merge never closes the authorizing issue. This codifies the linkage
|
||||||
|
builders already used on rig#112 and ceremony #13/#16 rather than adding a
|
||||||
|
new review obligation.
|
||||||
|
`Closes #N` also does not survive a post-merge criterion: when the issue's
|
||||||
|
body states that an acceptance criterion can only be checked after the
|
||||||
|
merge — a live proof of a workflow trigger, a released-artifact check,
|
||||||
|
anything whose subject does not exist until the change is on the base
|
||||||
|
branch — the same-repo PR uses `Refs #N` instead, and triage closes the
|
||||||
|
issue by hand on the evidence, exactly as it does for cross-repo work.
|
||||||
|
The issue body is what says so; you never judge which issues qualify, and
|
||||||
|
absent that instruction `Closes #N` remains the default. The exception was
|
||||||
|
bought the hard way: #143 carried `Closes #137` as doctrine then required,
|
||||||
|
and the merge closed #137 with its post-merge criterion unmet (#151).
|
||||||
|
Drafts are invisible to the reviewer panel on
|
||||||
|
purpose — the draft phase is yours.
|
||||||
- **The issue's acceptance criteria are your definition of done.** Reproduce
|
- **The issue's acceptance criteria are your definition of done.** Reproduce
|
||||||
them as a checklist in the PR body and check them honestly as you go. If
|
them as a checklist in the PR body and check them honestly as you go. If
|
||||||
one turns out to be wrong or unreachable, say so on the issue and get it
|
one turns out to be wrong or unreachable, say so on the issue and get it
|
||||||
amended by triage — do not silently ship less than the issue says.
|
amended by triage — do not silently ship less than the issue says.
|
||||||
- Every behavior change adds one line to `CHANGELOG.md` under
|
- Every behavior change writes one fragment, `changelog.d/<issue>.md`,
|
||||||
`## Unreleased` — insert **above** the heading below it, never over it
|
named for the authorizing issue (`<repo>-<issue>.md` when the work is
|
||||||
(the monotonic guard's whole reason to exist).
|
cross-repo) — the exact prose that will be published, nothing else: `- `
|
||||||
|
bullets, and in a grouped repo the `### Added` / `### Changed` /
|
||||||
|
`### Fixed` headings inside the fragment, creating a rarer kind only when
|
||||||
|
a change genuinely is one. Never edit `CHANGELOG.md` for an entry — the
|
||||||
|
release PR assembles the section from the fragments (#112); the monotonic
|
||||||
|
guard still refuses anything that deletes a shipped heading.
|
||||||
- Follow the repo's conventions file and match the code you touch. Tests are
|
- Follow the repo's conventions file and match the code you touch. Tests are
|
||||||
not optional: the issue's test plan is the floor, not the ceiling.
|
not optional: the issue's test plan is the floor, not the ceiling.
|
||||||
- **Scope discipline: the PR does the issue — whole, and nothing else.**
|
- **Scope discipline: the PR does the issue — whole, and nothing else.**
|
||||||
|
|
@ -49,16 +158,81 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
panel roster and any repo-specific flow notes live in that repo's own
|
panel roster and any repo-specific flow notes live in that repo's own
|
||||||
CONTRIBUTING; everything below is the shared flow.)
|
CONTRIBUTING; everything below is the shared flow.)
|
||||||
|
|
||||||
1. Mark ready-for-review; request **the whole panel** (the roster is in the
|
1. Mark ready-for-review; request **the whole panel**. The panel is the roster
|
||||||
repo's CONTRIBUTING).
|
of the repo the **PR** is in, minus you — never the roster of the repo the
|
||||||
|
issue is in. The PR repo's `.github/labels.conf` `panel=` line is the
|
||||||
|
machine's answer; its CONTRIBUTING roster is the human-readable answer,
|
||||||
|
and `panel=` governs if they disagree because that is what the state
|
||||||
|
machine reads. If the PR repo names no roster, ask triage on the
|
||||||
|
authorizing issue before marking ready-for-review; do not guess. You may
|
||||||
|
request an off-panel reviewer, but say that their verdict is advisory and
|
||||||
|
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
|
||||||
|
larger, and the doctrine had not said which roster governed.
|
||||||
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, then push the fixes, then re-request exactly the
|
||||||
reviewers who did not approve. Prefer verification over argument: when a
|
reviewers who did not approve. 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
|
||||||
in the PR — a maintainer can be asked for a ruling; silence and
|
in the PR — silence and force-forward are not options. A panel deadlock
|
||||||
force-forward are not options.
|
is one kind of human-owned decision; use the ruling ask below
|
||||||
|
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
|
||||||
|
|
||||||
|
## The ruling ask
|
||||||
|
|
||||||
|
Set `needs-ruling` whenever a decision belongs to a human: org policy,
|
||||||
|
published artifacts, secrets, prod, or any choice whose cost lands outside
|
||||||
|
the PR. A panel deadlock is one instance, not the definition. The builder is
|
||||||
|
the accountable flag-setter on a PR and consolidates the decision into one
|
||||||
|
comment rather than forwarding several reviewers' phrasings
|
||||||
|
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
|
||||||
|
|
||||||
|
Keep at most these five lines above the fold and put all other analysis
|
||||||
|
inside the fold. The field labels are fixed because the ruling machinery
|
||||||
|
checks for them ([#50 D12](https://github.com/heavy-duty/ceremony/issues/50)):
|
||||||
|
|
||||||
|
```text
|
||||||
|
🧭 needs-ruling — <the decision, one line>
|
||||||
|
Options: A — <one clause> B — <one clause>
|
||||||
|
Recommend: A, because <one clause>.
|
||||||
|
Blocked: <what stops; what continues meanwhile>
|
||||||
|
Default: <A at 2026-07-23T21:00Z if no ruling> | none — hard block
|
||||||
|
<details><summary>Analysis</summary>…everything else…</details>
|
||||||
|
```
|
||||||
|
|
||||||
|
The options must be exhaustive and mutually exclusive; more than three means
|
||||||
|
the question is not ready. `Recommend:` is mandatory — omitting it hands the
|
||||||
|
whole problem to the human. `Blocked:` names both what stops and what
|
||||||
|
continues. Write a timed `Default:` only when you are affirmatively confident
|
||||||
|
the decision is reversible inside the PR before merge. Unsure is not a tie:
|
||||||
|
it is a hard block. Published artifacts, secrets, prod, and org policy are
|
||||||
|
hard blocks by construction ([#50 D12–D13](https://github.com/heavy-duty/ceremony/issues/50)).
|
||||||
|
|
||||||
|
The ladder is anchored to the current episode's `needs-ruling` **`labeled`
|
||||||
|
event**, not its `Default:` deadline or the last activity
|
||||||
|
([#50 D13–D14](https://github.com/heavy-duty/ceremony/issues/50)):
|
||||||
|
|
||||||
|
- **0–12h:** proceed when a still-clear, reversible default expires, and say
|
||||||
|
out loud that you did. A hard block waits.
|
||||||
|
- **at 12h:** do not fire a stale default. Re-read it against what has landed
|
||||||
|
and ask whether it still holds and whether reasonable doubt remains. If
|
||||||
|
doubt has appeared, make it a hard block.
|
||||||
|
- **at 24h:** proceed regardless, **as a PR**. Pick an option and state in the
|
||||||
|
PR body which way you went and what doubt remains. Nothing merges by this;
|
||||||
|
the human still gates the merge.
|
||||||
|
- **past 24h:** hand the choice to triage. Triage picks the option, records it
|
||||||
|
as a decision, and remains accountable; the operator can overturn it at
|
||||||
|
merge.
|
||||||
|
|
||||||
|
A re-flag starts a fresh ladder. The ladder applies whatever `Default:` says,
|
||||||
|
including a hard block, and an active back-and-forth still climbs it. This is
|
||||||
|
different from the 7-day nudge, which resets on real activity. The machine
|
||||||
|
observes both clocks but never sets, clears, or decides `needs-ruling`.
|
||||||
|
|
||||||
|
The label stays until agreement is *reached*, not until the maintainer
|
||||||
|
replies. The setter records the ruling, removes the label, and returns the
|
||||||
|
item to its flow in the same comment ([LABELS.md](LABELS.md)).
|
||||||
|
|
||||||
## Handoff
|
## Handoff
|
||||||
|
|
||||||
|
|
@ -72,5 +246,6 @@ this is a release PR) — hand it to the human, in order:
|
||||||
|
|
||||||
The label write is optimistic — the reconciler validates it, and takes it
|
The label write is optimistic — the reconciler validates it, and takes it
|
||||||
back if the PR is not actually mergeable-right-now. Then stop: the PR is the
|
back if the PR is not actually mergeable-right-now. Then stop: the PR is the
|
||||||
human's. Address what comes back (`state:addressing`) and re-hand-off the
|
human's. The claim is now parked as shape 4 (Picking, above) — the handoff
|
||||||
same way.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ the labels workflow's bootstrap dispatch (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
|
||||||
triage → build queue, doctrine-enforced today, machinery to follow —
|
triage → build queue, reconciled by the work-queue sweep). One rule joins
|
||||||
issue #18). One rule joins everything: **states are machine-owned, intent
|
everything: **states are machine-owned, intent
|
||||||
labels are hand-set** — a hand-moved state label is a lie waiting to happen,
|
labels are hand-set** — a hand-moved state label is a lie waiting to happen,
|
||||||
and the reconciler recomputes it from GitHub's own facts.
|
and the reconciler recomputes it from GitHub's own facts.
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ and the reconciler recomputes it from GitHub's own facts.
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `state:building` | `#FBCA04` | the builder — PR is a draft |
|
| `state:building` | `#FBCA04` | the builder — PR is a draft |
|
||||||
| `state:bots-reviewing` | `#1D76DB` | the reviewer panel to finish the round (a request is live) |
|
| `state:bots-reviewing` | `#1D76DB` | the reviewer panel to finish the round (a request is live) |
|
||||||
| `state:addressing` | `#D93F0B` | the builder — round complete without full approval, or nobody was asked, or a blocker is up |
|
| `state:addressing` | `#D93F0B` | the builder — round complete without full approval, or nobody was asked, or a blocker is up, or a ruling is pending |
|
||||||
| `state:needs-human` | `#8250DF` | the human — **this PR could be merged right now**: zero blockers, whole panel approved the current head |
|
| `state:needs-human` | `#8250DF` | the human — **this PR could be merged right now**: zero blockers, whole panel approved the current head |
|
||||||
|
|
||||||
`bots-reviewing` vs `addressing` is deliberate: staleness in the first means
|
`bots-reviewing` vs `addressing` is deliberate: staleness in the first means
|
||||||
|
|
@ -54,11 +54,11 @@ strips it on sight).
|
||||||
| `blocked` | `#6A737D` | waiting on another issue or PR (`Blocked by #N` in the body names it) | triage; anyone may correct it |
|
| `blocked` | `#6A737D` | waiting on another issue or PR (`Blocked by #N` in the body names it) | triage; anyone may correct it |
|
||||||
| `epic` | `#5319E7` | organizes other issues via a dependency-ordered task list; **builders never pick an epic** | triage |
|
| `epic` | `#5319E7` | organizes other issues via a dependency-ordered task list; **builders never pick an epic** | triage |
|
||||||
|
|
||||||
The invariant a board scan relies on: every open issue is either
|
The work-queue sweep enforces the invariant a board scan relies on: every open issue is either
|
||||||
`needs-triage`, `epic`, or carries exactly one of `ready` / `claimed` /
|
`needs-triage`, `epic`, or carries exactly one of `ready` / `claimed` /
|
||||||
`blocked`. A `claimed` issue with no open PR and no activity is what the
|
`blocked`. It flags conflicts rather than guessing intent. A `claimed` issue
|
||||||
staleness sweep will reclaim (issue #18); until that machinery exists,
|
with no open PR and no activity for 48 hours is reclaimed by the sweep: it
|
||||||
[TRIAGE.md](TRIAGE.md) owns the hygiene by hand.
|
comments, unassigns the stale owner, and restores `ready`.
|
||||||
|
|
||||||
## Cross-cutting (PRs and issues)
|
## Cross-cutting (PRs and issues)
|
||||||
|
|
||||||
|
|
@ -66,21 +66,119 @@ staleness sweep will reclaim (issue #18); until that machinery exists,
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied |
|
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied |
|
||||||
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) |
|
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) |
|
||||||
|
| `offsite` | `#CFD3D7` | issue deliverable is a PR in another repository; set by the builder with the draft link and cleared by the builder at handoff |
|
||||||
|
| `needs-ruling` | `#D4C5F9` | a human-owned decision is required; use BUILDER.md's ruling template and ladder. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply |
|
||||||
|
| `attention` | `#D93F0B` | issue-only demand parked for the assignee; hand-set, and never written by the machine |
|
||||||
| `release` | `#0E8A16` | release flow, versioning, packaging work — and the ceremony PR itself |
|
| `release` | `#0E8A16` | release flow, versioning, packaging work — and the ceremony PR itself |
|
||||||
| `merge-next` | `#0E8A16` | head of the merge queue — merge this one next. Queue order is *intent*: never set by the reconciler, only cleared by it |
|
| `merge-next` | `#0E8A16` | head of the merge queue — merge this one next. Queue order is *intent*: never set by the reconciler, only cleared by it |
|
||||||
|
|
||||||
|
`needs-ruling` marks where the human's turn is when the pending thing is a
|
||||||
|
*decision*, not a merge ([#50 D1–D14](https://github.com/heavy-duty/ceremony/issues/50)).
|
||||||
|
It applies to any human-owned decision — org policy, published artifacts,
|
||||||
|
secrets, prod, or any choice whose cost lands outside the work. A panel
|
||||||
|
deadlock is one instance, not the definition (D11). It is not
|
||||||
|
`state:needs-human`: that label means exactly "this PR could be merged right
|
||||||
|
now", and the retired `state:needs-rebase` is the family's proof that a
|
||||||
|
label meaning two things lies about both. It is not a `blocker:*` either:
|
||||||
|
every blocker names work the *builder* owes, a ruling is owed by the human —
|
||||||
|
and the flag must live on issues too, where blockers do not exist. On issues
|
||||||
|
it coexists with the queue labels (the one-of-three invariant above ignores
|
||||||
|
it); its color is the light shade of `state:needs-human`'s, so the human
|
||||||
|
axis reads as one family. It is a state, not a signal: set only with the
|
||||||
|
[canonical escalation contract](BUILDER.md#the-ruling-ask) (D12). A bare
|
||||||
|
flag is noise. The comment carries exhaustive, mutually exclusive options
|
||||||
|
(at most three), a mandatory recommendation, what stops and what continues,
|
||||||
|
and either a default affirmatively known to be reversible inside the PR or
|
||||||
|
`none — hard block`. Unsure is a block; published artifacts, secrets, prod,
|
||||||
|
and org policy are hard blocks by construction (D13).
|
||||||
|
|
||||||
|
The ruling ladder runs from the current episode's `needs-ruling` **`labeled`
|
||||||
|
event** (D13–D14):
|
||||||
|
|
||||||
|
- **0–12h:** a clear, reversible decision may proceed when its stated default
|
||||||
|
expires, saying out loud that it did; anything with reasonable doubt waits
|
||||||
|
as a hard block.
|
||||||
|
- **at 12h:** the setter re-reads the default against what has landed and asks
|
||||||
|
whether it still holds and whether doubt remains. A stale default does not
|
||||||
|
fire; new doubt makes it a hard block.
|
||||||
|
- **at 24h:** the builder proceeds regardless, **as a PR**, stating the option
|
||||||
|
chosen and the doubt that remains. Nothing merges by this; the human still
|
||||||
|
gates the merge.
|
||||||
|
- **past 24h:** triage picks the option, records it as a decision, and remains
|
||||||
|
accountable. The operator may overturn it at merge.
|
||||||
|
|
||||||
|
A re-flag starts a new ladder. The rungs apply whatever `Default:` says,
|
||||||
|
including a hard block. Active discussion still climbs the ladder; by
|
||||||
|
contrast, the separate 7-day nudge resets on real activity. The machine
|
||||||
|
observes the rungs but never sets, clears, or decides `needs-ruling`.
|
||||||
|
|
||||||
|
The flag stays up until agreement is *reached* — a human reply alone does not
|
||||||
|
clear it — and its setter closes it out: records the ruling as a decision in
|
||||||
|
one comment, removes the label, and returns the item to its flow in that same
|
||||||
|
comment, never as a side effect. If the human disagrees that agreement was
|
||||||
|
reached, the label goes back on. The reconciler refuses `state:needs-human`
|
||||||
|
while it stands (the PR falls to `state:addressing` — the ball on the PR is
|
||||||
|
the builder's, who carries the ruling in), and the staleness sweep skips it,
|
||||||
|
because waiting on a human is legitimately quiet. Quiet, but not unwatched
|
||||||
|
(#52, both surfaces): a flag set with no escalation comment from its setter
|
||||||
|
is called out by the sweep — comment-only, scoped to the labeled event, the
|
||||||
|
label never removed — and a ruling with no real activity for 7 days draws a
|
||||||
|
comment-only nudge addressed to the decider, linking the escalation. The
|
||||||
|
nudge carries no marker on purpose: the comment is itself activity, so it
|
||||||
|
resets its own window and never repeats within a quiet week. Label churn is
|
||||||
|
not activity — the clock reads comments, reviews and commits, or the sweep
|
||||||
|
would reset itself.
|
||||||
|
|
||||||
|
`offsite` is issue-only and records that a claimed issue's deliverable lives
|
||||||
|
in another repository, where a closing reference cannot make a local open PR
|
||||||
|
visible to the sweep (#68). The builder sets it in the same step that posts
|
||||||
|
the cross-repo draft link, then clears it at handoff in the same comment that
|
||||||
|
reports whether that PR merged or closed. The machine reads the flag and
|
||||||
|
never writes it. It stops only the claim-reclaim clock: missing assignees are
|
||||||
|
still flagged, queue-label conflicts and missing queue state are still
|
||||||
|
repaired, and epic-completion and PR-side stale behavior are unchanged. The
|
||||||
|
sweep tells the assignee once when every visible cross-referenced PR has
|
||||||
|
closed; it only tells, and never clears the flag or changes the claim.
|
||||||
|
|
||||||
|
`attention` is issue-only and says a demand is parked on an issue for its
|
||||||
|
assignee. Anyone who needs that assignee's hands — triage, the operator, or a
|
||||||
|
sibling agent — sets it. The assignee alone clears it, as the first act of
|
||||||
|
pickup together with a short comment; that removal is the acknowledgement
|
||||||
|
and re-arms the flag for the next demand. If the session dies before the ack,
|
||||||
|
the still-visible flag launches the next pickup instead. An unanswered flag
|
||||||
|
is auditable evidence on the board.
|
||||||
|
|
||||||
|
The flag is additive: it composes with `ready`, `claimed`, or `blocked` and
|
||||||
|
with `needs-ruling`, and never substitutes for queue state. It pauses no
|
||||||
|
clock. Unlike `offsite` and `needs-ruling`, which make silence legitimate,
|
||||||
|
unanswered `attention` is exactly the silence the 48-hour reclaim should
|
||||||
|
take. It is hand-set doctrine only: nothing in `actions/` sets, clears,
|
||||||
|
reads, or validates it, and no reconciler enforces the assignee requirement.
|
||||||
|
An `attention` issue without an assignee is therefore a board bug, not a
|
||||||
|
demand; anyone may assign it or remove the flag.
|
||||||
|
|
||||||
|
The three signals are mutually distinct: `attention` means an assignee owes
|
||||||
|
a move; `needs-ruling` means a human owes a decision under
|
||||||
|
[the escalation contract and ladder](BUILDER.md#the-ruling-ask); and a bare
|
||||||
|
`@`-mention is an FYI that demands nothing and remains perfectly fine. A
|
||||||
|
demand that is itself a human decision carries `needs-ruling`, never both.
|
||||||
|
This distinction records the
|
||||||
|
[#16 missed-ruling incident](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198)
|
||||||
|
and why the rejected mention poll is not returning: ordinary thread traffic
|
||||||
|
re-arms mentions, but only the writer can declare that a move is owed (#83).
|
||||||
|
|
||||||
## Scope — which surface? (PRs and issues, any number)
|
## Scope — which surface? (PRs and issues, any number)
|
||||||
|
|
||||||
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The
|
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The
|
||||||
set is per-repo (`.github/labels.conf`); PRs get theirs from changed paths via
|
set is per-repo: PRs get theirs from changed paths via the labels workflow's
|
||||||
actions/labeler, issues get theirs from triage. This repo's set:
|
scope job — an additive write only, so a label applied by hand or by an agent
|
||||||
|
while the machine runs always survives it (#130) — and issues get theirs from
|
||||||
| Label | Covers |
|
triage. This file never enumerates a set — it is mirrored
|
||||||
|---|---|
|
byte-identically into every governed repo, and any list it carried would be
|
||||||
| `scope:release-flow` | the reusable release workflow, decide, the doors |
|
true in one repo and false in the rest (#104). The set for the repo you are
|
||||||
| `scope:guards` | changelog-armed / changelog-monotonic / drill-recorded |
|
standing in lives in the two places that are true wherever you read them: its
|
||||||
| `scope:labels` | the labels workflow, reconciler, this taxonomy |
|
`.github/labels.conf` (the definitions, one `name|color|description` row per
|
||||||
| `scope:docs` | README doctrine, CONSUMERS.md, the role files |
|
scope) and its own `CONTRIBUTING.md`, beside the other repo-specific facts.
|
||||||
|
|
||||||
## Issue types
|
## Issue types
|
||||||
|
|
||||||
|
|
@ -92,7 +190,8 @@ on a PR would say the same thing twice and drift.
|
||||||
|
|
||||||
The labels workflow (issue #10) recomputes PR state statelessly on PR events
|
The labels workflow (issue #10) recomputes PR state statelessly on PR events
|
||||||
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
|
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
|
||||||
manual dispatch. Issue-flow labels are doctrine-owned until #18 lands
|
manual dispatch. The sweep warns when the core taxonomy declares a label the
|
||||||
machinery for them. Default GitHub labels (`duplicate`, `invalid`,
|
repository lacks. The same workflow reconciles issue-flow labels on issue
|
||||||
`question`, `wontfix`, `help wanted`, `good first issue`) are deleted at
|
events and during the scheduled sweep. Default GitHub labels (`duplicate`,
|
||||||
bootstrap — a `question` is a discussion, not an issue.
|
`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
|
||||||
|
deleted at bootstrap — a `question` is a discussion, not an issue.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@ The machine reads only your **verdict**; humans read your reasons.
|
||||||
their discretion. Anything blocking — including a question whose answer
|
their discretion. Anything blocking — including a question whose answer
|
||||||
gates your approval — is **request changes**, saying exactly what
|
gates your approval — is **request changes**, saying exactly what
|
||||||
unblocks it.
|
unblocks it.
|
||||||
|
- **Name what you could not verify, in the verdict body.** Say which checks
|
||||||
|
you could not run and why, and what you relied on instead: CI, reading, or
|
||||||
|
a narrower probe. An unstated environment gap reads as coverage — exactly
|
||||||
|
the blind spot Kimi's [crew report](https://github.com/heavy-duty/crew/blob/main/kimi-bot-andresmgsl/assessment.md)
|
||||||
|
identified for boxes without `node` or `shellcheck`.
|
||||||
- An approval you would not defend to the human is a defect. You are not
|
- An approval you would not defend to the human is a defect. You are not
|
||||||
being asked to be agreeable; you are being asked to be right.
|
being asked to be agreeable; you are being asked to be right.
|
||||||
|
|
||||||
|
|
@ -23,14 +28,27 @@ The machine reads only your **verdict**; humans read your reasons.
|
||||||
|
|
||||||
In order of authority:
|
In order of authority:
|
||||||
|
|
||||||
1. **The issue's acceptance criteria** — the PR's `Closes #N` names your
|
1. **The issue's acceptance criteria** — the PR's `Closes #N`, its
|
||||||
spec. Check every criterion; a PR that ships less than the issue says is
|
cross-repo `Part of <owner>/<repo>#N`, or its `Refs #N` when the issue
|
||||||
a request-changes even if the code is beautiful.
|
body marks a criterion post-merge, names your spec. That last shape is
|
||||||
|
not a defect: the issue directs it, triage owns that close, and a
|
||||||
|
request-changes on the "missing" keyword enforces the bug the shape
|
||||||
|
exists to fix — `Closes #137` closed its issue with a post-merge
|
||||||
|
criterion unmet (#151). Check every
|
||||||
|
criterion; a PR that ships less than the issue says is a request-changes
|
||||||
|
even if the code is beautiful.
|
||||||
2. **The repo's load-bearing constraints** — the rules bought with
|
2. **The repo's load-bearing constraints** — the rules bought with
|
||||||
incidents (in ceremony itself: issue #1's constraint list; in a governed
|
incidents (in ceremony itself: issue #1's constraint list; in a governed
|
||||||
repo: its own CONTRIBUTING plus ceremony's README). A change that
|
repo: its own CONTRIBUTING plus ceremony's README). A change that
|
||||||
"simplifies away" a constraint gets request-changes with a link to the
|
"simplifies away" a constraint gets request-changes with a link to the
|
||||||
incident that made the rule.
|
incident that made the rule.
|
||||||
|
- **Verify a pinned consumer at its pin, not ceremony's `main`.** Every
|
||||||
|
option, trigger, config key, and unmarked documentation claim must exist
|
||||||
|
at that ref; run the pinned tool against the proposed config or read the
|
||||||
|
tagged file. On [box#164](https://github.com/heavy-duty/box/pull/164),
|
||||||
|
`0.1.0`'s `load_config` rejected `triage-actors=...` with
|
||||||
|
`malformed label row` and `exit=1`. CI green on a conversion PR proves
|
||||||
|
nothing about the new config: the base branch's workflow is what ran.
|
||||||
3. **The code itself** — correctness first, then tests (does the test plan's
|
3. **The code itself** — correctness first, then tests (does the test plan's
|
||||||
floor exist? do the failure cases actually fail?), then conventions.
|
floor exist? do the failure cases actually fail?), then conventions.
|
||||||
Changelog line present for behavior changes; comments carry why, not
|
Changelog line present for behavior changes; comments carry why, not
|
||||||
|
|
@ -40,6 +58,48 @@ In order of authority:
|
||||||
test settles what a comment thread can't. A review that says "I ran X and
|
test settles what a comment thread can't. A review that says "I ran X and
|
||||||
saw Y" outranks one that says "this looks like it might".
|
saw Y" outranks one that says "this looks like it might".
|
||||||
|
|
||||||
|
## Where you review
|
||||||
|
|
||||||
|
- **A review request on you is your authorization** in any `heavy-duty` repo
|
||||||
|
and on any fleet member's fork. You need no separate permission and do not
|
||||||
|
wait for the repo to appear on a list: review is reversible
|
||||||
|
read-plus-comment work, and the requester already decided it should happen.
|
||||||
|
- **A request is authorization, not panel membership.** Convergence is
|
||||||
|
measured against the target repo's `panel=` roster minus the author. If you
|
||||||
|
are requested off-panel, post the verdict anyway and say in its body that
|
||||||
|
it is advisory; neither your silence nor your request-changes is a gate the
|
||||||
|
reconciler enforces. The nine-hour wait for kimi's off-panel verdict on
|
||||||
|
rig#112 showed why authorization and membership must not be conflated.
|
||||||
|
- **Being requested is a wake condition of its own.** It is how work in a
|
||||||
|
repo you have never heard of reaches you; a repo list finds only work in
|
||||||
|
repos somebody thought to list.
|
||||||
|
|
||||||
|
## How you work the queue
|
||||||
|
|
||||||
|
- **Your queue is the API, not the search index.** Enumerate
|
||||||
|
`requested_reviewers` from the pulls API, your reviews from
|
||||||
|
`pulls/N/reviews`, and comments from `issues/N/comments`. Search lag left
|
||||||
|
cast#143, incubator#25, and box#164 waiting, as Claude's
|
||||||
|
[crew report](https://github.com/heavy-duty/crew/blob/main/claude-bot-andresmgsl/assessment.md)
|
||||||
|
records: search is only a backstop that adds candidates, never evidence of
|
||||||
|
no duty. `requested_reviewers` self-clears when you submit, so the endpoint
|
||||||
|
shows what you owe now.
|
||||||
|
- **Every write is one-shot, keyed to (you, PR, head SHA).** Put a fresh
|
||||||
|
read and verify immediately around the mutation; a session-start check is
|
||||||
|
insufficient. If verification says it landed, stop even when the CLI
|
||||||
|
looked unhappy. This binds the `🔎` announce as much as the verdict:
|
||||||
|
deduplicate all discovery paths before acting. Duplicate verdicts on
|
||||||
|
[#26](https://github.com/heavy-duty/ceremony/pull/26),
|
||||||
|
[#29](https://github.com/heavy-duty/ceremony/pull/29), and
|
||||||
|
[#39](https://github.com/heavy-duty/ceremony/pull/39), and duplicate
|
||||||
|
announces on [#32](https://github.com/heavy-duty/ceremony/pull/32), bought
|
||||||
|
the rule; do not answer a double-post with a third comment.
|
||||||
|
- **Review each head in a throwaway checkout; keep the main clone clean.**
|
||||||
|
Use a detached worktree per PR head and remove it after the verdict. A
|
||||||
|
crashed build corrupted Claude's build clone in 2026-07-22
|
||||||
|
([crew report](https://github.com/heavy-duty/crew/blob/main/claude-bot-andresmgsl/knowledge.md));
|
||||||
|
running another tree in the clone you keep risks the whole box.
|
||||||
|
|
||||||
## What you do not do
|
## What you do not do
|
||||||
|
|
||||||
- **Re-litigate the spec.** The issue's decisions were made in triage and,
|
- **Re-litigate the spec.** The issue's decisions were made in triage and,
|
||||||
|
|
@ -64,7 +124,16 @@ saw Y" outranks one that says "this looks like it might".
|
||||||
- Convergence = every panel verdict approves the current head, no
|
- Convergence = every panel verdict approves the current head, no
|
||||||
`blocker:*` standing. Then the builder hands off (`state:needs-human`) and
|
`blocker:*` standing. Then the builder hands off (`state:needs-human`) and
|
||||||
the panel's job is done.
|
the panel's job is done.
|
||||||
- If a round exposes a disagreement **within the panel**, argue it in the PR
|
- Flag an unowned decision when it belongs to a human: org policy, published
|
||||||
with evidence until one side concedes or the builder escalates to the
|
artifacts, secrets, prod, or any choice whose cost lands outside the PR. A
|
||||||
maintainer for a ruling. Two reviewers pulling a builder in opposite
|
disagreement within the panel is one instance, not the definition
|
||||||
directions without resolution is a panel failure, not a builder failure.
|
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)). Argue a
|
||||||
|
panel disagreement in the PR with evidence until one side concedes or the
|
||||||
|
builder escalates; two reviewers pulling a builder in opposite directions
|
||||||
|
without resolution is a panel failure, not a builder failure.
|
||||||
|
`needs-ruling` is set by the **builder**, never by you: one accountable
|
||||||
|
flag-setter per PR hands the human one consolidated question. State the
|
||||||
|
unowned decision precisely enough for the builder to write
|
||||||
|
[the canonical ruling ask](BUILDER.md#the-ruling-ask), including what
|
||||||
|
stops and what continues ([#50 D12](https://github.com/heavy-duty/ceremony/issues/50);
|
||||||
|
[LABELS.md](LABELS.md)).
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,44 @@ is the failure this whole flow exists to prevent.
|
||||||
answers would let you write the issue — then stop and wait. Do not mint an
|
answers would let you write the issue — then stop and wait. Do not mint an
|
||||||
issue that carries the ambiguity forward; that just moves your job onto
|
issue that carries the ambiguity forward; that just moves your job onto
|
||||||
the builder.
|
the builder.
|
||||||
3. **Escalate.** The blocker is a *decision* only a human owns — scope,
|
3. **Escalate.** The pending thing is a decision only a human owns — org
|
||||||
money, product direction, breaking a public contract. Say precisely what
|
policy, published artifacts, secrets, prod, or any choice whose cost lands
|
||||||
the decision is, list the options with your recommendation, and name the
|
outside the work. A panel deadlock is one instance, not the definition
|
||||||
decider. The discussion is where humans decide; wait there.
|
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)). Say
|
||||||
|
precisely what the decision is, name the decider, and use
|
||||||
|
[BUILDER.md's canonical ruling template](BUILDER.md#the-ruling-ask),
|
||||||
|
including its options, recommendation, blocked/continues statement, and
|
||||||
|
reversible-only default rules ([#50 D12–D13](https://github.com/heavy-duty/ceremony/issues/50)).
|
||||||
|
The discussion is where humans decide; wait there. When the decision
|
||||||
|
blocks something already on the board — an existing issue, or minted work
|
||||||
|
a discussion's ruling gates — set `needs-ruling` on it too, so the board
|
||||||
|
shows where the human's turn is; the issue keeps its queue label.
|
||||||
|
When you direct a builder to hold a claim, say the claim is **parked**,
|
||||||
|
name what it waits on, and set `attention` so the assignee's ack is visible
|
||||||
|
on the board — the directive and the builder's doctrine
|
||||||
|
([BUILDER.md](BUILDER.md#claiming)) must use one word.
|
||||||
|
Immediately before asserting label-borne state in prose — a hold, a
|
||||||
|
claim, a queue state, whether in a comment, a body header, or a
|
||||||
|
`needs-ruling` ask — re-read that issue's **label events**
|
||||||
|
(`gh api /repos/{owner}/{repo}/issues/{n}/timeline`), not just its
|
||||||
|
comments: the answer often arrives as a label with no comment, and a
|
||||||
|
write that re-read only the thread races it. Both 2026-07-24 failures —
|
||||||
|
[a header correction on #149](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070758613)
|
||||||
|
asserting a hold 58 seconds after its lift, and
|
||||||
|
[a `needs-ruling` ask on #151](https://github.com/heavy-duty/ceremony/issues/151#issuecomment-5070768876)
|
||||||
|
the operator's label events had answered 132 seconds earlier — are this
|
||||||
|
sentence's absence.
|
||||||
|
Past 24 hours from the current episode's `labeled` event, if the ruling
|
||||||
|
still stands and doubt remains, it is triage's duty to pick the option the
|
||||||
|
builder proceeds on, record that pick as a decision, and stay accountable
|
||||||
|
for it; the operator may overturn it at merge
|
||||||
|
([#50 D13–D14](https://github.com/heavy-duty/ceremony/issues/50)). You set
|
||||||
|
the flag, so you also close it out ([LABELS.md](LABELS.md)): judge when
|
||||||
|
agreement is reached, record the ruling as a decision in one comment,
|
||||||
|
remove the label, and return the issue to its flow in that same comment;
|
||||||
|
when that ruling or any directive or answered builder question delivers
|
||||||
|
the assignee's next move in prose, set `attention` in the same comment.
|
||||||
|
This is not a substitute for minting work or for `needs-ruling`.
|
||||||
4. **Decline.** Real idea, wrong repo or wrong time. Say why plainly, link
|
4. **Decline.** Real idea, wrong repo or wrong time. Say why plainly, link
|
||||||
where it belongs if anywhere, close. A refusal with reasons is a good
|
where it belongs if anywhere, close. A refusal with reasons is a good
|
||||||
outcome; a zombie discussion is not.
|
outcome; a zombie discussion is not.
|
||||||
|
|
@ -56,9 +90,18 @@ Every issue you mint carries, in this order:
|
||||||
- **Tasks**: the steps, checkboxed, in order.
|
- **Tasks**: the steps, checkboxed, in order.
|
||||||
- **Acceptance criteria**: checkboxed, verifiable, and honest — these become
|
- **Acceptance criteria**: checkboxed, verifiable, and honest — these become
|
||||||
the builder's definition of done and the reviewer's review spec, verbatim.
|
the builder's definition of done and the reviewer's review spec, verbatim.
|
||||||
|
A criterion that can only be checked after the merge must carry its own
|
||||||
|
mechanism, in the criterion itself: that it is post-merge, that triage
|
||||||
|
owns the close, and that the PR references the issue with `Refs #N`
|
||||||
|
rather than `Closes #N`. A criterion that survives the merge only if
|
||||||
|
someone remembers to reopen the issue is an incomplete criterion — #137's
|
||||||
|
amended body is the worked example, reopened by hand after `Closes #137`
|
||||||
|
closed it with the criterion unmet (#151).
|
||||||
- **Test plan**: what proves it, including the cases that must fail.
|
- **Test plan**: what proves it, including the cases that must fail.
|
||||||
- **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an
|
- **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an
|
||||||
epic organizes it.
|
epic organizes it. Name a cross-repo dependency the same way with its
|
||||||
|
repository qualified (`Blocked by repo#N` or `owner/repo#N`); the sweep
|
||||||
|
cannot resolve it, so triage verifies it and flips the issue by hand.
|
||||||
- **Labels**: type (`bug`/`enhancement`/`documentation`), `scope:*`, and
|
- **Labels**: type (`bug`/`enhancement`/`documentation`), `scope:*`, and
|
||||||
exactly one of `ready` / `blocked` (see [LABELS.md](LABELS.md)).
|
exactly one of `ready` / `blocked` (see [LABELS.md](LABELS.md)).
|
||||||
|
|
||||||
|
|
@ -75,16 +118,27 @@ dependency-ordered task list of child issues. Children reference the epic;
|
||||||
the epic's checklist is the progress view. Builders never pick the epic
|
the epic's checklist is the progress view. Builders never pick the epic
|
||||||
itself. Keep the checklist current — a stale epic misleads every scan.
|
itself. Keep the checklist current — a stale epic misleads every scan.
|
||||||
|
|
||||||
## Backlog hygiene (yours until #18 automates it)
|
## Backlog hygiene
|
||||||
|
|
||||||
- **Dedup before minting** — search issues *and* closed issues; extend or
|
- **Dedup before minting** — search issues *and* closed issues; extend or
|
||||||
reopen before duplicating.
|
reopen before duplicating.
|
||||||
- **Flip `blocked` → `ready`** when the named dependency lands.
|
- The issue-flow sweep flips `blocked` → `ready` when every named dependency
|
||||||
- **Reclaim abandoned claims**: `claimed` + no open PR + no activity →
|
lands, and flags a blocked issue whose dependency declaration is unreadable.
|
||||||
comment, unassign, restore `ready`.
|
- The sweep reclaims abandoned claims after 48 hours: `claimed` + no open PR
|
||||||
|
+ no activity → comment, unassign, restore `ready`.
|
||||||
|
- Automation never guesses intent. Resolve the conflict comments it leaves on
|
||||||
|
malformed queue states, and close or extend completed epics when nudged.
|
||||||
- **Close obsolete issues** with the reason and a link to what obsoleted
|
- **Close obsolete issues** with the reason and a link to what obsoleted
|
||||||
them. Every label on every open issue stays true; the board is only worth
|
them. Every label on every open issue stays true; the board is only worth
|
||||||
scanning if it does not lie.
|
scanning if it does not lie.
|
||||||
|
- **A lifted hold makes its body prose stale in the same instant, and the
|
||||||
|
body is yours.** The "stays true" bar above extends past the labels to
|
||||||
|
the prose that describes them: when a hold lifts, correcting the body
|
||||||
|
header that described it is your move in the same tick — not the
|
||||||
|
builder's, and not left for the next reader to diff. On
|
||||||
|
[#149](https://github.com/heavy-duty/ceremony/issues/149) the lift
|
||||||
|
arrived by label alone and the body said held for the next five and a
|
||||||
|
half minutes; two builders read that window to opposite conclusions.
|
||||||
|
|
||||||
## What you never do
|
## What you never do
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/DISCUSSION_TEMPLATE/ideas.yml
vendored
2
.github/DISCUSSION_TEMPLATE/ideas.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# Copied from heavy-duty/ceremony@0.1.0 .github/DISCUSSION_TEMPLATE/ideas.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
# Copied from heavy-duty/ceremony@0.3.0 .github/DISCUSSION_TEMPLATE/ideas.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
||||||
# Light on purpose: discussions are where ambiguity is ALLOWED — a form
|
# Light on purpose: discussions are where ambiguity is ALLOWED — a form
|
||||||
# that demands rigor at the door defeats the room's purpose (heavy-duty/ceremony#24,
|
# that demands rigor at the door defeats the room's purpose (heavy-duty/ceremony#24,
|
||||||
# decision 4). If these prompts fight the flow in practice, delete them
|
# decision 4). If these prompts fight the flow in practice, delete them
|
||||||
|
|
|
||||||
2
.github/DISCUSSION_TEMPLATE/q-a.yml
vendored
2
.github/DISCUSSION_TEMPLATE/q-a.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# Copied from heavy-duty/ceremony@0.1.0 .github/DISCUSSION_TEMPLATE/q-a.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
# Copied from heavy-duty/ceremony@0.3.0 .github/DISCUSSION_TEMPLATE/q-a.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
||||||
# Light on purpose — same reasoning as ideas.yml (heavy-duty/ceremony#24, decision 4):
|
# Light on purpose — same reasoning as ideas.yml (heavy-duty/ceremony#24, decision 4):
|
||||||
# optional prompts only, delete before hardening.
|
# optional prompts only, delete before hardening.
|
||||||
body:
|
body:
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# Copied from heavy-duty/ceremony@0.1.0 .github/ISSUE_TEMPLATE/config.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
# Copied from heavy-duty/ceremony@0.3.0 .github/ISSUE_TEMPLATE/config.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
||||||
# The new-issue interception (heavy-duty/ceremony#24, decision 1): interception over
|
# The new-issue interception (heavy-duty/ceremony#24, decision 1): interception over
|
||||||
# instruction — prose alone has already proven insufficient everywhere else
|
# instruction — prose alone has already proven insufficient everywhere else
|
||||||
# in this org. With blank issues disabled and exactly one form, a "New
|
# in this org. With blank issues disabled and exactly one form, a "New
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/work-order.yml
vendored
2
.github/ISSUE_TEMPLATE/work-order.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# Copied from heavy-duty/ceremony@0.1.0 .github/ISSUE_TEMPLATE/work-order.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
# Copied from heavy-duty/ceremony@0.3.0 .github/ISSUE_TEMPLATE/work-order.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
||||||
# The issue contract (.ceremony/TRIAGE.md "The issue contract") as a form, so every
|
# The issue contract (.ceremony/TRIAGE.md "The issue contract") as a form, so every
|
||||||
# minted issue lands with the same bones and a later parser (heavy-duty/ceremony#18) can key on
|
# minted issue lands with the same bones and a later parser (heavy-duty/ceremony#18) can key on
|
||||||
# stable section headings. Consistency, not a gate: the form applies no
|
# stable section headings. Consistency, not a gate: the form applies no
|
||||||
|
|
|
||||||
1
.github/labels.conf
vendored
1
.github/labels.conf
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
panel=claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl kimi-bot-andresmgsl
|
panel=claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl kimi-bot-andresmgsl
|
||||||
|
triage-actors=dan-claude-bot
|
||||||
scope:bootstrap|C5DEF5|bootstrap — hardening a pristine server into a node
|
scope:bootstrap|C5DEF5|bootstrap — hardening a pristine server into a node
|
||||||
scope:users|C5DEF5|users-* — class model, apply/status, close-root
|
scope:users|C5DEF5|users-* — class model, apply/status, close-root
|
||||||
scope:runner|C5DEF5|runner-* — GitHub runner lifecycle
|
scope:runner|C5DEF5|runner-* — GitHub runner lifecycle
|
||||||
|
|
|
||||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- Copied from heavy-duty/ceremony@0.1.0 .github/pull_request_template.md (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
<!-- Copied from heavy-duty/ceremony@0.3.0 .github/pull_request_template.md (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
|
||||||
Short on purpose: a template longer than a small PR gets deleted by
|
Short on purpose: a template longer than a small PR gets deleted by
|
||||||
reflex, not read (heavy-duty/ceremony#24). One issue, one PR — draft while building. -->
|
reflex, not read (heavy-duty/ceremony#24). One issue, one PR — draft while building. -->
|
||||||
|
|
||||||
|
|
@ -12,8 +12,8 @@ to the issue for triage to amend, not silently unshipped. -->
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
- [ ] One line under `## Unreleased` — inserted **above** the heading below
|
- [ ] One `changelog.d/<issue>.md` fragment containing the exact publishable
|
||||||
it, never over it — or no behavior change, stated here.
|
prose — or no behavior change, stated here.
|
||||||
|
|
||||||
## Round log
|
## Round log
|
||||||
|
|
||||||
|
|
|
||||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
|
@ -9,7 +9,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# changelog-monotonic compares HEAD against the merge base; a
|
# changelog-monotonic and changelog-assembled compare HEAD against
|
||||||
|
# the merge base; a
|
||||||
# checkout that cannot resolve it is a hard failure in CI, not
|
# checkout that cannot resolve it is a hard failure in CI, not
|
||||||
# a skip (a guard that can quietly stop guarding is the failure
|
# a skip (a guard that can quietly stop guarding is the failure
|
||||||
# shape these checks exist to refuse).
|
# shape these checks exist to refuse).
|
||||||
|
|
@ -46,23 +47,29 @@ jobs:
|
||||||
run: bash test/drill.sh
|
run: bash test/drill.sh
|
||||||
# The release guards, doctrine in heavy-duty/ceremony's README (#13's
|
# The release guards, doctrine in heavy-duty/ceremony's README (#13's
|
||||||
# conversion). Each one's war story — why it exists, what it refuses —
|
# conversion). Each one's war story — why it exists, what it refuses —
|
||||||
# lives with its implementation upstream; the four pins below and the
|
# lives with its implementation upstream; the six pins below and the
|
||||||
# two workflow callers must always name the same ceremony tag.
|
# two workflow callers must always name the same ceremony tag.
|
||||||
#
|
#
|
||||||
# changelog-armed: the version-keyed arming rule (rig#66; the
|
# changelog-armed: the version-keyed arming rule (rig#66; the
|
||||||
# unconditional form rig#44 reverted — this is its correct return).
|
# unconditional form rig#44 reverted — this is its correct return).
|
||||||
- uses: heavy-duty/ceremony/actions/changelog-armed@0.1.0
|
- uses: heavy-duty/ceremony/actions/changelog-armed@0.3.0
|
||||||
# changelog-monotonic: no shipped heading deleted or duplicated
|
# changelog-monotonic: no shipped heading deleted or duplicated
|
||||||
# (#98, box#122). Strict by default: an unresolvable base ref is red,
|
# (#98, box#122). Strict by default: an unresolvable base ref is red,
|
||||||
# never a quiet skip — hence the fetch-depth: 0 above.
|
# never a quiet skip — hence the fetch-depth: 0 above.
|
||||||
- uses: heavy-duty/ceremony/actions/changelog-monotonic@0.1.0
|
- uses: heavy-duty/ceremony/actions/changelog-monotonic@0.3.0
|
||||||
|
# changelog-assembled: a release's stamped section must exactly match
|
||||||
|
# the fragments it consumed. Vacuous on non-release PRs.
|
||||||
|
- uses: heavy-duty/ceremony/actions/changelog-assembled@0.3.0
|
||||||
# drill-recorded: a release version carries drills/<version>.md
|
# drill-recorded: a release version carries drills/<version>.md
|
||||||
# (rig's drill meaning: drills/README.md). Vacuous on -dev trees.
|
# (rig's drill meaning: drills/README.md). Vacuous on -dev trees.
|
||||||
- uses: heavy-duty/ceremony/actions/drill-recorded@0.1.0
|
- uses: heavy-duty/ceremony/actions/drill-recorded@0.3.0
|
||||||
|
# runner-isolated: PR-triggered workflows never execute unreviewed
|
||||||
|
# branch code on a self-hosted runner.
|
||||||
|
- uses: heavy-duty/ceremony/actions/runner-isolated@0.3.0
|
||||||
# docs-sync: the .ceremony/ doctrine mirror is byte-identical to the
|
# docs-sync: the .ceremony/ doctrine mirror is byte-identical to the
|
||||||
# pin read from release.yml (ceremony#19) — a hand edit or a
|
# pin read from release.yml (ceremony#19) — a hand edit or a
|
||||||
# half-done pin bump goes red here.
|
# half-done pin bump goes red here.
|
||||||
- uses: heavy-duty/ceremony/actions/docs-sync@0.1.0
|
- uses: heavy-duty/ceremony/actions/docs-sync@0.3.0
|
||||||
|
|
||||||
# The install LIFECYCLE against a tree install.sh itself produced — the four
|
# The install LIFECYCLE against a tree install.sh itself produced — the four
|
||||||
# beats box and cast already run in CI (#106): install from this checkout,
|
# beats box and cast already run in CI (#106): install from this checkout,
|
||||||
|
|
|
||||||
8
.github/workflows/labels.yml
vendored
8
.github/workflows/labels.yml
vendored
|
|
@ -8,11 +8,15 @@ on:
|
||||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
||||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]
|
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
checks: read
|
||||||
|
statuses: read
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
jobs:
|
jobs:
|
||||||
labels:
|
labels:
|
||||||
uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.1.0
|
uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.3.0
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -17,6 +17,6 @@ permissions:
|
||||||
issues: write # --label on that fallback PR rides the issues API
|
issues: write # --label on that fallback PR rides the issues API
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
uses: heavy-duty/ceremony/.github/workflows/release.yml@0.1.0
|
uses: heavy-duty/ceremony/.github/workflows/release.yml@0.3.0
|
||||||
with:
|
with:
|
||||||
version-source: file
|
version-source: file
|
||||||
|
|
|
||||||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -4,30 +4,6 @@ History before 0.1.0 lives in git — rig grew its version surface (`VERSION`,
|
||||||
`rig --version`, the side-by-side `versions/<v>` install layout; #35/#36)
|
`rig --version`, the side-by-side `versions/<v>` install layout; #35/#36)
|
||||||
on the way to cutting its first release, and this file starts there.
|
on the way to cutting its first release, and this file starts there.
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `drill/drill.sh` — the drill has an instrument: pinned-ref assertion, a mechanical idempotence diff, and a `drills/<version>.md` record emitter (#105)
|
|
||||||
- GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123)
|
|
||||||
- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95)
|
|
||||||
- Platform, drill, docs and labels changes receive dedicated scope labels (#119)
|
|
||||||
- CI drills the install lifecycle against a real tree — install from the checkout, converge to an empty diff, uninstall to proven absence (#106)
|
|
||||||
- `rig bootstrap --undo` removes only a tailnet join rig can prove it made (#63)
|
|
||||||
- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109)
|
|
||||||
- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13)
|
|
||||||
- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `bootstrap --host yes` installs a pinned box release instead of `main` (#103)
|
|
||||||
- Release and labels machinery is consumed from heavy-duty/ceremony@0.1.0 by reference — the workflows shrink to caller stubs, the guard scripts and their tests move upstream (#112, ceremony#13)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- The drill's docs no longer claim both installers default to `main` — box installs the `BOX_RELEASE` pin, rig the latest release, and its `--box-ref` example is now a tag (#133)
|
|
||||||
- `kimi-bot-andresmgsl` is on the review panel — the roster predated it joining the bench (#120)
|
|
||||||
|
|
||||||
## 0.3.0 — 2026-07-21
|
## 0.3.0 — 2026-07-21
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,18 @@ genuinely rig's.
|
||||||
`bash test/release.sh` locally mirror what CI runs; the db dump/restore
|
`bash test/release.sh` locally mirror what CI runs; the db dump/restore
|
||||||
round-trip (`test/db-integration.sh`) executes in CI where Docker is
|
round-trip (`test/db-integration.sh`) executes in CI where Docker is
|
||||||
present. The release guards (`changelog-armed`, `changelog-monotonic`,
|
present. The release guards (`changelog-armed`, `changelog-monotonic`,
|
||||||
`drill-recorded`, `docs-sync`) run as ceremony's pinned actions.
|
`changelog-assembled`, `drill-recorded`, `runner-isolated`, `docs-sync`)
|
||||||
4. **Feature PRs land their changelog entry as part of the PR**: add it
|
run as ceremony's pinned actions.
|
||||||
under `CHANGELOG.md`'s `## Unreleased` heading — that section becomes
|
4. **Feature PRs land their changelog entry as part of the PR**: write
|
||||||
the release notes verbatim when a release is cut.
|
`changelog.d/<issue>.md` — the release PR assembles those fragments into
|
||||||
|
the release notes verbatim.
|
||||||
|
|
||||||
## Changelog entries
|
## Changelog entries
|
||||||
|
|
||||||
Every PR that changes behaviour adds one line to `## Unreleased`. One line is
|
Every PR that changes behaviour writes one `changelog.d/<issue>.md` fragment.
|
||||||
the whole rule — if it wraps more than twice in your editor, cut it down.
|
The fragment keeps the relevant `### Added` / `### Changed` / `### Fixed`
|
||||||
|
heading above its entry. One line is the whole rule — if it wraps more than
|
||||||
|
twice in your editor, cut it down.
|
||||||
|
|
||||||
- **Say what changed, and stop.** Why it was wrong, how it was found, what it
|
- **Say what changed, and stop.** Why it was wrong, how it was found, what it
|
||||||
cost, what it implies — that belongs in the PR body and the commit message,
|
cost, what it implies — that belongs in the PR body and the commit message,
|
||||||
|
|
|
||||||
3
changelog.d/103.md
Normal file
3
changelog.d/103.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `bootstrap --host yes` installs a pinned box release instead of `main` (#103)
|
||||||
3
changelog.d/105.md
Normal file
3
changelog.d/105.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `drill/drill.sh` — the drill has an instrument: pinned-ref assertion, a mechanical idempotence diff, and a `drills/<version>.md` record emitter (#105)
|
||||||
3
changelog.d/106.md
Normal file
3
changelog.d/106.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- CI drills the install lifecycle against a real tree — install from the checkout, converge to an empty diff, uninstall to proven absence (#106)
|
||||||
3
changelog.d/109.md
Normal file
3
changelog.d/109.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109)
|
||||||
8
changelog.d/112.md
Normal file
8
changelog.d/112.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13)
|
||||||
|
- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Release and labels machinery is consumed from heavy-duty/ceremony@0.1.0 by reference — the workflows shrink to caller stubs, the guard scripts and their tests move upstream (#112, ceremony#13)
|
||||||
3
changelog.d/119.md
Normal file
3
changelog.d/119.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Platform, drill, docs and labels changes receive dedicated scope labels (#119)
|
||||||
3
changelog.d/120.md
Normal file
3
changelog.d/120.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `kimi-bot-andresmgsl` is on the review panel — the roster predated it joining the bench (#120)
|
||||||
3
changelog.d/123.md
Normal file
3
changelog.d/123.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123)
|
||||||
3
changelog.d/133.md
Normal file
3
changelog.d/133.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The drill's docs no longer claim both installers default to `main` — box installs the `BOX_RELEASE` pin, rig the latest release, and its `--box-ref` example is now a tag (#133)
|
||||||
3
changelog.d/136.md
Normal file
3
changelog.d/136.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changelog entries land in per-issue fragments assembled by the release PR (#136)
|
||||||
3
changelog.d/63.md
Normal file
3
changelog.d/63.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `rig bootstrap --undo` removes only a tailnet join rig can prove it made (#63)
|
||||||
3
changelog.d/95.md
Normal file
3
changelog.d/95.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95)
|
||||||
10
changelog.d/README.md
Normal file
10
changelog.d/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# changelog.d/ — the next release's section, one fragment per issue
|
||||||
|
|
||||||
|
Machine-assembled by `bin/changelog-assemble` (#112): every PR that changes
|
||||||
|
behavior writes one file here — `<issue>.md`, the exact prose that will be
|
||||||
|
published, nothing else — and the release PR folds them all into the next
|
||||||
|
`## X.Y.Z — DATE` section of `CHANGELOG.md`, consuming them. Distinct
|
||||||
|
filenames never conflict, which is this directory's whole reason to exist.
|
||||||
|
This README is the marker that keeps the directory tracked when it holds no
|
||||||
|
fragments (#112 D1) — `changelog-armed` refuses a tree without it; do not
|
||||||
|
delete it.
|
||||||
Loading…
Reference in a new issue