Second leg of the same debt #232 records. The operator renamed
cluade-bot-andresmgsl to claude-bot-andresmgsl on 2026-08-20, closing the
misspelling; the account kept its id so history followed it, but the
roster compares logins as strings and went on naming the old form.
Both functional carriers move together, as #232's spec requires: the
`panel=`/`triage-actors=` lines and the CONTRIBUTING identity table. The
conf/prose sync guard is why they cannot move apart.
Verified: `test/labels.test.sh` is 43/44 before and after — unchanged.
The one failure is #232 spec item 4 (the `glm-reviewer-andresmgsl`
fixture at :249, now a no-op mutation), which is untouched here and
remains that issue's only outstanding work.
Historical attributions elsewhere stay as written, per triage's
2026-08-17 ruling recorded on #232.
Refs #232
Emergency operator-authorized hotfix, sibling of crew 36c6745: engine and
sweep panel requests fail on the pre-rename -reviewer names. ceremony#232
records the debt and verifies. Authorized by @andres in-session.
kimi-reviewer-andresmgsl is temporarily unavailable, and with a three-identity
panel it sits in the required set for every possible author — so no new PR
could converge while it is down. glm-reviewer-andresmgsl takes the seat.
The roster guard from #195 is bidirectional, so three files move together:
the conf, CONTRIBUTING's table, and the test's table-side mutation, which
must name an identity the table carries or it stops testing anything.
Refs #222
'By decision rather than omission' published a back-tagging ruling that
belongs to the operator and has not been made. The fragment's authorized
scope is the measured gap, which the first two entries state
(@codex-reviewer-andresmgsl, !221 review).
Refs #220
Three Changed entries naming what a reader comparing the two forges' tags
would otherwise reconstruct: 0.5.0/0.6.0 arrived by merge and were never
released here; the carried ## 0.6.0 section is upstream's record; and the
absent 0.6.0 tag is a decision, not an omission. Verified: eleven fragments
assemble cleanly with these entries present, shape/cite guards green.
Refs #220
All three schedule-simulation probes carried the assignment the script no
longer reads. This time the head was verified clean BEFORE the push, not
after. Refs #215
The script stopped reading GITHUB_EVENT_NAME with the input gate; the
previous commit pushed with this red because I trusted an && echo that
printed nothing. Refs #215
The venue drill caught what no hermetic test had: with the workflow_call
bridge delivering "no" perfectly, drill runs 16/17 still bootstrapped. The
script gated on GITHUB_EVENT_NAME = workflow_dispatch and never read
$BOOTSTRAP at all; the wrapper's only coupling was exporting the event name
for yes. Correct while an operator's manual dispatch was the only dispatch
there was — inert from #209 on, when the trigger job made every machine wake
a workflow_dispatch event. Runs 459/523 bootstrapped for this reason, not
for the input-delivery defect, which is real but was never the operative
cause of the observed re-upserts.
The script now gates on ${BOOTSTRAP:-no} = yes; the wrapper passes the input
through untouched; the hermetic suite pins the exact regression pair (a
dispatch event with no/unset creates and deletes nothing) alongside the
yes path's full create+delete assertions.
Refs #215
The evidence shows the called workflow did not see the caller's event inputs
on THIS instance while the top level received them (runs 459/523 vs probe
6/7); it does not show what GitHub or a declared-and-passed input does, so
every prose site now states declare-and-pass as the measured-reliable channel
rather than attributing a drop to Forgejo (@codex-reviewer-andresmgsl, !218
blocker 2).
Refs #215
A called workflow cannot read the caller's dispatch inputs on this forge:
github.event.inputs is empty inside workflow_call even though the top-level
caller receives the value in both contexts (probe runs 6/7). The sweep's gate
read exactly that, so every dispatch-woken sweep bootstrapped — runs 459 and
523, ~20 label upserts per board event — while the trigger honestly logged
bootstrap=no.
The bridge, per the #6361 contract: labels-sweep.yml declares
workflow_call.inputs.bootstrap (string, default "no"); the dogfood caller and
the published CONSUMERS.md stub pass it via with.bootstrap with empty mapped
to "no" at the caller — kimi's edge: on schedule the top-level context is
empty, and an empty that slipped through would have turned every cron into a
bootstrap. The gate feeds the declared input to labels-reconcile unchanged,
so an invalid value meets the action's own yes|no refusal.
test/labels-bootstrap.test.sh pins every hop: the declared boundary, both
gates as the identity, no expression reading github.event.inputs (scoped to
${{ }} bodies — the file's prose names the context to explain it), the two
pass-throughs byte-exact, and the four value paths driven through the shipped
expressions into the action's real validator. Mutations: dropping the
declaration reds 4, dropping the pass-through reds 3, restoring the old gate
reds 2.
Refs #215
Off-by-one in the evidence anchor; a record that links the wrong comment is
a record that does not resolve (@codex-reviewer-andresmgsl, !216 round 2).
Refs #202
Four corrections from @codex-reviewer-andresmgsl on 9c17a9e:
- changelog.d/202.md keeps !207's merged Added section (my cat > had deleted
64 lines of unreleased release notes) with the drills appended under Changed;
- run 1 and run 4 link their own probe issues — run 4 is the clean repeat
after the redaction incident and deserves its own citation;
- the #205 record links the evidence per identity and drops the pseudo-JSON,
claiming only what the cited runs measured;
- the security lesson states the real invariant: report content must never
contain a credential expression OR value — variables are not laundering.
Refs #202
The owed-probes list keeps delivered probes with their probe-issue URLs: a
claim like 'the asymmetry reproduces' should carry a link a reader can open.
Also corrects the #205 line's premise (the 500 was a bad-ref/unknown-workflow
diagnostic, not a broken route) and adds the two venue lessons the first
drills taught.
Refs #202
Plain `env` preserves the parent environment, and on the runner every step
arrives with GITHUB_API_URL set — the premise of the fix under test — so the
unset-refusal case inherited it and never exercised the refusal. It passed
only in a dev shell that lacks the variable: the environment distance
UPSTREAM-SYNC.md step 7 warns about, in the test written the same day
(@kimi-reviewer-andresmgsl, run 468).
Refs #205
The check grepped the gh workflow run line #205 removes, so it passed on
every REST implementation including one that swallows a failed POST — a
green assertion whose name claimed an invariant its implementation could not
observe. Its replacement lives in test/labels-dispatch.test.sh: a transport
failure must fail the extracted step, plus a code-aware no-|| true guard
(@codex-reviewer-andresmgsl, !213 review round 2).
Refs #205
Three corrections from @codex-reviewer-andresmgsl's review of 935a813.
1. `api="${GITHUB_API_URL:-https://api.github.com}"` guessed GitHub when the
variable was absent — driven with a recording curl, it reported success
after POSTing to api.github.com from this forge. That is the "Never
'probably github'" rule, and the same unset-environment refusal #201 just
established for docs-sync. It now refuses before any request, and the test
asserts zero calls were made: refusing after a POST is not refusing.
2. The "never silenced with || true" invariant was still asserted by grepping
the gh line this port removed, so it passed on any REST implementation
including one that swallows a failed POST. It is rebound behaviourally: a
curl that dies at the transport must fail the step. Doing that revealed the
step failed with a bare exit 7 and no sentence, so it now names the failure
— owning the diagnostic is the whole point of the surrounding code.
3. docs/CONSUMERS.md and both caller comments still described `gh workflow
run` as the mechanism. They describe the REST dispatch now, and the manual
bootstrap command carries a forge-neutral curl form beside the gh one: a
cross-forge runbook that sends this forge to a missing binary is wrong even
where the prose around it is right.
Refs #205
The action's entire gather was one GraphQL query asking GitHub for its own
parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql
404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so
there was nothing to translate it to. It is re-expressed, as #188 re-expressed
its own two GraphQL sites, over two reads both backends serve plus this repo's
own parser.
The graph was called authoritative for including "closing keywords and sidebar
links". Those halves resolve differently here: Forgejo has no sidebar-link
concept, so nothing is lost there, but it DOES honour closing keywords in
commit messages. A body-only port would miss a PR that closes an issue from a
commit subject — exactly the contradiction this action exists to catch — so
the closing set unions the body and every commit message.
The hasNextPage refusal is relocated, not dropped: --paginate carries the
forgejo backend's x-total-count completeness proof, and a short gather refuses
rather than returning a partial verdict.
lib/issue_references.sh extracts the LOCAL/CROSS classifier from
issueflow-reconcile's executable. closes_references.sh's header recorded that
dependency in prose; a composite action cannot source a reconciler to borrow
one function, because sourcing a reconciler runs one.
refs-guard.yml's github-only gate is removed in the same change. A portable
action behind that gate is a guard that passes by never running.
The contract test drives the boundary on BOTH backends with stubs at the
transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds
the partial-gather case, ignoring a failed read reds 9.
Refs #199
`.github/workflows/labels.yml` dispatched the sweep with `gh workflow run`,
the eighth runtime gh call site the 0.6.0 merge reintroduced and the only one
!204 did not port. On this forge the runner carries neither gh nor a GitHub
API, so the step refused and the entire event-driven reconcile path ended
there — every transition waiting up to an hour for the scheduled sweep.
The workflow-dispatch endpoint has the SAME shape on both forges:
POST {api}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
{"ref": "<branch>", "inputs": {...}} -> 204, empty body
so the step no longer decides a forge at all. The CEREMONY_FORGE_CLIENT=gh
declaration and both inline refusals are removed rather than ported, and
test/no-runtime-gh.test.sh now asserts their ABSENCE — an opt-out with no gh
behind it is a standing permission slip.
The ref is supplied explicitly and taken from the repository, never from
GITHUB_REF_NAME: on a pull_request_target run that is `<n>/merge`, which is
not a branch. A non-204 still fails the job, keeping the misconfiguration
alarm the trigger exists to be, and the diagnostic explains Forgejo's empty
500 rather than passing a bare status to a reader who will go looking for an
outage that is not there.
test/labels-dispatch.test.sh extracts the shipped step and executes it against
a recording stub, asserting the method, endpoint, ref and inputs actually
sent. Dropping the inputs or ignoring a non-204 both red the suite.
Refs #205
The manifest target-validation added in 6874e76 checked owner and sha per
kind but never bound the kind to the path class, so a consistently swapped
caller layer passed: `<fork>/actions/x@<armed>` declared workflow_caller
satisfies the owner rebuild and the armed-sha test
(@codex-reviewer-andresmgsl, #202 review).
Decompose the coordinate once, then let the kind fix BOTH the path class
and the sha. Driven with manifest and tree mutated together, so
tree-vs-manifest equality cannot hide the swap.
Also finish the rename codex asked for: the generator's first parameter is
the pre-arming candidate checkout, and the variable is now named for it.
Refs #202
@codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY,
which my "parse, lint clean" claim had never meant.
1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha>
<armed-sha> and used none of them — SC2034 on all three, which is the same
defect the linter and the reviewer found independently. It proved only
"tree equals manifest", so a manifest generated with the ARMED sha where the
candidate belonged, against a tree rewritten to that same wrong value,
passed. Wrong-but-consistent is exactly what this gate exists to reject.
Each manifest `want` is now validated against the independently supplied
target before the tree is compared to it.
2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest"
while the prose below correctly said to generate from the PRE-arming tree.
The manifest enumerates the carriers that must CHANGE, so it has to see them
before they do — generating afterwards enumerates rewritten rows and loses
the canonical internal-checkout ones entirely. The generator's first
parameter is <candidate-checkout> now, and says so.
3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and
the first placement was itself invalid: SC1124, a directive may precede a
complete command, not an individual case branch. The checker's mktemp gets
a trap.
Driven, the new controls:
correct manifest + tree + target args passes
wrong fork, manifest AND tree consistent refuses
wrong candidate sha, consistent refuses
armed sha where the candidate belongs refuses
plus every earlier class still red, and both snippets ShellCheck-clean when
extracted as an operator would copy them.
test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean.
Refs #202