From bc3c1bd5cff8ad4a432ada2ed8e538179eb010c5 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:17:39 +0000 Subject: [PATCH 1/6] feat: nudge resolved offsite claims --- CHANGELOG.md | 1 + LABELS.md | 4 +- .../issueflow-reconcile.sh | 48 +++++++++++++++++++ test/issueflow-reconcile.test.sh | 17 ++++++- 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a58430..c3ff203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ so entries say what changed, cite the issue, and stop. - `actions/runner-isolated` — a `pull_request`-triggered job may never run on a self-hosted runner (#58). - The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52). - `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68). +- `issueflow-reconcile` — nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69). ## 0.1.0 — 2026-07-22 diff --git a/LABELS.md b/LABELS.md index e5539ae..a4f38a1 100644 --- a/LABELS.md +++ b/LABELS.md @@ -107,7 +107,9 @@ 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. +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. ## Scope — which surface? (PRs and issues, any number) diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index 920e9b1..31b12c0 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -192,6 +192,27 @@ epic_decision() { # $1 refs, $2 states fi } +offsite_cross_referenced_prs() { # timeline JSON on stdin -> owner/repo#N + jq -r ' + .[] + | select(.event == "cross-referenced") + | .source.issue + | select(.pull_request != null) + | select(.repository.full_name != null and .number != null) + | "\(.repository.full_name)#\(.number)" + ' | sort -u +} + +offsite_resolved_decision() { # PR states on stdin -> NUDGE | QUIET + local states + states="$(cat)" + if [ -n "$states" ] && ! grep -Eq '^(OPEN|UNKNOWN)$' <<<"$states"; then + echo NUDGE + else + echo QUIET + fi +} + # API edge. Marker comments make warnings and nudges idempotent across sweeps. ensure_comment() { # $1 issue, $2 marker, $3 message local n="$1" marker="$2" message="$3" @@ -210,6 +231,21 @@ reference_states() { done } +offsite_pr_states() { + local ref repo number state + while IFS= read -r ref; do + [ -n "$ref" ] || continue + repo="${ref%#*}" + number="${ref##*#}" + state="$(gh api "repos/$repo/pulls/$number" --jq '.state' 2>/dev/null || echo UNKNOWN)" + case "$state" in open) echo OPEN ;; closed) echo CLOSED ;; *) echo UNKNOWN ;; esac + done +} + +offsite_timeline() { # unreadable timelines are deliberately silent + gh api --paginate "repos/$REPO/issues/$1/timeline" 2>/dev/null || return 1 +} + last_issue_activity() { local n="$1" created="$2" latest latest="$({ @@ -269,6 +305,18 @@ reconcile_issue() { fi log "#$n: stale claim reclaimed -> ready" ;; esac + if has_issue_label offsite; then + local timeline + if timeline="$(offsite_timeline "$n")"; then + refs="$(offsite_cross_referenced_prs <<<"$timeline")" + states="$(offsite_pr_states <<<"$refs")" + if [ "$(offsite_resolved_decision <<<"$states")" = NUDGE ]; then + ensure_comment "$n" offsite-resolved \ + "$(tr '\n' ' ' <<<"$refs" | sed 's/[[:space:]]*$//') is closed; this issue's \`offsite\` flag is still up. Clear it and close the issue, or say what is still outstanding. @$(jq -r '.assignees[0].login' <<<"$ISSUE_JSON")" + log "#$n: resolved offsite PRs nudged" + fi + fi + fi elif has_issue_label blocked; then refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")" diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index d7e2f06..a45cfe5 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -76,6 +76,21 @@ check "both quiet flags still produce one exemption verdict" 0 "EXEMPT" \ check "blocked does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"blocked" check "ready does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"ready" check "empty labels do not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt Date: Thu, 23 Jul 2026 13:19:11 +0000 Subject: [PATCH 2/6] test: cover offsite nudge integration --- test/issueflow-reconcile.test.sh | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index a45cfe5..52e65a5 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -208,6 +208,8 @@ issue_stub_gh() { shift done file="$TMP/$(printf '%s' "$endpoint" | tr '/' '_').json" + printf '%s\n' "$endpoint" >>"$TMP/api-calls" + [ ! -f "$file.error" ] || return 1 [ -f "$file" ] || { printf '[]\n'; return 0; } if [ -n "$jqexpr" ]; then jq -r "$jqexpr" "$file"; else cat "$file"; fi elif [ "$1" = issue ] && [ "$2" = comment ]; then @@ -289,6 +291,59 @@ check "an offsite claim with an open PR stays claimed" 1 "" \ offsite_both="$(issue_probe 28 $'claimed\noffsite\nneeds-ruling')" check "offsite plus needs-ruling stays claimed" 1 "" \ grep -q 'reclaimed' <<<"$offsite_both" + +# -- resolved offsite work nudges once and only from complete evidence ------- +jq -n --arg at "$(iso_at $((INOW - 3600)))" \ + '[{"event":"assigned","created_at":$at}, + {"event":"cross-referenced","source":{"issue":{"number":112,"repository":{"full_name":"heavy-duty/rig"},"pull_request":{"url":"x"}}}}]' \ + >"$(tfix 29)" +printf '{"state":"closed"}\n' >"$TMP/repos_heavy-duty_rig_pulls_112.json" +printf '[]\n' >"$(cfix 29)" +resolved="$(issue_probe 29 $'claimed\noffsite')" +check "a closed cross-referenced PR nudges and names the PR" 0 "" \ + grep -q 'heavy-duty/rig#112 is closed' "$TMP/posted-29" +check "the resolved nudge leaves the claim untouched" 1 "" \ + grep -q 'reclaimed' <<<"$resolved" +issue_probe 29 $'claimed\noffsite' >/dev/null +check "the resolved nudge is idempotent across sweeps" 0 "1" \ + grep -cF '' "$TMP/posted-29" + +jq -n --arg at "$(iso_at $((INOW - 3600)))" \ + '[{"event":"assigned","created_at":$at}, + {"event":"cross-referenced","source":{"issue":{"number":112,"repository":{"full_name":"heavy-duty/rig"},"pull_request":{"url":"x"}}}}, + {"event":"cross-referenced","source":{"issue":{"number":9,"repository":{"full_name":"heavy-duty/box"},"pull_request":{"url":"x"}}}}]' \ + >"$(tfix 30)" +printf '{"state":"open"}\n' >"$TMP/repos_heavy-duty_box_pulls_9.json" +printf '[]\n' >"$(cfix 30)" +issue_probe 30 $'claimed\noffsite' >/dev/null +check "one open cross-referenced PR suppresses the nudge" 1 "" \ + test -f "$TMP/posted-30" + +printf '[]\n' >"$(tfix 31)" +printf '[]\n' >"$(cfix 31)" +issue_probe 31 $'claimed\noffsite' >/dev/null +check "no visible cross-referenced PR stays silent" 1 "" test -f "$TMP/posted-31" + +: >"$(tfix 32).error" +printf '[]\n' >"$(cfix 32)" +unreadable="$(issue_probe 32 $'claimed\noffsite')" +check "an unreadable timeline stays silent" 1 "" test -f "$TMP/posted-32" +check "...and leaves the sweep running without an alarming log" 1 "" \ + grep -qiE 'error|failed' <<<"$unreadable" + +: >"$TMP/api-calls" +printf '[]\n' >"$(tfix 33)" +printf '[]\n' >"$(cfix 33)" +issue_probe 33 claimed >/dev/null +check "a non-offsite claim performs only the ordinary timeline read" 0 "1" \ + grep -cF 'repos/owner/repo/issues/33/timeline' "$TMP/api-calls" +: >"$TMP/api-calls" +printf '[]\n' >"$(tfix 34)" +printf '[]\n' >"$(cfix 34)" +issue_probe 34 $'claimed\noffsite' >/dev/null +check "an offsite claim performs the one guarded verification read" 0 "2" \ + grep -cF 'repos/owner/repo/issues/34/timeline' "$TMP/api-calls" + check "a one-hour claim stays claimed for the ordinary age reason" 0 "KEEP" \ claim_decision 1 false 3600 check "no reconciler mutation names offsite (#68 D4)" 1 "" \ -- 2.45.2 From 043aeaf17382d656c9b39a49041fea2f6fa01548 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:57:51 +0000 Subject: [PATCH 3/6] docs: define the ruling escalation contract --- BUILDER.md | 66 +++++++++++++++++++++++++++++++++++++++++++++------- CHANGELOG.md | 1 + LABELS.md | 66 +++++++++++++++++++++++++++++++++++++--------------- REVIEWER.md | 23 ++++++++++-------- TRIAGE.md | 31 +++++++++++++++--------- 5 files changed, 139 insertions(+), 48 deletions(-) diff --git a/BUILDER.md b/BUILDER.md index d966f86..11eb9ed 100644 --- a/BUILDER.md +++ b/BUILDER.md @@ -76,14 +76,64 @@ CONTRIBUTING; everything below is the shared flow.) 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 - in the PR — a maintainer can be asked for a ruling; silence and - force-forward are not options. The ruling ask has mechanics: set - `needs-ruling` on the PR, with one comment carrying the question, the - options, and your recommendation — a panel deadlock is consolidated into - that one question, never forwarded as three phrasings of it. The label - stays until agreement is *reached*, not until the maintainer replies; - you record the ruling, remove the label, and return the PR to its flow - ([LABELS.md](LABELS.md)). + in the PR — silence and force-forward are not options. A panel deadlock + 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 — +Options: A — B — +Recommend: A, because . +Blocked: +Default: | none — hard block +
Analysis…everything else…
+``` + +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ff203..e0c9250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ so entries say what changed, cite the issue, and stop. - The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52). - `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68). - `issueflow-reconcile` — nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69). +- Ruling doctrine — define every human-owned trigger, the fixed escalation shape, and the 0–24h builder-to-triage ladder (#72). ## 0.1.0 — 2026-07-22 diff --git a/LABELS.md b/LABELS.md index a4f38a1..6d86a5b 100644 --- a/LABELS.md +++ b/LABELS.md @@ -67,12 +67,15 @@ comments, unassigns the stale owner, and restores `ready`. | `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) | | `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 decision is required; the question, options and a recommendation are in the flagging comment. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply | +| `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 | | `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 | `needs-ruling` marks where the human's turn is when the pending thing is a -*decision*, not a merge (#50 settled it, D1–D10). It is not +*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: @@ -81,24 +84,49 @@ 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 -escalation contract (the question, the options, a recommendation — a bare -flag is noise), it 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 machine -reads it and never writes it: the reconciler refuses `state:needs-human` +[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. +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 diff --git a/REVIEWER.md b/REVIEWER.md index f92006d..64b1b7b 100644 --- a/REVIEWER.md +++ b/REVIEWER.md @@ -81,13 +81,16 @@ saw Y" outranks one that says "this looks like it might". - Convergence = every panel verdict approves the current head, no `blocker:*` standing. Then the builder hands off (`state:needs-human`) and the panel's job is done. -- If a round exposes a disagreement **within the panel**, argue it in the PR - with evidence until one side concedes or the builder escalates to the - maintainer for a ruling. Two reviewers pulling a builder in opposite - directions without resolution is a panel failure, not a builder failure. - The escalation is flagged `needs-ruling` — by the **builder**, never by - you: one accountable flag-setter per PR is what keeps the escalation - contract enforceable and hands the human one consolidated question - instead of three phrasings of it. Your job is to state the disagreement - precisely enough that the builder can write that escalation - ([LABELS.md](LABELS.md)). +- Flag an unowned decision when it belongs to a human: org policy, published + artifacts, secrets, prod, or any choice whose cost lands outside the PR. A + disagreement within the panel is one instance, not the definition + ([#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)). diff --git a/TRIAGE.md b/TRIAGE.md index 65193e2..d245d72 100644 --- a/TRIAGE.md +++ b/TRIAGE.md @@ -32,17 +32,26 @@ is the failure this whole flow exists to prevent. 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 the builder. -3. **Escalate.** The blocker is a *decision* only a human owns — scope, - money, product direction, breaking a public contract. Say precisely what - the decision is, list the options with your recommendation, and name the - decider. 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. You set the flag, so you 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. +3. **Escalate.** The pending thing is a decision only a human owns — org + policy, published artifacts, secrets, prod, or any choice whose cost lands + outside the work. A panel deadlock is one instance, not the definition + ([#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. + 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. 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 outcome; a zombie discussion is not. -- 2.45.2 From 3e96d8038974208169a029c84e564dfb3a0eed5f Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:20:29 +0000 Subject: [PATCH 4/6] docs: make consumer guards tag-aware --- docs/CONSUMERS.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index bea4151..ad8a977 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -70,6 +70,8 @@ the machinery at all: - uses: heavy-duty/ceremony/actions/changelog-armed@ - uses: heavy-duty/ceremony/actions/changelog-monotonic@ - uses: heavy-duty/ceremony/actions/drill-recorded@ + # Unreleased: runner-isolated is not in 0.1.0. Adopt this step with + # the pin bump to the first tag that carries it; never mix refs. - uses: heavy-duty/ceremony/actions/runner-isolated@ ``` @@ -87,6 +89,13 @@ the machinery at all: one file; the unblock is splitting the workflow. A repo with **no** self-hosted runner still wants it: the guard's value is the day somebody adds one. + + This guide documents `main`. New machinery is marked **unreleased** + here until a release tag ships it. If an action does not exist at the + consumer's pinned tag, adopt it with the pin bump to the first tag that + carries it; never mix a moving or newer ref into an otherwise exact-pin + consumer. In particular, `0.1.0` carries the three release guards above + plus `docs-sync`, but not `runner-isolated`. 6. **Labels automation** (optional but recommended): the caller from [Labels automation](#labels-automation), plus `.github/labels.conf` (panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the @@ -338,9 +347,8 @@ Bumping the pin re-syncs the mirror in the same PR — [releases page](https://github.com/heavy-duty/ceremony/releases) is that section, verbatim). One bump PR updates **every** ceremony `uses:` reference in the repo to the new tag — the workflow callers *and* each - guard step; a release-only setup already has five (the - [release caller](#release-workflow) plus the - [four CI guards](#bootstrap-a-new-repo)), and changing only one line + guard step. The exact count is tag-dependent: it is the caller plus the + guards that the pinned tag carries. Changing only one line leaves the consumer split across ceremony versions, which the same-tag rule above forbids. A repo that has adopted the agent team flow additionally bumps the mirror in the same PR — -- 2.45.2 From 162ce6b8173a84d77669fb22171ac4accc7ffbd2 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:34:01 +0000 Subject: [PATCH 5/6] docs: count every consumer workflow caller --- docs/CONSUMERS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index ad8a977..019f8d1 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -347,10 +347,10 @@ Bumping the pin re-syncs the mirror in the same PR — [releases page](https://github.com/heavy-duty/ceremony/releases) is that section, verbatim). One bump PR updates **every** ceremony `uses:` reference in the repo to the new tag — the workflow callers *and* each - guard step. The exact count is tag-dependent: it is the caller plus the - guards that the pinned tag carries. Changing only one line - leaves the consumer split across ceremony versions, which the same-tag - rule above forbids. A repo that has adopted the agent team flow + guard step. The exact count is tag-dependent: it is the workflow caller + or callers plus the guards that the pinned tag carries. Changing only + one line leaves the consumer split across ceremony versions, which the + same-tag rule above forbids. A repo that has adopted the agent team flow additionally bumps the mirror in the same PR — [the pin-bump procedure](#the-pin-bump-procedure). - **One pin governs machinery and doctrine.** The ref in the consumer's -- 2.45.2 From 0d74c827e714e1071e7c2a2b039db37411ab7f5c Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:09:42 +0000 Subject: [PATCH 6/6] docs: make consumer labels tag-aware --- docs/CONSUMERS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 019f8d1..38077fd 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -251,6 +251,7 @@ on: workflow_dispatch: # bootstraps missing labels on a fresh repo pull_request_target: types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled] + # Unreleased — not in 0.1.0; add only with the first tag carrying ceremony#32. issues: types: [opened, labeled, unlabeled, assigned, unassigned, closed] permissions: @@ -262,6 +263,10 @@ jobs: uses: heavy-duty/ceremony/.github/workflows/labels.yml@ ``` +The `issues:` trigger is **unreleased** and is not in `0.1.0`. A consumer +pinned to `0.1.0` omits it. Add it only when bumping every ceremony reference +to the first tag carrying ceremony#32; never mix refs to adopt it early. + `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. @@ -278,6 +283,12 @@ scope:cli|C5DEF5|The command-line surface scope:docs|C5DEF5|Documentation ``` +The mandatory `triage-actors=` setting is also **unreleased** and is not +accepted by `0.1.0`. At that tag the file contains `panel=` plus scope rows +only; adding `triage-actors=` is a parse failure, not an ignored setting. Add +it at the same pin bump as the `issues:` trigger, to the first tag carrying +ceremony#32 — never before it and never through mixed refs. + Both actor lists are whitespace-separated. `triage-actors` names the identities allowed to mint issues without the sweep applying `needs-triage`. Label rows use exactly `name|color|description`; blank lines are ignored and extra pipes are refused. -- 2.45.2