The ruled-term paragraph now names its field — a check carrying a
terminal conclusion is green or not-green by that conclusion whatever
its status reports — and rules the head with no checks configured:
nothing to wait for, request straight away, no argued exception owed.
The draft-round restatement comes out so the file states the rule once.
Closes#260, closes#272 via the PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"a comment, a review or a commit" is the ruling nudge's house phrasing and
false on the issue surface twice over: there is no review or commit fact in
what the sweep reads, and an assignment is no longer counted here. Say
what is read, and say what does not buy another 7 days of silence.
Refs #254
LABELS.md said the sweep never reclaims post-merge and stopped there, which
now reads as 'the machine says nothing' — after this change it says one
thing, once per 7 quiet days. Plus the fragment.
Refs #254
BUILDER.md's review round assumed ready-throughout, so a builder or
reviewer meeting a mid-round draft found behaviour the doctrine never
described. Three points, doctrine not mechanism: the draft phase stays
the builder's through a fix round, ready-for-review is the builder's own
act and no engine's, and where the draft suppressed CI green is proven
at the flip with the request following it — step 1's rule at a stated
moment, not a second rule.
REVIEWER.md gains the reading that keeps a reviewer from misfiling it:
a draft carrying state:addressing is a fix round in progress.
Refs #258
The idempotency contract is the marker's scope, so both directions are
pinned: an unchanged set must reuse its marker (or a 15-minute cron
repeats itself forever) and a changed one must not (or a misparse hides
under a marker the thread already carries). crew#308's negated clause is
replayed through the sweep, and the empty parse is echoed beside the
untouched `blocked-unparseable` flag.
Refs #252
The per-read guards closed the reported class — a failed read never reaches
a decision function — and left one layer standing. A pass could mutate and
only THEN reach a guarded read, fail it, and report the issue as skipped:
`stale` removed, or `needs-triage` minted, under a log line saying the
sweep had touched nothing. That is the same false report #247 exists to
close, told from the other end, and the panel reproduced it on four
separate compositions.
Fixed as the ordering invariant rather than per site. Inside
reconcile_issue_pass's subshell, run() and log() stage their effects, and
commit_staged_effects replays them in order once the pass has completed.
skip_issue emits its own line directly and exits, so the buffer dies with
the subshell. A skip therefore implies zero `gh issue edit`, zero
`gh issue comment`, and no log line about a mutation that never landed —
for compositions nobody has written yet, because reconcile_issue has no way
to mutate directly. Reads stay where they are: they may happen anywhere,
since nothing lands until the end.
Stated per site it would hold until the next composition. Two consequences
worth naming: reconcile_ruling is covered without touching lib/ruling.sh,
because it posts through the sourcing script's run()/log() — the PR surface
keeps its own and is unaffected; and a genuine crash mid-pass now also
lands nothing, where before it left the earlier mutations applied. D4's
handler string, D6's tail and D7's exit 0 are all unchanged, and the
healthy path is byte-identical: every staged write commits under the same
`>/dev/null` its call site already applied.
Refs #247
`gh api` prints a 5xx response body to stdout AND exits non-zero, and
GitHub's 5xx body is a JSON object. Inside the per-issue subshell that
payload passed `has("pull_request") | not`, emptied `.labels[]`, and
`queue_decision` — correct on the input it was handed — wrote
`needs-triage` onto a healthy epic. The run then logged `reconciled.`
and exited 0 (crew#329, #247).
errexit could not have caught it: a command whose status is tested by
`||` runs with errexit suppressed, and the suppression extends through
the whole subshell body, so the `|| log` handler is what disables the
errexit that would have aborted at the failed read. Removing the handler
revives errexit and loses #91's resilience, and an inline `set -e` does
not re-arm it. Explicit per-read checks are the mechanism.
Every read inside that subshell is now checked — the issue read on its
status AND on its payload shape (an HTTP 200 whose body is `null` exits
0 and empties the label set just the same), both reads in
`last_issue_activity`, and the comments read in
`issue_comment_has_marker`. On failure the issue is left exactly as it
is, the reason rides its own `#$n:` line, and the subshell exits with a
distinguished status the sweep counts, so a deliberate skip is not
reported as a crash and a genuine crash is still named byte-identically.
`read_failure_reason` moves to lib/read.sh beside a new `guarded_read`,
sourced by both reconcilers: labels-reconcile's copy was the only one,
and the issue surface needs the identical rule.
Refs #247
The six cases the issue names, plus the boundary (the grace is inclusive), a
verdict inside the window against an old head, both unreadable timestamps, and
the configured-grace override.
Two proofs run rather than asserted in prose: a copy of the script with the
gate removed must flag the PENDING fixture, and a copy with the grace removed
must flag the inside-the-window one. The harness checks itself against the
unmutated copy first, or a flip would prove nothing.
The pre-#236 stall fixtures gain real timestamps. Their symbolic stamps are
not unreadable — GNU date reads `t1` as 01:00 in military timezone T, a time on
whatever day the suite runs — so a grace measured against a fixed NOW would
flip with the calendar. Every assertion is byte-identical.
Refs #236
issue_probe's merged-PR argument becomes a spec list — `PR` or `PR@<iso>`
— so a probe can state merge order; the bare form keeps every existing call
site literal.
The direct-drive cases cover crew#176's shape (the lower number merged
later), agreeing orders, interleaved issues, the mergedAt tie broken by
highest PR number under both input orders, and the empty answer. The
end-to-end probe is crew#321's: a marker already standing for the
later-merged, lower-numbered PR must suppress the transition, which
selecting by number could never do.
Two static pins keep the request count honest — the sweep issues exactly two
GraphQL queries, with mergedAt selected on the merged-PR node it already
fetched.
Refs #242
LABELS.md asserted 'nothing in actions/ sets, clears, reads, or validates
it' and then documented two exceptions to itself four sentences later. The
sentence is false on two of the four verbs: issueflow-reconcile.sh clears a
carried attention on the derived claimed -> post-merge transition and reads
it to gate the post-merge-assigned diagnostic. Keep the hand-set intent,
drop the absolute (#231).
Five fragments assembled into '## 0.5.0 — 2026-08-03' (#205#216#221#224#226); VERSION to bare 0.5.0; the three CEREMONY_SELF_REF carriers
stamped "0.5.0" in this one commit; the panel-rows unreleased marker in
docs/CONSUMERS.md cleared to name 0.5.0; drills/0.5.0.md records the
doors-unchanged ruling with the measurements as they are at 0ac3a6f.
Refs #233.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes-adjacent contract in the PR body; authorizing issue #226.
ruling_escalation_row scored every setter in-window row 0-4 by the shared
field matcher; highest wins, equal scores break to the earliest epoch, an
undecodable body scores 0. ruling_shape_decision now grades through the
same matcher, so the selector and the check cannot drift (crew#293).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ruling from discussion #171 as ruled: canonical text in REVIEWER.md
§What you review against item 2 (beside the verify-at-pin sub-bullet it
is the sibling of), short form in BUILDER.md §Building pointing at it.
CONTRIBUTING.md and docs/CONSUMERS.md checked for contradiction or
duplication: none — their pin prose is the mirror/caller pinning rule —
so both are deliberately untouched.
Refs #216
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each marker now says available-at-tag in the guide's existing L420
phrasing, verified by tag containment in #221; every never-mix-refs
sentence survives verbatim. The convention paragraph gains its missing
half: the release PR that ships machinery clears, in that same PR,
every marker its assembled section makes false.
Refs #221
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
round_outranks_draft consults the round before draft short-circuits: a
re-drafted PR carrying CHANGES_REQUESTED, an owed round-reply, or
push-staled approvals reads state:addressing; a live panel request on a
draft surfaces as state:bots-reviewing rather than being absorbed
(the must-not-paper-over combination, decided as: visible). Approvals do
not outrank draft, so a draft never reads needs-human, and a virgin
draft is byte-identical to before. LABELS.md's state:building row makes
draft evidence, not the definition.
Refs #205
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One resolution point (panel_for_author) feeds set_required_bots; the
author's row when the conf defines one, the base panel= otherwise, minus
the author in either case. Bracket prefixes are matched quoted so the
case patterns cannot glob (D7, panela= tripwire). configured_label_rows
skips the rows so a dispatch bootstrap cannot mint a label named after
one. BUILDER.md/REVIEWER.md carry the one D9 wording; CONSUMERS.md
publishes the row as unreleased with the parse-failure warning.
Refs #224
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consume the two displacement-fix fragments into the 0.4.1 section, stamp
VERSION and every CEREMONY_SELF_REF carrier, and record the doors-unchanged
drill ruling with the candidate-head evidence table.
Refs #212
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sweep rode the same workflow run as the PR event that woke it, so
every displacement in the shared labels-reconcile queue recorded a
CANCELLED reconcile check on some PR — fake red CI that held review
requests. The reconcile + issueflow jobs move, unchanged, to a new
reusable labels-sweep.yml behind their own caller; labels.yml gains a
trigger job that dispatches the consumer's sweep caller with the plain
GITHUB_TOKEN (workflow_dispatch is a documented no-retrigger exemption)
on every event that used to run reconcile. A displaced sweep now cancels
on the Actions tab, attached to no PR; PR checks show scope + trigger.
Because every trigger-driven wake arrives as workflow_dispatch, the event
name alone no longer separates the operator's manual bootstrap from an
event-woken sweep: the sweep caller's bootstrap dispatch input does — the
trigger passes no, a bare manual dispatch defaults to yes. The sweep
reusable also takes pr_workflow_name, exported as SELF_WORKFLOW for the
#208 reconciler (harmless to earlier ones; zero file overlap with #208).
The trigger is deliberately loud: a pin bumped without the sweep caller,
its bootstrap input, or actions: write on the labels caller goes red at
the trigger job instead of silently never sweeping again — documented in
docs/CONSUMERS.md with the split stubs and the atomic-adoption note.
Refs #209
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared reconcile concurrency group displaces queued sweeps as
CANCELLED, and the displaced run's successor attaches to a different PR —
so on the victim the newest self entry stayed CANCELLED, scored FAILURE,
and the sweep set blocker:ci-red off its own corpse every cadence
(crew#227). Drop rollup entries whose workflowName matches SELF_WORKFLOW
(defaulting to the ambient GITHUB_WORKFLOW — the caller's name, so no
workflow edit and no hardcoded consumer name) before the newest-per-context
collapse; an empty name filters nothing. A self-only rollup now honestly
scores NONE, and a genuine foreign failure still blocks beside a cancelled
self entry — the must-fail guard against re-opening #136.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trigger comments, CONSUMERS stub, changelog, and reusable labels.yml
comment said "events carry every real state change in seconds; the cron
only backstops a forgotten handoff." That is backwards: no subscribed
event wakes this sweep for a review verdict landing (no pull_request_review
trigger), blocker:ci-red set/cleared, blocker:conflict when another PR
merges, or the time-based stale/48h-reclaim. The hourly cron is the sole
discovery path for those four classes, not a forgotten-handoff net —
so the comments now name them and warn against deleting the cron (AC5).
codex-2 / kimi-2 (both blocking, round 2 @0a812c4b).
Round fixes on #200.
codex-1 (blocking): the issues narrowing dropped `edited`/`reopened`, but both
carry a queue-state change an event uniquely carries — `edited` a body rewrite
of the `Blocked by #N` declaration the sweep parses
(issueflow-reconcile.sh:179), `reopened` a closed issue re-entering the queue.
Dropping them tripped #199's must-fail. Narrow to
`[opened, closed, edited, reopened]`, dropping only the churn/validation
actions labeled/unlabeled/assigned/unassigned. Trigger tests now pin
edited/reopened present and the four dropped; labels.test.sh exact-list updated.
kimi (blocking): the "supersedes unreleased #144" prose was false — #144's
edited/reopened shipped in 0.3.0. Dissolved: we now keep them. CONSUMERS prose
rewritten to the real version history (0.2.0 #32 / 0.3.0 #144 / #199 narrows),
and the #137 review-request line corrected from "unreleased" to shipped-in-0.3.0.
kimi (non-blocking): reusable labels.yml comment no longer cites */15.
codex-2 (AC1 after-measurement / closing) escalated to triage on #199 — held,
not guessed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tree is grouped (fragments carry ### headings) and changelog-armed
caps each '- ' entry at 300 chars; the single 574-char bullet failed
both. Rewrite as three '### Changed' bullets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The advisory */15 cron fired a full-board sweep four times an hour at
GitHub's 1-minute billing floor; events carry real state changes in
seconds, so the cron only backstops a forgotten handoff. Relax it to
hourly and narrow the issues: trigger to [opened, closed] — the two
actions with promptness-critical reconcile behavior — dropping the
labeled/unlabeled churn source. Per-trigger why-comments added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refs #189. Round 1 on #191 (codex blocking, kimi nit).
codex caught that advancing the stamp to crew@4da17c4 made the attention
section false. It did, and the error is mine in kind, not only in detail: a
reconciliation stamp is a claim about the WHOLE file against that tree, and I
audited only the ci-red surface across a 17-commit advance. That is the #187
failure mode, on the PR that closes#189.
What was false at the pinned SHA, all from the crew#66 ruling (danmt,
2026-07-27) landing in d578150e:
- "One wake is registry-independent, by design: attention" — no wake is
exempt now. _attention_partition splits rows against the registry; OUT rows
are reported and never acted on.
- "the assignment is what carries the authorization — there is nothing here
for a repo list to scope" — this is the position the ruling REJECTED. The
cost was argued first: a cross-repo handoff now waits on an operator adding
the repo, which is why an out-of-scope demand also pings the operator over
the boot-gate channel rather than only reaching duty.log.
- "a fix that bounds this wake to the registry would re-create the #16
incident" — that fix landed, with the ping as its mitigation.
- The parenthetical calling crew's repos-default.txt header a contradiction
"raised there as a discussion" — the discussion is crew#66, it was ruled,
and crew's header now names the attention wake explicitly. This file was
preserving the losing side of a settled question.
Also in range and owned by this section: d849f166 ledgered the wake, so
"a session that dies before acking is relaunched; that is the whole
crash-recovery story" no longer is. Dying relaunches; COMPLETING without
acking is a decline, and the ledger stops it re-firing until the issue
moves. Meaning, not mechanism, so it belongs here.
Re-audited the rest of the advance rather than spot-fixing: fleet.roster
still declares itself the TARGET environment (the roster paragraph holds),
and the roster/install commits touch role resolution inside crew, which this
file does not describe.
kimi's nit, which the earlier rewrite made mine: the paper inventory counted
one row while the triage-signals bullet marks a second. Both are needs-ruling
rows and both are now named — a number that has to be recounted every time a
wake lands is the thing that went wrong.
18/18 test files; changelog_fragment_problem OK; shellcheck, self-ref and
git diff --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs #189 — the post-merge, triage-owned task, now reachable: heavy-duty/crew#64
merged at 4da17c49594c2d86bd3793fa3567846cbca38e90.
#190 wrote the ci-red wake into FLEET.md deliberately marked on paper, per this
file's convention for a spec that is not yet running, and left the reconciliation
stamp at crew@01fb49c because advancing it to a SHA that did not carry the wake
would be the drift #187 exists to remove. crew#64 has merged, so both halves flip:
- The stamp advances to crew@4da17c4, and every crew permalink in the file with
it. A stamp and its evidence links naming different trees is worse than no
stamp: the reader diffs the wrong engine and finds no drift because the tree
they were pointed at is the one the prose was written against.
- The four on-paper markers go: the duty-order caveat, the ci-red bullet's
parenthetical, the Build bullet's "once ci-red deploys", and the closing
paper inventory — which now names one remaining paper wake, the notifier's
needs-ruling queue, not two.
The Build bullet is not a pure marker removal. crew#64's last review round
changed what it has to say: the operator ruled the round gate a whitelist, so
the wake admits a green head OR one with no checks configured, and holds a red
head AND one whose check has not finished. Copying the old "never a round at a
red head" through the flip would have shipped a fresh inaccuracy on the same
commit that claims the file is reconciled. The ci-red bullet gains the matching
sentence from the other side: an unfinished check is not a red head and wakes
nothing, because nothing has failed yet.
Verified at the stamped SHA rather than assumed: duty.sh's header carries
attention → … → resume → ci-red → build (and says it is what this file is
reconciled against), shared/README.md's duty order matches, and notify.sh's
only label filter is still state:needs-human — which is what keeps the
remaining paper claim true.
18/18 test files pass; changelog_fragment_problem OK (it caught a 387-char
entry against the 300 bound, now split); shellcheck, actionlint self-ref and
git diff --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The round protocol told the author to re-request exactly the reviewers
who did not approve, but the handoff predicate counts an approval only
at the current head: any push while answering a round staled the
earlier approver's verdict, doctrine said not to re-request them, and
the PR could never converge — the silent-stall shape of #26/#39.
Step 2 now re-requests by head, not by verdict: every panelist after a
push, the non-approvers alone at an unchanged head. Shape 2's wording
is aligned so the two paragraphs agree.
Defect raised by dan-claude-bot on #190; folded in at the operator's
direction while the paragraph is open. Refs #189.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>