HUMAN_REVIEWER has no plumbing — every consumer’s needs-human handoff 404s on danmt, and the log reports it as done #276

Open
opened 2026-08-31 20:27:37 +00:00 by claude-bot-andresmgsl · 4 comments

Context

labels-reconcile.sh hands a completed round to a human by requesting a review from
$HUMAN, and lib/ruling.sh addresses its 7-day nudge to the same identity. Both resolve it the
same way, and neither identity can be configured by a consumer: the value is
${HUMAN_REVIEWER:-danmt}, and nothing plumbs HUMAN_REVIEWER in.

Read at main 85290031b271cae9288173a2fd9c29fc55616a8f (VERSION 0.6.4-dev):

  • labels-reconcile.sh L39
    HUMAN="${HUMAN_REVIEWER:-danmt}"
  • lib/ruling.sh L417
    local decider="${HUMAN_REVIEWER:-danmt}"
  • actions/labels-reconcile/action.yml declares
    exactly one input, bootstrap, and its reconcile labels step exports only BOOTSTRAP
    and LABELS_CONF
  • load_config accepts only panel=, panel[<login>]= and triage-actors= rows, so
    .github/labels.conf cannot carry the identity either

So on every consumer whose human is not literally danmt, the handoff request is made against a
login that does not exist on that forge.

Measured firing, twice, on heavy-duty/stoke. GET /api/v1/users/danmt404 on that
instance. With !44 sitting at state:needs-human, sweeps 679 and 680 (2026-08-31T20:12:48Z and
20:12:50Z, running the pinned 0.6.3 callers) each logged:

forge_api: HTTP 404 from 'POST repos/heavy-duty/stoke/pulls/44/requested_reviewers'
labels: #44: requested danmt (round passed)

and each ended labels: reconciled. with a success conclusion. The same pair appeared on
!41 (sweeps 605/607) and has been reproducible on that board since 2026-08-21. Its consumers work
around it with a clause in every issue they mint — "when the PR reaches state:needs-human,
request @andres by hand — do not wait for the engine"
— which is a per-issue tax paid for a
one-line configuration gap.

The second half is the log line. L837-838:

run forge_request_reviewer "$n" "$HUMAN"
log "#$n: requested $HUMAN (round passed)"

The log is unconditional. It says requested after a request that returned 404, on a line no
status check guards, in a run that then concludes success. That is the
degraded-write-reports-success class this repository's own comment at
L905-913 says #188 exists to eliminate — and
it is the reason the defect survived nine days on a consumer board that reads every sweep log: the
log agreed with the happy path.

Spec — decisions

  1. The identity goes in .github/labels.conf, not in a workflow input. Add a
    human-reviewer=<login> row to the grammar and parse it in load_config beside
    triage-actors=. Reasons, in order: it is a per-repository roster fact and belongs beside
    panel= and triage-actors=, which are the same kind of fact; it is versioned and reviewed
    in the consumer's own tree; and it reaches both readers at oncelib/ruling.sh is called
    from a different path than the reconciler action, so an action.yml input would fix the
    handoff and leave the ruling nudge still addressing danmt.
  2. danmt stays as the fallback when the row is absent. No consumer breaks on upgrade, and
    this repository's own board keeps its current behaviour with no conf change.
  3. A configured human-reviewer is validated like the rest of the roster — one login, the
    same character class triage-actors= enforces, duplicate row rejected.
  4. Make the log line tell the truth. Gate it on the request actually succeeding, and say so
    plainly when it does not: a failed handoff request is the one failure the whole
    state:needs-human path exists to deliver, so it must not read as done. Follow #101's rule —
    report what was attempted and that it did not happen; do not diagnose why.
  5. Not in scope: making a failed handoff request fail the run. It is a real question — a
    success conclusion on an undelivered handoff is exactly #188's class — but it changes every
    consumer's run status and deserves its own decision. This issue makes the identity configurable
    and the log honest; whether to red the run is left to a follow-up, and this issue does not
    presume the answer.

Tasks

  • Add human-reviewer= to load_config's accepted rows, with validation and the
    danmt fallback
  • Read it into HUMAN at labels-reconcile.sh L39 and into decider at
    lib/ruling.sh L417, so both readers take the same value from the same place
  • Gate the L838 log on the request's exit status and emit an honest line on failure
  • Document the row in CONTRIBUTING.md’s labels-conf reference and in docs/CONSUMERS.md, beside panel= and
    triage-actors=
  • Add fixture coverage for: row present, row absent (fallback), malformed row rejected, and the
    failed-request log line
  • Write changelog.d/276.md — one - bullet, at most 300 characters, ending with
    its citation
  • Open the PR from a same-repo branch with Closes this issue

Acceptance criteria

  • A consumer that sets human-reviewer=<login> in .github/labels.conf gets that login on
    the requested_reviewers POST and in the ruling nudge's @ address — both, from the one row
  • A consumer with no such row behaves exactly as today (danmt), proven by a fixture, not by
    reasoning
  • A malformed or duplicated human-reviewer= row fails config validation with a message naming
    the file, the same way a malformed panel= row does
  • When the request fails, the log does not say requested <login> (round passed); it says
    the request was attempted and did not happen
  • Every new behaviour above has a fixture that fails against the pre-fix script; each red output
    is recorded in the PR
  • The repository's full check suite is green at the PR head

Test plan

Fixture-level throughout, in test/labels-reconcile.test.sh and the config tests. The four
config cases are table-driven off a temporary labels.conf. The log case stubs
forge_request_reviewer to a non-zero exit and asserts the emitted line — that stub is the whole
point, because the live defect is invisible when the call succeeds. For the ruling reader, assert the
rendered nudge body addresses the configured login.

Dependencies

Blocked by #275.

A collision edge and nothing else (#288). #275 was minted in the same tick as this issue
(both 2026-08-31T20:27:37Z) and is an open carrier of two files this issue also writes: it was
ready when this edge was owed, and has been claimed by codex-bot-andresmgsl since
2026-08-31T20:58:32Z, its build announced at 20:59:04Z. The overlap, read from both bodies at
2026-08-31T20:59Z rather than assumed:

file #275 writes this issue writes
actions/labels-reconcile/labels-reconcile.sh the L273 variable table, L921, L1033 L39, load_config at L128, L837-838
test/labels-reconcile.test.sh two release-shape fixtures the four config fixtures and the failed-request log fixture

The regions are disjoint and neither fix depends on the other's behaviour, and that is not an
exemption: the edge is what keeps every ready issue concurrently claimable, and #275's close
releases this one. It was owed at mint and missed there; triage corrects it here rather than
leaving a second builder to discover it in a merge conflict. When #275's pull request opens,
re-read the overlap against pulls/N/files — a claimed carrier's real deliverable set is its
diff, not its body.

Nothing else stands in the way: the board carried no open pull requests and no third open
issue at 2026-08-31T20:59Z.

Reported by heavy-duty/stoke triage, whose inventory of this defect is
stoke #36 defect 1 — measured
present at that repository's pinned 0.6.1, at 0.6.3, and here at main, so no consumer
re-pin can answer it. Until this lands, consumers must keep the hand-request clause in every issue.

## Context `labels-reconcile.sh` hands a completed round to a human by requesting a review from `$HUMAN`, and `lib/ruling.sh` addresses its 7-day nudge to the same identity. Both resolve it the same way, and **neither identity can be configured by a consumer**: the value is `${HUMAN_REVIEWER:-danmt}`, and nothing plumbs `HUMAN_REVIEWER` in. Read at `main` `85290031b271cae9288173a2fd9c29fc55616a8f` (VERSION `0.6.4-dev`): - [`labels-reconcile.sh` L39](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/85290031b271cae9288173a2fd9c29fc55616a8f/actions/labels-reconcile/labels-reconcile.sh#L39) — `HUMAN="${HUMAN_REVIEWER:-danmt}"` - [`lib/ruling.sh` L417](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/85290031b271cae9288173a2fd9c29fc55616a8f/lib/ruling.sh#L417) — `local decider="${HUMAN_REVIEWER:-danmt}"` - [`actions/labels-reconcile/action.yml`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/85290031b271cae9288173a2fd9c29fc55616a8f/actions/labels-reconcile/action.yml) declares **exactly one** input, `bootstrap`, and its `reconcile labels` step exports only `BOOTSTRAP` and `LABELS_CONF` - `load_config` accepts only `panel=`, `panel[<login>]=` and `triage-actors=` rows, so `.github/labels.conf` cannot carry the identity either So on every consumer whose human is not literally `danmt`, the handoff request is made against a login that does not exist on that forge. **Measured firing, twice, on `heavy-duty/stoke`.** `GET /api/v1/users/danmt` → **404** on that instance. With !44 sitting at `state:needs-human`, sweeps 679 and 680 (2026-08-31T20:12:48Z and 20:12:50Z, running the pinned `0.6.3` callers) each logged: ``` forge_api: HTTP 404 from 'POST repos/heavy-duty/stoke/pulls/44/requested_reviewers' labels: #44: requested danmt (round passed) ``` and each ended `labels: reconciled.` with a **`success`** conclusion. The same pair appeared on !41 (sweeps 605/607) and has been reproducible on that board since 2026-08-21. Its consumers work around it with a clause in **every** issue they mint — *"when the PR reaches `state:needs-human`, request `@andres` by hand — do not wait for the engine"* — which is a per-issue tax paid for a one-line configuration gap. **The second half is the log line.** [L837-838](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/85290031b271cae9288173a2fd9c29fc55616a8f/actions/labels-reconcile/labels-reconcile.sh#L837): ```bash run forge_request_reviewer "$n" "$HUMAN" log "#$n: requested $HUMAN (round passed)" ``` The `log` is unconditional. It says *requested* after a request that returned 404, on a line no status check guards, in a run that then concludes `success`. That is the degraded-write-reports-success class this repository's own comment at [L905-913](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/85290031b271cae9288173a2fd9c29fc55616a8f/actions/labels-reconcile/labels-reconcile.sh#L905) says #188 exists to eliminate — and it is the reason the defect survived nine days on a consumer board that reads every sweep log: the log agreed with the happy path. ## Spec — decisions 1. **The identity goes in `.github/labels.conf`, not in a workflow input.** Add a `human-reviewer=<login>` row to the grammar and parse it in `load_config` beside `triage-actors=`. Reasons, in order: it is a per-repository roster fact and belongs beside `panel=` and `triage-actors=`, which are the same kind of fact; it is versioned and reviewed in the consumer's own tree; and **it reaches both readers at once** — `lib/ruling.sh` is called from a different path than the reconciler action, so an `action.yml` input would fix the handoff and leave the ruling nudge still addressing `danmt`. 2. **`danmt` stays as the fallback when the row is absent.** No consumer breaks on upgrade, and this repository's own board keeps its current behaviour with no conf change. 3. **A configured `human-reviewer` is validated like the rest of the roster** — one login, the same character class `triage-actors=` enforces, duplicate row rejected. 4. **Make the log line tell the truth.** Gate it on the request actually succeeding, and say so plainly when it does not: a failed handoff request is the one failure the whole `state:needs-human` path exists to deliver, so it must not read as done. Follow #101's rule — report what was attempted and that it did not happen; do not diagnose why. 5. **Not in scope: making a failed handoff request fail the run.** It is a real question — a `success` conclusion on an undelivered handoff is exactly #188's class — but it changes every consumer's run status and deserves its own decision. This issue makes the identity configurable and the log honest; whether to red the run is left to a follow-up, and this issue does not presume the answer. ## Tasks - [ ] Add `human-reviewer=` to `load_config`'s accepted rows, with validation and the `danmt` fallback - [ ] Read it into `HUMAN` at `labels-reconcile.sh` L39 and into `decider` at `lib/ruling.sh` L417, so both readers take the same value from the same place - [ ] Gate the L838 `log` on the request's exit status and emit an honest line on failure - [ ] Document the row in `CONTRIBUTING.md`’s labels-conf reference and in `docs/CONSUMERS.md`, beside `panel=` and `triage-actors=` - [ ] Add fixture coverage for: row present, row absent (fallback), malformed row rejected, and the failed-request log line - [ ] Write `changelog.d/276.md` — one `- ` bullet, at most 300 characters, ending with its citation - [ ] Open the PR from a same-repo branch with `Closes` this issue ## Acceptance criteria - [ ] A consumer that sets `human-reviewer=<login>` in `.github/labels.conf` gets that login on the `requested_reviewers` POST and in the ruling nudge's `@` address — both, from the one row - [ ] A consumer with no such row behaves exactly as today (`danmt`), proven by a fixture, not by reasoning - [ ] A malformed or duplicated `human-reviewer=` row fails config validation with a message naming the file, the same way a malformed `panel=` row does - [ ] When the request fails, the log does **not** say `requested <login> (round passed)`; it says the request was attempted and did not happen - [ ] Every new behaviour above has a fixture that fails against the pre-fix script; each red output is recorded in the PR - [ ] The repository's full check suite is green at the PR head ## Test plan Fixture-level throughout, in `test/labels-reconcile.test.sh` and the config tests. The four config cases are table-driven off a temporary `labels.conf`. The log case stubs `forge_request_reviewer` to a non-zero exit and asserts the emitted line — that stub is the whole point, because the live defect is invisible when the call succeeds. For the ruling reader, assert the rendered nudge body addresses the configured login. ## Dependencies Blocked by #275. **A collision edge and nothing else (#288).** #275 was minted in the same tick as this issue (both `2026-08-31T20:27:37Z`) and is an open carrier of two files this issue also writes: it was `ready` when this edge was owed, and has been `claimed` by `codex-bot-andresmgsl` since `2026-08-31T20:58:32Z`, its build announced at `20:59:04Z`. The overlap, read from both bodies at 2026-08-31T20:59Z rather than assumed: | file | #275 writes | this issue writes | | --- | --- | --- | | `actions/labels-reconcile/labels-reconcile.sh` | the L273 variable table, L921, L1033 | L39, `load_config` at L128, L837-838 | | `test/labels-reconcile.test.sh` | two release-shape fixtures | the four config fixtures and the failed-request log fixture | The regions are disjoint and neither fix depends on the other's behaviour, and that is not an exemption: the edge is what keeps every `ready` issue concurrently claimable, and #275's close releases this one. It was owed at mint and missed there; triage corrects it here rather than leaving a second builder to discover it in a merge conflict. When #275's pull request opens, re-read the overlap against `pulls/N/files` — a claimed carrier's real deliverable set is its diff, not its body. Nothing else stands in the way: the board carried **no open pull requests** and no third open issue at 2026-08-31T20:59Z. Reported by `heavy-duty/stoke` triage, whose inventory of this defect is [stoke #36](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/36) defect 1 — measured present at that repository's pinned `0.6.1`, at `0.6.3`, and here at `main`, so no consumer re-pin can answer it. Until this lands, consumers must keep the hand-request clause in every issue.
claude-bot-andresmgsl added the
bug
ready
scope:labels
labels 2026-08-31 20:27:37 +00:00
claude-bot-andresmgsl added
blocked
scope:docs
and removed
ready
labels 2026-08-31 21:00:32 +00:00

This issue's Blocked by declarations parse to: {#275}

That is the exact set this sweep gates on — what the machine read, never a
judgment about whether it is what you meant. The parse unions every clause it
finds, so a sentence like no longer blocked by #9 contributes #9 like
any other; over-retaining is the deliberate direction of error, because a stale
blocked is a triage comment away and a false ready sends a builder into
work that cannot merge. If this set names something you did not declare, or
omits something you did, edit the declaration — the next sweep echoes the
correction.

Comment only: nothing on this path writes a label. The marker carries the set
itself, so a parse unchanged since the last echo never re-posts.

<!-- issueflow:blockers-parsed-275-466ff03eea1b --> This issue's `Blocked by` declarations parse to: {#275} That is the exact set this sweep gates on — what the machine read, never a judgment about whether it is what you meant. The parse unions every clause it finds, so a sentence like `no longer blocked by #9` contributes `#9` like any other; over-retaining is the deliberate direction of error, because a stale `blocked` is a triage comment away and a false `ready` sends a builder into work that cannot merge. If this set names something you did not declare, or omits something you did, edit the declaration — the next sweep echoes the correction. *Comment only: nothing on this path writes a label. The marker carries the set itself, so a parse unchanged since the last echo never re-posts.*
Author
Member

Corrected to blocked: this issue owed a #288 collision edge at mint and did not carry one.

#275 and this issue were minted in the same tick (2026-08-31T20:27:37Z), and both write
actions/labels-reconcile/labels-reconcile.sh and test/labels-reconcile.test.sh. The issue
contract makes the newer issue name the newest open carrier unconditionally, and says in so many
words that there is "no alternative for disjoint regions" — the edge is what keeps every ready
issue concurrently claimable and makes each close release one successor. #275 is claimed as of
2026-08-31T20:58:32Z, so this is a live overlap, not a formality.

What changed here, and how each half was verified rather than assumed:

  • Body. ## Dependencies went from "No blockers." to a `Blocked by #275.` declaration
    plus the measured file-by-file overlap. The shipped parser — blocked_references in
    actions/issueflow-reconcile/issueflow-reconcile.sh — was run against the exact bytes before the
    write and reads this body as {#275} with no cross-repo references.
  • Labels. ready removed and blocked added, 2026-08-31T21:00:30Z21:00:31Z, confirmed from
    this issue's label events rather than from .labels.
  • scope:docs added. The Tasks require CONTRIBUTING.md and docs/CONSUMERS.md, and
    .github/labeler.yml maps both to scope:docs — so the pull request would have worn a scope the
    issue lacked. Precedent: #247, #271, #273.
  • Tasks. changelog.d/<this issue>.md resolved to changelog.d/276.md, the house spelling
    (changelog.d/<issue>.md, BUILDER.md).

Nothing in the Context, Spec, acceptance criteria or test plan changed, and the value of this issue
is untouched. This is not a hold and it needs no ruling: the wake condition is #275's close, which
the sweep converts to ready on its own and which triage flips by hand if the sweep is slow. When
#275's pull request opens, the overlap is worth re-reading against pulls/N/files — a claimed
carrier's real deliverable set is its diff, not its body.

**Corrected to `blocked`: this issue owed a #288 collision edge at mint and did not carry one.** #275 and this issue were minted in the same tick (`2026-08-31T20:27:37Z`), and both write `actions/labels-reconcile/labels-reconcile.sh` and `test/labels-reconcile.test.sh`. The issue contract makes the newer issue name the newest open carrier unconditionally, and says in so many words that there is *"no alternative for disjoint regions"* — the edge is what keeps every `ready` issue concurrently claimable and makes each close release one successor. #275 is `claimed` as of `2026-08-31T20:58:32Z`, so this is a live overlap, not a formality. What changed here, and how each half was verified rather than assumed: - **Body.** `## Dependencies` went from *"No blockers."* to a `` `Blocked by #275.` `` declaration plus the measured file-by-file overlap. The shipped parser — `blocked_references` in `actions/issueflow-reconcile/issueflow-reconcile.sh` — was run against the exact bytes before the write and reads this body as `{#275}` with no cross-repo references. - **Labels.** `ready` removed and `blocked` added, `2026-08-31T21:00:30Z`–`21:00:31Z`, confirmed from this issue's label events rather than from `.labels`. - **`scope:docs` added.** The Tasks require `CONTRIBUTING.md` and `docs/CONSUMERS.md`, and `.github/labeler.yml` maps both to `scope:docs` — so the pull request would have worn a scope the issue lacked. Precedent: #247, #271, #273. - **Tasks.** `changelog.d/<this issue>.md` resolved to `changelog.d/276.md`, the house spelling (`changelog.d/<issue>.md`, BUILDER.md). Nothing in the Context, Spec, acceptance criteria or test plan changed, and the value of this issue is untouched. This is not a hold and it needs no ruling: the wake condition is #275's close, which the sweep converts to `ready` on its own and which triage flips by hand if the sweep is slow. When #275's pull request opens, the overlap is worth re-reading against `pulls/N/files` — a claimed carrier's real deliverable set is its diff, not its body.
Author
Member

Collision-edge re-read against pulls/277/files — the edge holds; this issue stays blocked.

The Dependencies section above owed this re-read as soon as #275's pull request opened
("a claimed carrier's real deliverable set is its diff, not its body"). !277 opened
2026-08-31T21:05:44Z; read at 2026-08-31T21:56Z it is open, non-draft, same-repo head
build/275-merge-base-release-shape, and its diff is exactly three paths:

path !277's hunks this issue's regions shared
actions/labels-reconcile/labels-reconcile.sh @@ -270,7 +270,8 @@, @@ -918,7 +919,8 @@, @@ -1031,6 +1033,7 @@ L39, load_config at L128, L837-838 yes
test/labels-reconcile.test.sh +87 lines after L175 the four config fixtures and the failed-request log fixture yes
changelog.d/275.md new file changelog.d/276.md no

The set the body derived from #275's prose is the set its diff actually delivers — no path added,
none dropped — and the regions are still disjoint. Disjointness was never the exemption (#288):
the edge is what keeps every ready issue concurrently claimable, and #275's close is what
releases this one. No change is owed to this issue's contract.

One item recorded here for the flip tick, so it is not rediscovered then. Each of !277's three
script hunks adds exactly one line, so this issue's own line references move when that merge lands:
L39 and load_config at L128 are unchanged (both precede the first hunk), and the unconditional
log at L837-838 becomes L838-839. test/labels-reconcile.test.sh gains 87 lines after L175.
Triage re-pins those references — and re-checks the acceptance criteria against the merged tree —
in the same tick it flips this issue to ready.

Board state at this write, read from this issue's label events rather than its prose: blocked,
bug, scope:docs, scope:labels, unassigned, no open pull request. Unchanged.

**Collision-edge re-read against `pulls/277/files` — the edge holds; this issue stays `blocked`.** The Dependencies section above owed this re-read as soon as #275's pull request opened ("a claimed carrier's real deliverable set is its diff, not its body"). !277 opened `2026-08-31T21:05:44Z`; read at `2026-08-31T21:56Z` it is open, non-draft, same-repo head `build/275-merge-base-release-shape`, and its diff is exactly three paths: | path | !277's hunks | this issue's regions | shared | | --- | --- | --- | --- | | `actions/labels-reconcile/labels-reconcile.sh` | `@@ -270,7 +270,8 @@`, `@@ -918,7 +919,8 @@`, `@@ -1031,6 +1033,7 @@` | L39, `load_config` at L128, L837-838 | yes | | `test/labels-reconcile.test.sh` | +87 lines after L175 | the four config fixtures and the failed-request log fixture | yes | | `changelog.d/275.md` | new file | `changelog.d/276.md` | no | The set the body derived from #275's prose is the set its diff actually delivers — no path added, none dropped — and the regions are still disjoint. Disjointness was never the exemption (#288): the edge is what keeps every `ready` issue concurrently claimable, and #275's close is what releases this one. No change is owed to this issue's contract. **One item recorded here for the flip tick, so it is not rediscovered then.** Each of !277's three script hunks adds exactly one line, so this issue's own line references move when that merge lands: L39 and `load_config` at L128 are unchanged (both precede the first hunk), and the unconditional `log` at L837-838 becomes L838-839. `test/labels-reconcile.test.sh` gains 87 lines after L175. Triage re-pins those references — and re-checks the acceptance criteria against the merged tree — in the same tick it flips this issue to `ready`. Board state at this write, read from this issue's label events rather than its prose: `blocked`, `bug`, `scope:docs`, `scope:labels`, unassigned, no open pull request. Unchanged.

Every issue named by Blocked by is closed. The sweep is moving this issue to ready.

<!-- issueflow:blockers-cleared --> Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.
forgejo-actions added
ready
and removed
blocked
labels 2026-09-01 05:53:02 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#276
No description provided.