Compare commits
No commits in common. "84bb1a424dfe467358e99d4c6ab4e76ac89cc328" and "d9c5b92dd1616fc66bf0c6c2dbc14fefc6b0001d" have entirely different histories.
84bb1a424d
...
d9c5b92dd1
21 changed files with 280 additions and 682 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
|
||||
|
||||
<!-- The engine appends each whole-round reply here, newest last: what
|
||||
changed and what was verified. Builders write the reply, not this section. -->
|
||||
<!-- Append each round's summary here: what changed, what was verified.
|
||||
Rounds are answered whole — one reply covering every point. -->
|
||||
|
|
|
|||
15
.github/workflows/labels.yml
vendored
15
.github/workflows/labels.yml
vendored
|
|
@ -10,14 +10,11 @@ name: labels
|
|||
# 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
|
||||
# workflow directly — which is why the caller's cron is load-bearing, not a
|
||||
# safety net (#199 relaxed it from */15 to hourly, but did NOT drop it). The
|
||||
# cron is the sweep's only discovery path for every transition no subscribed
|
||||
# event carries: a verdict landing, blocker:ci-red set/cleared, a
|
||||
# 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.
|
||||
# workflow directly — and the */15 cron is advisory: GitHub deprioritises
|
||||
# short intervals hard enough that a quiet repo goes hours between ticks. The
|
||||
# handoff wakes the sweep itself: the author sets state:needs-human, and the
|
||||
# caller's `labeled` event confirms or corrects that optimistic write within
|
||||
# seconds. The cron stays as the last resort for a forgotten handoff.
|
||||
#
|
||||
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
|
||||
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a
|
||||
|
|
@ -28,7 +25,7 @@ on:
|
|||
env:
|
||||
# A called workflow arrives without its repository. Keep this literal pin
|
||||
# aligned with the ceremony release consumed by callers (issue #9 D3).
|
||||
CEREMONY_SELF_REF: "0.4.0"
|
||||
CEREMONY_SELF_REF: "0.3.0"
|
||||
|
||||
jobs:
|
||||
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
|
||||
# shared logic arrives as script files via checkout, not as inner `uses:`
|
||||
# references.
|
||||
CEREMONY_SELF_REF: "0.4.0"
|
||||
CEREMONY_SELF_REF: "0.3.0"
|
||||
VERSION_SOURCE: ${{ inputs.version-source }}
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
35
.github/workflows/self-labels.yml
vendored
35
.github/workflows/self-labels.yml
vendored
|
|
@ -5,40 +5,11 @@ name: labels
|
|||
# unpinned — correct only for the repo that IS the source). Consumers write:
|
||||
# uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
||||
on:
|
||||
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||
# 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.
|
||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
||||
issues:
|
||||
types: [opened, closed, edited, reopened]
|
||||
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
|
||||
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
|
||||
# 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
|
||||
|
|
|
|||
125
BUILDER.md
125
BUILDER.md
|
|
@ -11,27 +11,6 @@ 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
|
||||
unclaimed child. Between epics and strays, prefer the issue that unblocks
|
||||
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
|
||||
writing or revising a deliverable — finish or release that work before
|
||||
starting new work. The rule counts build work in flight, not claims: a
|
||||
|
|
@ -41,16 +20,9 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
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 owed re-requests posted —
|
||||
by head, not by verdict: every panelist after a push, the
|
||||
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;
|
||||
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:*`
|
||||
|
|
@ -81,8 +53,7 @@ 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
|
||||
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 (a red head is your own work, above; a pending
|
||||
one resolves without you), or waiting for a good moment. An issue you have
|
||||
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).
|
||||
|
|
@ -107,12 +78,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`
|
||||
([#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 factual handoff
|
||||
comment plus the `state:needs-human` write *is* its declaration — both
|
||||
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 comment beside it. No second comment is owed on
|
||||
the issue. Every other shape still declares as above.
|
||||
`labeled` event with the summary beside it. No second comment is owed
|
||||
on the issue. Every other shape still declares as above.
|
||||
Declared once, the declaration **stands** until the park's facts change:
|
||||
a resumption that finds nothing changed posts nothing — the standing
|
||||
declaration is the record, and silence while parked is compliant, not
|
||||
|
|
@ -208,9 +179,9 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
|
||||
## The review round
|
||||
|
||||
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo:
|
||||
repo-specific facts such as the panel roster live in that repo's own
|
||||
CONTRIBUTING; the shared flow lives here and is not restated there.)
|
||||
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo: the
|
||||
panel roster and any repo-specific flow notes live in that repo's own
|
||||
CONTRIBUTING; everything below is the shared flow.)
|
||||
|
||||
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
|
||||
|
|
@ -223,56 +194,9 @@ CONTRIBUTING; the shared flow lives here and is not restated there.)
|
|||
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.
|
||||
**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
|
||||
covering every point and stating what changed and what was verified.
|
||||
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
|
||||
covering every point, then push the fixes, then re-request exactly the
|
||||
reviewers who did not approve. Prefer verification over argument: when a
|
||||
reviewer doubts behavior, add the test that settles it.
|
||||
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
|
||||
|
|
@ -339,19 +263,14 @@ item to its flow in the same comment ([LABELS.md](LABELS.md)).
|
|||
|
||||
When the round passes — every panel verdict approves the **current head**,
|
||||
and no `blocker:*` stands (conflicts rebased, CI green, drill recorded if
|
||||
this is a release PR) — the engine performs these mechanical steps on the
|
||||
builder's behalf, in order:
|
||||
this is a release PR) — hand it to the human, in order:
|
||||
|
||||
1. request the human's review;
|
||||
2. set `state:needs-human`;
|
||||
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**.
|
||||
1. post the round summary (what changed per round, what was verified);
|
||||
2. request the human's review;
|
||||
3. set `state:needs-human` yourself.
|
||||
|
||||
The builder composes no new summary at handoff: the authored record already
|
||||
lives in the Round log, mirrored mechanically from each whole-round reply as
|
||||
specified above. 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 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.
|
||||
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
|
||||
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,102 +9,6 @@ Entries arrive as fragments — one `changelog.d/<issue>.md` per PR, never
|
|||
an edit to this file — and the release PR assembles them into the next
|
||||
section here (`bin/changelog-assemble`, #112).
|
||||
|
||||
## 0.4.0 — 2026-07-29
|
||||
|
||||
### Added
|
||||
|
||||
- `changelog.d/shape` — an optional one-line sentinel, `flat` or `grouped`,
|
||||
that pins the fragment set's shape and outranks the newest-published-section
|
||||
inference; absent, the inference binds unchanged (#182).
|
||||
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.
|
||||
- `changelog_fragment_problem` bounds every entry at 300 normalized
|
||||
characters, red on the PR that writes the fragment; the armed guard and
|
||||
the assembler inherit the one definition (#167).
|
||||
- BUILDER.md and CHANGELOG.md state the bound and the split rule: a long
|
||||
change ships several short entries, never one long one (#167).
|
||||
|
||||
### Changed
|
||||
|
||||
- Labels automation docs now make sweep cadence a consumer-owned tradeoff,
|
||||
retain hourly as the engine-less default, and document manual dispatch as
|
||||
the operator's immediate full-board sweep (#203).
|
||||
- `labels` — the reconcile cron relaxes from `*/15` to hourly (#199), cutting a
|
||||
private consumer's schedule-triggered full-board sweeps ~4× at GitHub's
|
||||
1-minute billing floor.
|
||||
- `labels` — the hourly cron is the sweep's only wake for transitions no
|
||||
subscribed event carries — a verdict landing, blocker:ci-red, a
|
||||
blocker:conflict when another PR merges, the time-based stale/reclaim — so it
|
||||
bounds their latency to ≤1h, delaying no event-carried transition (#199).
|
||||
- `labels` — the caller's `issues:` trigger narrows to
|
||||
`[opened, closed, edited, reopened]` (#199), the actions that carry a
|
||||
queue-state change the cron cannot wait a cadence for. The churn/validation
|
||||
actions — labeled/unlabeled/assigned/unassigned — come off; the PR handoff
|
||||
wake is unaffected.
|
||||
- `labels` — each caller trigger now carries a comment saying why it is
|
||||
subscribed, and reconcile keeps `cancel-in-progress: false` (#199) —
|
||||
cancelling a sweep mid-board is the race that guard exists to prevent.
|
||||
- `CONTRIBUTING.md` now points to `BUILDER.md` for the shared PR flow instead
|
||||
of restating doctrine that can drift, while retaining ceremony's roster and
|
||||
other repo-specific facts (#198).
|
||||
- Builder doctrine makes each whole-round reply the durable Round log record
|
||||
mirrored by the engine, leaving handoff as a mechanical facts-only step
|
||||
instead of a newly composed summary (#196).
|
||||
- `FLEET.md` removes its duplicate bench roster, records crew as a general
|
||||
operator-configured tool, and advances its whole-file audit stamp to
|
||||
`crew@eaeb302` with every surviving crew link re-pinned (#193).
|
||||
- `FLEET.md` keeps the registry's authorization rule and its crew#16/crew#66
|
||||
provenance, while replacing duplicated mechanism and path claims with a
|
||||
pinned pointer to crew's registry header (#192).
|
||||
- `BUILDER.md` gates both review-request points on a green check at the
|
||||
head, carries crew#45's argued exception for failures outside the PR,
|
||||
and states the ruled classification: cancelled and stale are not a
|
||||
green head; skipped and neutral are (#189).
|
||||
- `BUILDER.md` documents CI-red recovery in pickup precedence: a red head
|
||||
of your own PR is picked up before claiming another issue, is never a
|
||||
parked claim, and follows crew#17's recovery path (#189).
|
||||
- `FLEET.md` writes the ci-red wake into the duty order between resume
|
||||
and build, now as deployed engine rather than on paper: the
|
||||
reconciliation stamp advances to the crew SHA carrying crew#64 (#189).
|
||||
- `FLEET.md` describes the build wake's check gate as the engine
|
||||
implements it: a green head, or one with no checks configured, opens a
|
||||
round; a red head and an unfinished one are held and reported
|
||||
separately (#189).
|
||||
- `FLEET.md` corrects the attention wake to the crew#66 ruling: the query
|
||||
is cross-repo, the action is registry-bounded, and an out-of-scope
|
||||
demand is reported and escalated to the operator rather than worked. It
|
||||
no longer claims attention is exempt from the registry (#189).
|
||||
- `FLEET.md` distinguishes an attention session that dies before acking,
|
||||
which relaunches, from one that completes without acking, which is a
|
||||
decline a ledger keeps from re-firing (#189).
|
||||
- `BUILDER.md` re-requests by head, not by verdict: a push while
|
||||
answering a round stales every approval, so every panelist is
|
||||
re-requested; only an unchanged head re-requests the non-approvers
|
||||
alone (#190).
|
||||
- FLEET.md's duty-loop mechanism is a pointer to crew's shared engine; the
|
||||
wake lists follow the engine's duty order, the roster keeps the as-built
|
||||
bench beside `fleet.roster`'s target, and the reconciliation stamp names
|
||||
crew@`01fb49c` (#187).
|
||||
- Ceremony's changelog is grouped from this release forward: the pending
|
||||
fragments carry `### ` headings under a `grouped` sentinel (#182).
|
||||
- BUILDER.md: a park declaration stands until its facts change — a
|
||||
nothing-changed resumption posts nothing; only a no-open-PR park owes a
|
||||
refresh, inside the 48-hour reclaim window (#178).
|
||||
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).
|
||||
|
||||
### Fixed
|
||||
|
||||
- FLEET.md no longer says a review request outside the registry is
|
||||
authorization: `repos.txt` is the scope for the review queue, out-of-scope
|
||||
requests are logged and never acted on, and the attention wake is stated
|
||||
as the one registry-independent exception, by design (#187).
|
||||
- `blocked_reference_records` unions every `Blocked by` clause in the body
|
||||
instead of binding to the first marker occurrence — a repeated declaration
|
||||
no longer promotes on its first sentence alone, and earlier prose that
|
||||
merely mentions being blocked no longer hijacks the parse (#184).
|
||||
- `decide_state()` refuses `state:needs-human` while the hand-set `blocked`
|
||||
label stands — the PR falls to `state:addressing`, exactly parallel to the
|
||||
`needs-ruling` exclusion; never emitted by `blockers()` (#180).
|
||||
|
||||
## 0.3.0 — 2026-07-24
|
||||
|
||||
- Make `changelog-armed` reject fragment shape drift on the PR that introduces it.
|
||||
|
|
|
|||
|
|
@ -35,9 +35,43 @@ Who may set which label is [LABELS.md](LABELS.md)'s contract.
|
|||
|
||||
## The PR flow
|
||||
|
||||
PRs move through review rounds that builders answer whole, and only a human
|
||||
merges. [BUILDER.md](BUILDER.md) is the shared flow contract; this file names
|
||||
only ceremony-specific facts such as the roster and code conventions.
|
||||
The same flow the sibling repos run, and the part of this pipeline that is
|
||||
already proven:
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
312
FLEET.md
312
FLEET.md
|
|
@ -1,67 +1,54 @@
|
|||
# FLEET.md — the fleet shape, and how it actually runs
|
||||
# FLEET.md — the roster, and how it actually runs
|
||||
|
||||
> **Status:** descriptive snapshot, not doctrine. This file records how the
|
||||
> heavy-duty operator fleet is wired *today*. It is **not** part of the
|
||||
> vendored doctrine set (`.ceremony/`) and is never mirrored to consumer
|
||||
> repos. [Crew](https://github.com/heavy-duty/crew) is a general tool: its
|
||||
> repository ships the engine, while the fleet definition belongs to the
|
||||
> operator; heavy-duty is one operator of it. Membership, repository scope,
|
||||
> agent-profile overrides and doctrine paths belong to that definition;
|
||||
> membership itself lives outside every checkout. Crew's shipped defaults
|
||||
> name heavy-duty's AGENTS.md, TRIAGE.md, BUILDER.md and REVIEWER.md, but
|
||||
> those are compatibility defaults, not vocabulary compiled into the engine
|
||||
> — operator `doctrine.conf` values can replace them.
|
||||
> 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.
|
||||
> agent fleet that builds this repo is wired *today*, so the setup can later be
|
||||
> solidified into a replicable fleet-management solution. It is **not** part of
|
||||
> the vendored doctrine set (`.ceremony/`) and is never mirrored to consumer
|
||||
> repos. The doctrine files (AGENTS.md, TRIAGE.md, BUILDER.md, REVIEWER.md,
|
||||
> LABELS.md, CONTRIBUTING.md) say what roles *must* do; this file says how the
|
||||
> current bench *physically* does it. Last reconciled against the deployed
|
||||
> duty scripts at
|
||||
> [`heavy-duty/crew@b2fd864`](https://github.com/heavy-duty/crew/tree/b2fd8642e7f7aa8dc9de6b44edadbe1dc557b140)
|
||||
> (private to the org; the fleet can read it), 2026-07-24 — a descriptive
|
||||
> file with no reconciliation stamp gives the next reader nothing to diff,
|
||||
> which is exactly how the #149 drift went unnoticed.
|
||||
|
||||
## Fleet shape
|
||||
## The roster
|
||||
|
||||
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
|
||||
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.
|
||||
network path — GitHub is the only queue.
|
||||
|
||||
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
|
||||
| Identity | Box | CLI | Roles |
|
||||
|---|---|---|---|
|
||||
| `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
|
||||
holds the triage role, not write), not as convention.
|
||||
|
||||
## Anatomy of a duty loop
|
||||
|
||||
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.
|
||||
Every box runs the same skeleton, adapted to its CLI:
|
||||
|
||||
What belongs here is what a wake *means*:
|
||||
|
||||
- **The registry is the scope.** A box acts only on repos its operator
|
||||
listed. Work it finds outside that scope is reported and never acted on;
|
||||
the report is part of the boundary, because a bounded wake that goes quiet
|
||||
is indistinguishable from a broken one. Adding a repo is an **operator
|
||||
decision**, never something a sweep makes by writing where nobody listed.
|
||||
The 2026-07-25 scope ruling (crew#16) closed the org-wide review and
|
||||
author-side write surface; the crew#66 attention ruling closed the last
|
||||
exemption. Crew's
|
||||
[`examples/repos.txt` header](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/examples/repos.txt)
|
||||
is the pinned source for how that rule is implemented and reported.
|
||||
- **Tick:** cron `*/5` runs `~/duty/duty.sh` under a non-blocking `flock`; the
|
||||
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
|
||||
this identity.
|
||||
- **Poll:** the script reads `~/duty/repos.txt` and queries GitHub with `gh`
|
||||
for work matching the box's role. Whose registry that file is depends on
|
||||
the role: the triage box's `repos.txt` **is** its registry — adding a repo
|
||||
is adding a line — while a reviewer's registry is the org itself, and its
|
||||
`repos.txt` is a backstop that cannot scope it (grok's copy says so in its
|
||||
own first line).
|
||||
- **Act:** when there is work, the script launches the box's CLI as a one-shot
|
||||
session with a role prompt; the session does the work via `gh` as the box's
|
||||
own identity, then exits. Sessions are stateless and disposable — all state
|
||||
lives on the board (issues, PRs, labels) and in git branches.
|
||||
|
||||
### Wake conditions
|
||||
|
||||
|
|
@ -75,27 +62,9 @@ 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
|
||||
the authenticated-user endpoint —
|
||||
`gh api "/issues?filter=assigned&state=open&labels=attention"` — one call, no
|
||||
search index (the review queue below already records that the index lags) —
|
||||
and it **sees** repos outside the operator's registry, because that endpoint
|
||||
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.
|
||||
search index (the reviewer trigger below already records that the index
|
||||
lags), and like the review-request trigger it reaches repos `~/duty/repos.txt`
|
||||
does not name.
|
||||
|
||||
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
|
||||
|
|
@ -103,13 +72,9 @@ plus removing the label — [the `attention`
|
|||
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.
|
||||
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
|
||||
simply relaunched at the next tick — the same crash-only shape as resume
|
||||
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.
|
||||
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,
|
||||
and it is the same crash-only shape as resume below.
|
||||
|
||||
The design this replaces was built and rejected: polling notifications for
|
||||
`reason: mention` re-arms a thread on every comment, so ordinary round
|
||||
|
|
@ -120,91 +85,47 @@ wake: [#16's 16:49Z
|
|||
ruling](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198)
|
||||
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
|
||||
never saw the comment, and the eventual pickup ran on a manual bridge. The
|
||||
wake is no longer on paper: `duty-attention.sh` is deployed engine, and
|
||||
`duty.sh` runs it first on every box, whatever its roles.
|
||||
never saw the comment, and the eventual pickup ran on a manual bridge. Like
|
||||
the notifier's queue below, this wake is the spec for a box-side change only
|
||||
the operator can make; until `duty.sh` polls it, the wake exists on paper —
|
||||
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.
|
||||
|
||||
The engine's duty order is fleet-standard
|
||||
([`bin/duty.sh`](https://github.com/heavy-duty/crew/blob/eaeb3022aa47d90e797f2b9e007b831df7ca8406/shared/bin/duty.sh)):
|
||||
**attention → triage signals → review queue → resume → ci-red → build →
|
||||
handoff → rebase → worktree hygiene → backlog hygiene (hourly)** — attention
|
||||
role-independent and first, then each duty family the box's roles enable.
|
||||
Every position in that order is deployed engine at the stamped SHA:
|
||||
[crew#64](https://github.com/heavy-duty/crew/pull/64) merged ci-red between
|
||||
resume and build, and `duty.sh`'s own header carries the same order.
|
||||
The earlier form of this file folded handoff and rebase into the other
|
||||
builder wakes; they are duties of their own.
|
||||
|
||||
- **Triage signals**, per registry repo: `needs-triage` issues,
|
||||
queue-unlabeled strays, discussions without triage's voice, unread
|
||||
`@`-mentions (their own session), and `blocked` issues whose named blockers
|
||||
have all landed — a lead the session verifies, never a label the engine
|
||||
flips. Backlog hygiene (stale claims, label invariants) runs hourly,
|
||||
self-scheduled inside the duty tick. A `needs-ruling` standing **past
|
||||
24h** is still triage's to pick up — the ladder's last rung makes the
|
||||
option triage's to choose — but, like the notifier queue below, that
|
||||
detection row is on paper only today.
|
||||
- **Review queue**: one candidate set, enumerated from the pulls pages of
|
||||
every registry repo — object endpoints, never the search index for the
|
||||
queue itself, whose lag left cast#143, incubator#25 and box#164 sitting
|
||||
unreviewed — filtered to PRs listing me in `requested_reviewers`, deduped
|
||||
by (repo, PR) before acting (the sequential shape double-announced on
|
||||
ceremony#32), and worked oldest-first. One search-backed **awareness pass**
|
||||
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.
|
||||
- **Triage:** new discussions to mint from, builder questions on issues, stray
|
||||
issues to reconcile, `@`-mentions, hourly hygiene (stale claims, label
|
||||
invariants), and a `needs-ruling` standing **past 24h** — the ladder's last
|
||||
rung makes the option triage's to pick, and this wake list is where triage
|
||||
learns such an item exists (see the notifier section below).
|
||||
- **Builders**, in priority order: **resume** (an open draft PR of mine, or a
|
||||
claimed issue with my `build/*` branch but no PR — possible only if a
|
||||
previous session died mid-work), a `ready` issue to claim, a completed
|
||||
review round on my PR (act on whole rounds, never single verdicts), my PR
|
||||
fully approved (write the closing summary, flip to `state:needs-human`,
|
||||
request the human), my PR `CONFLICTING` (rebase; never act on `UNKNOWN` —
|
||||
post-merge flap).
|
||||
- **Reviewers**, one candidate set from two merged sources. Source 1,
|
||||
authoritative: every open PR in the `heavy-duty` org **plus the named bot
|
||||
forks** that lists me in `requested_reviewers`, enumerated straight from
|
||||
the pulls API — never `gh search`, whose index lags (cast#143,
|
||||
incubator#25 and box#164 each sat unreviewed behind it). A review request
|
||||
is authorization, so no repo filter may gate it. Source 2, backstop: the
|
||||
`repos.txt` poll for an open PR by someone else whose head I have not yet
|
||||
reviewed — it only **adds** candidates the sweep may have missed (an
|
||||
org-enumeration failure, say) and never concludes "nothing to do". The
|
||||
sources are merged and deduplicated by (repo, PR) **before** acting, not
|
||||
run as sequential passes — the sequential shape double-announced on
|
||||
ceremony#32, when the request sweep and the repo-list poll each acted on
|
||||
the same PR in one tick (operator protocol 2026-07-23) — and worked
|
||||
oldest-first. Unchanged: one verdict per head, deduplicated against my
|
||||
own latest review's SHA.
|
||||
|
||||
#### The operator notifier — the `needs-ruling` queue
|
||||
|
||||
The operator notifier (`notify.sh`, a fleet singleton on the triage box; its
|
||||
mechanism is crew's too) watches open PRs carrying `state:needs-human`. That
|
||||
poll never reads `needs-ruling`, which lives mostly on *issues* — so an
|
||||
escalation waits invisibly on the very human it names. Not hypothetical: on
|
||||
2026-07-23 alone, three escalations spent their whole lives outside the
|
||||
operator's view — [#16's fork-PR-workflows
|
||||
The operator notifier (`notify.sh`, on the triage box) watches open PRs
|
||||
carrying `state:needs-human`. That poll never reads `needs-ruling`, which
|
||||
lives mostly on *issues* — so an escalation waits invisibly on the very human
|
||||
it names. Not hypothetical: on 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)
|
||||
(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
|
||||
|
|
@ -214,12 +135,11 @@ and [epic #50's own 13:04Z
|
|||
flag](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181),
|
||||
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
|
||||
for the engine-side update:
|
||||
for the box-side update:
|
||||
|
||||
- **The second query.** Alongside the `state:needs-human` PR poll, `notify.sh`
|
||||
polls **open issues and PRs labelled `needs-ruling`** across every repo in
|
||||
`notify-repos.txt`, which is deliberately wider than the duty registry:
|
||||
a cross-repo handoff is precisely what the operator cannot discover alone.
|
||||
`~/duty/repos.txt`.
|
||||
- **One tracked message per item, edited in place** — the same
|
||||
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
|
||||
|
|
@ -244,46 +164,54 @@ 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
|
||||
board already shows; the label itself moves only by the doctrine's hands.
|
||||
|
||||
The duty engine is crew's shared tree, one source deployed to every box.
|
||||
Specs written in this file have a record of becoming engine: the attention
|
||||
wake and the reviewers' request sweep both started here as paper (the sweep's
|
||||
org-wide form was then retired by the 2026-07-25 scope ruling), and the
|
||||
builders' ci-red wake above is the latest: written here as paper while
|
||||
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
|
||||
only label filter is `state:needs-human` — and triage's **past 24h**
|
||||
detection row above, which the triage-signals bullet already marks. Earlier
|
||||
counts here said "two" while silently excluding the second; naming them is
|
||||
cheaper than a number that has to be recounted every time a wake lands.
|
||||
`~/duty/repos.txt` and the duty scripts live inside each box and are the
|
||||
operator's to change; this descriptive edit is the spec for those box-side
|
||||
updates. The reviewers' request sweep is one such spec made real — deployed
|
||||
on all four reviewer boxes since 2026-07-23 (the Reviewers wake above). The
|
||||
notifier's `needs-ruling` queue is still on paper only: `notify.sh`'s one
|
||||
label filter today is `state:needs-human`.
|
||||
|
||||
### Resilience
|
||||
|
||||
- **Boot gate:** each tick compares the kernel boot id
|
||||
(`/proc/sys/kernel/random/boot_id`) to a stored marker. First tick after any
|
||||
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
|
||||
|
||||
- `🔎 reviewing head <sha>` — a reviewer announces work before starting, so
|
||||
liveness is visible instead of hoped for.
|
||||
- `⟲ 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.
|
||||
- `⟲ resuming from <sha>` — a builder announces recovery after interruption.
|
||||
- Claim ritual: comment on the issue + self-assign + label flip, before any
|
||||
branch exists.
|
||||
- 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
|
||||
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
|
||||
|
||||
This wiring proved itself on day one (seven merged PRs, unanimous three-model
|
||||
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
|
||||
configuration model, real-host rehearsal and fixture tests — so standing up
|
||||
a fleet is a bootstrap, not an archaeology dig. What remains is adoption:
|
||||
crew#85 tracks the road to a `0.1.0` another operator can use without a fork.
|
||||
Membership stays in the operator definition; this file remains the map of
|
||||
what a wake means, and crew is the map of how it runs.
|
||||
review convergence on #39, and a full-fleet crash recovery). The plan:
|
||||
|
||||
1. Once the ceremony machinery is complete and adopted, each agent will be
|
||||
asked to write a **detailed, replicable description of its own setup** —
|
||||
cron lines, duty script, prompts, probes — as durable documentation.
|
||||
2. Those five descriptions get converged into a **solidified fleet-management
|
||||
solution** (duty loops as reusable templates, likely living alongside the
|
||||
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,8 +3,7 @@
|
|||
The taxonomy shared across the heavy-duty repos. Only the `scope:` set
|
||||
differs per repo (each repo's `.github/labels.conf` names its actual
|
||||
surfaces); everything else below is core and identical everywhere, created by
|
||||
the labels workflow's dispatch (which is also the operator's manual
|
||||
full-board reconcile sweep; issue #10).
|
||||
the labels workflow's bootstrap dispatch (issue #10).
|
||||
|
||||
Two state machines share the taxonomy: the **PR machine** (proven in
|
||||
box/rig/cast, reconciled by machinery) and the **issue flow** (the
|
||||
|
|
@ -212,13 +211,10 @@ on a PR would say the same thing twice and drift.
|
|||
|
||||
## Maintenance
|
||||
|
||||
The labels workflow (issue #10) recomputes PR state statelessly on subscribed
|
||||
events plus a consumer-owned scheduled discovery sweep. Hourly is the
|
||||
recommended default when no other engine drives board state; relax it only as
|
||||
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
|
||||
The labels workflow (issue #10) recomputes PR state statelessly on PR events
|
||||
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
|
||||
manual dispatch. 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`,
|
||||
`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
|
||||
deleted at bootstrap — a `question` is a discussion, not an issue.
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.1-dev
|
||||
0.3.1-dev
|
||||
|
|
|
|||
7
changelog.d/167.md
Normal file
7
changelog.d/167.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
### 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).
|
||||
3
changelog.d/173.md
Normal file
3
changelog.d/173.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
### Changed
|
||||
|
||||
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).
|
||||
3
changelog.d/175.md
Normal file
3
changelog.d/175.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
### Added
|
||||
|
||||
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.
|
||||
5
changelog.d/178.md
Normal file
5
changelog.d/178.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
### 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).
|
||||
5
changelog.d/180.md
Normal file
5
changelog.d/180.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
### 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).
|
||||
10
changelog.d/182.md
Normal file
10
changelog.d/182.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
### 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).
|
||||
6
changelog.d/184.md
Normal file
6
changelog.d/184.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
### 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,8 +122,7 @@ the machinery at all:
|
|||
[Labels automation](#labels-automation), plus `.github/labels.conf`
|
||||
(panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the
|
||||
path→scope globs). Run `workflow_dispatch` once — **this bootstraps
|
||||
the taxonomy, `release` label included** — and use it again whenever an
|
||||
operator needs a full-board sweep immediately.
|
||||
the taxonomy, `release` label included**.
|
||||
7. **The artifact hook** (optional): `.github/actions/release-artifact/`
|
||||
per [The artifact hook](#the-artifact-hook). No hook → the source
|
||||
tarball is the package.
|
||||
|
|
@ -297,39 +296,20 @@ The complete caller is:
|
|||
```yaml
|
||||
name: labels
|
||||
on:
|
||||
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||
# 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:
|
||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
||||
pull_request_target:
|
||||
# Fork PRs; these carry the head/draft/review facts state:* derives from.
|
||||
# labeled/unlabeled are the handoff wake (state:needs-human confirmed here);
|
||||
# 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.
|
||||
# review_requested/review_request_removed are unreleased — not in 0.2.0;
|
||||
# add them with the pin bump to the first tag carrying ceremony#137. They
|
||||
# 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]
|
||||
# Available at 0.2.0 and later (the first tag carrying ceremony#32); a
|
||||
# consumer pinned to 0.1.0 omits this block.
|
||||
issues:
|
||||
# Narrowed (#199) to the actions carrying a queue-state change the hourly
|
||||
# cron cannot wait one cadence for: opened → the mint→needs-triage check,
|
||||
# closed → the 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. 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]
|
||||
# edited/reopened are unreleased — not in 0.2.0; add them with the pin
|
||||
# bump to the first tag carrying ceremony#144, the same bump as the
|
||||
# pull_request_target additions above.
|
||||
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
|
||||
permissions:
|
||||
contents: read
|
||||
checks: read # mergeability/check-rollup read for PR state
|
||||
|
|
@ -350,32 +330,23 @@ 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
|
||||
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
|
||||
refs to adopt it early. The type list has grown then narrowed across tags:
|
||||
`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.
|
||||
refs to adopt it early. At `0.2.0` the type list stops at `closed`: `edited`
|
||||
and `reopened` are newer, part of the pending stub edit below.
|
||||
|
||||
`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
|
||||
out only the consumer's base branch and the pinned ceremony implementation.
|
||||
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
|
||||
a bare pin bump. `review_requested` and `review_request_removed` on
|
||||
`pull_request_target:` shipped in `0.3.0` (ceremony#137) — the wake that
|
||||
a bare pin bump. The pending edits are `review_requested` and
|
||||
`review_request_removed` on `pull_request_target:` (#137) — the wake that
|
||||
clears `blocker:unrequested` the moment the panel is asked, without which a
|
||||
quiet repo wears that flag until the backstop cron; a consumer picks them up
|
||||
by pinning `0.3.0` or later, never through mixed refs.
|
||||
quiet repo wears that flag until the advisory cron — and `edited` and
|
||||
`reopened` on `issues:` (#144): the wakes for a body edit rewriting the
|
||||
`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
|
||||
`triage-actors` setting, and then zero or more scope rows:
|
||||
|
|
@ -404,20 +375,11 @@ Core state, blocker, work-queue, and release labels come from ceremony. Scope
|
|||
rows remain consumer-owned because paths and surfaces differ by repository.
|
||||
|
||||
After adding the caller and configuration, run `workflow_dispatch` once to
|
||||
bootstrap labels on a fresh repository. It is also the operator's general
|
||||
manual full-board sweep — the answer when the board looks wrong now rather
|
||||
than after the next scheduled cadence:
|
||||
|
||||
```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.
|
||||
bootstrap labels on a fresh repository. 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
#!/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,16 +69,15 @@ check "LABELS.md enumerates no repo's scope labels" 1 "0" \
|
|||
# same lists. review_requested/review_request_removed are the wake that
|
||||
# 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
|
||||
# listed (#137). The issues list narrowed to [opened, closed, edited, reopened]
|
||||
# (#199): each carries a queue-state change the hourly cron cannot wait one
|
||||
# cadence for — opened drives mint→needs-triage, closed the blocker-closes→ready
|
||||
# self-heal, edited a body rewrite of the `Blocked by #N` line the sweep parses,
|
||||
# reopened a closed issue re-entering the queue — while the churn/validation
|
||||
# actions (labeled/unlabeled/assigned/unassigned) came off. The stub is prose, so nothing but these rows
|
||||
# keeps the lists from drifting: a type in one file only is a wake that fires
|
||||
# at home and nowhere in the fleet, or the reverse — the drift #144 caught.
|
||||
# The NF guard keeps `issues: write` under permissions: from matching the
|
||||
# issues: trigger key.
|
||||
# listed (#137). edited/reopened are the wakes for the two events that
|
||||
# falsify issue labels silently — an edited body rewrites the `Blocked by
|
||||
# #N` declaration the reconcile sweep parses, and a reopened issue
|
||||
# re-enters the queue wearing labels derived at close; PR #32 widened the
|
||||
# caller by both and the stub never followed (#144). The stub is prose, so
|
||||
# nothing but these rows keeps the lists from drifting: a type in one file
|
||||
# only is a wake that fires at home and nowhere in the fleet, or the
|
||||
# reverse. 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
|
||||
awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
|
||||
}
|
||||
|
|
@ -89,20 +88,17 @@ types_in_sync() { # $1 = trigger key, $2 = caller, $3 = stub → 0 when both lis
|
|||
}
|
||||
CALLER="$ROOT/.github/workflows/self-labels.yml"
|
||||
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 "" \
|
||||
types_in_sync pull_request_target "$CALLER" "$STUB"
|
||||
check "the caller lists both review-request wakes" 0 "" pr_has_both_review_wakes
|
||||
# shellcheck disable=SC2016 # expansion belongs to the nested bash
|
||||
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 "" \
|
||||
types_in_sync issues "$CALLER" "$STUB"
|
||||
check "the caller lists exactly the queue-state-changing issue types" 0 \
|
||||
"types: [opened, closed, edited, reopened]" event_types "$CALLER" issues
|
||||
check "the caller still lists all eight issue types" 0 \
|
||||
"types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]" \
|
||||
event_types "$CALLER" issues
|
||||
# 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
|
||||
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"
|
||||
|
|
@ -116,13 +112,13 @@ sed 's/review_requested, review_request_removed/review_request_removed, review_r
|
|||
"$STUB" >"$mut_stub"
|
||||
check "a reorder in one list only goes red" 1 "" \
|
||||
types_in_sync pull_request_target "$CALLER" "$mut_stub"
|
||||
sed 's/, closed//' "$CALLER" >"$mut_caller"
|
||||
sed 's/, edited//' "$CALLER" >"$mut_caller"
|
||||
check "an issue type dropped from the caller goes red" 1 "" \
|
||||
types_in_sync issues "$mut_caller" "$STUB"
|
||||
sed 's/, closed//' "$STUB" >"$mut_stub"
|
||||
sed 's/, edited//' "$STUB" >"$mut_stub"
|
||||
check "an issue type dropped from the stub goes red" 1 "" \
|
||||
types_in_sync issues "$CALLER" "$mut_stub"
|
||||
sed 's/opened, closed/closed, opened/' "$STUB" >"$mut_stub"
|
||||
sed 's/closed, reopened/reopened, closed/' "$STUB" >"$mut_stub"
|
||||
check "an issue-list reorder in one file only goes red" 1 "" \
|
||||
types_in_sync issues "$CALLER" "$mut_stub"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue