The PATH-stubbed gh gains a `.http-error` mode: the response body goes
to STDOUT, the reason to stderr, the status non-zero. The existing
`.error` sentinel produces empty stdout, which is the *safe* path — an
empty label set either way — and is why this class was never caught.
The three must-fail-before cases, plus the 200-`null` path a status
check alone leaves open, the suppressed-marker duplicate, the D6 tail's
count and numbers, and the crash handler proven distinct from a skip.
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 predicate's fixtures cannot see the read that sets HEAD_COMMIT_AT, so a
sweep probe drives it both ways: read, and the blocker is written off a dated
head; denied, and the denial is named on its own line while the state still
converges — this read narrows one blocker, it does not skip the PR the way an
unreadable rollup does. Renaming the assignment reds the probe.
The read also moves after the mergeability/checks skip: a PR the sweep walks
away from must not pay for a call whose only consumer is a blocker that pass
will never decide.
Refs #236
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
`blocker:unrequested` is the one blocker that names an act the author must
perform, and it never knew whether performing it was permitted. BUILDER.md's
review round requires a green check at the head before requesting, so a
builder waiting out a pending run is complying — and the blocker fired on
compliance (crew#318 ~12:44Z, ceremony#235 12:30Z, both 2026-08-03).
Gate the branch on CHECKS ∈ SUCCESS | NONE (D1): PENDING is CI's move, which
state:addressing already says, and FAILURE belongs to blocker:ci-red rather
than to a second label on the same stall. Then require the supporting facts —
the head's own date and the round's newest submitted review — to have stood
for RECONCILE_UNREQUESTED_GRACE (default 300s, D2), measured off those
timestamps because this sweep is stateless per pass. A timestamp that cannot
be read refuses the blocker.
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
post_merge_pr_for_issue answered "which merged Refs PR is this issue's
deliverable?" with sort -n | tail -n1. Merge order is not number order:
crew#176's two Refs PRs merged #184 at 19:05:16Z and #182 at 19:05:18Z.
MERGED_REF_PR_RECORDS gains mergedAt as a third column — a field on the
merged-PR node set already fetched, so no additional GraphQL request — and
the selection sorts on it, breaking ties by highest PR number so the answer
never depends on input order.
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).
@grok-reviewer-andresmgsl landed the term-5 pins in ff17d1e while I had the
same two asks in flight locally; theirs is on the branch and I dropped my
duplicate rather than push a competing tip. This adds only what the two
suites did not share.
Their pins are positive: the github twins DO call the right endpoints, and
forge_pr_activity emits all three timestamp sources. Mine had two negatives
they did not, and negatives are what catch the drift a 1:1-extraction path
actually suffers — a positive pin still passes if the github path GAINS
forgejo behaviour, and term 5 is a statement about what must NOT change.
- the github timeline is never reshaped. That timeline already IS the
shape ruling.sh selects on, so a projection here would be a second,
divergent normalizer maintained by nobody.
- github activity never derives inline comments from reviews. That
derivation exists on the forgejo path only because the flat endpoint
404s there; a github twin quietly adopting the workaround is the
"both backends drift together" failure term 5 forbids.
Mutation-verified: adding a --jq projection to the github timeline, and
swapping the flat PR-comments read for a reviews-derived one, each red their
own case. forge-backends 77 -> 79.
Refs #188
Codex 1566 held APPROVE: only Forgejo stubs covered forge_timeline and
forge_pr_activity. Pin the github twins as 1:1 extractions (timeline
paginate; issue comments + flat pulls comments + commits).
Cluade #4879: drop 2>/dev/null on the labels-reconcile activity call site
so a failed read still degrades last_activity but names the failure in the
job log (keep || true).
Panel-unanimous batch that was staged unpushed on 57abe15 (#4853):
- forge_timeline: project Forgejo label events into the GitHub shape
so the ruling ladder fires on this forge (measured mapping #4849)
- forge_pr_activity: stop calling /pulls/{n}/comments (404 here); use
reviews with comments_count > 0 for inline comments (#4844)
- ci.yml: install shellcheck before lint, mirroring actionlint — the
act-22.04 runner image does not ship it
Status captured before jq so an unreadable timeline cannot report empty.
The #226 delta sits behind reconcile_ruling's needs-ruling gate and this
board has no such item — the post-merge sweeps ran the file, never the
delta. The record now says what was observed and why the live claim is
unreachable (round 1, claude).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Found by rehearsing DRY_RUN against heavy-duty/rig's live board, not by any
test. issueflow swept ZERO issues on Forgejo and printed "reconciled." — the
blind sweep again, one layer in, and invisible because the log is identical
to a legitimately empty queue.
Measured on the two list endpoints, 2026-08-02:
GitHub plain issues OMIT pull_request -> 0 of 9 carried the key
Forgejo every entry HAS it, null on issues -> 10 of 10 carried it
So `select(has("pull_request") | not)` matched nothing here. Replaced with
`.pull_request == null`, which is true for an issue on both forges (an absent
key reads as null) and false for a PR on both. Verified against both live
list endpoints: Forgejo 10 open issues, GitHub 9 — each matching its API.
With the fix the sweep produces real decisions against rig rather than
silence: needs-triage on six issues with no queue state, the merged-Refs
post-merge transition on #133, and the conflicting-queue-labels flag on #129.
The regression test keeps the old expression as a must-fail: it disagrees
with the new one on exactly the Forgejo shape.
Refs #188
Found by running ceremony's own CI at 9357f09 on a real Forgejo runner
rather than reasoning about it.
`label` is a reserved word in jq's grammar (`label $out | … | break $out`),
so jq **1.6** rejects `$label` outright:
jq: error: syntax error, unexpected label, expecting IDENT
jq 1.7 parses it, which is why this survived: GitHub's hosted ubuntu-latest
ships 1.7, and ghcr.io/catthehacker/ubuntu:act-22.04 — the image this
instance maps ubuntu-latest to — ships 1.6. So parse_labeler_config died on
a compile error before it read a byte of config, and EVERY scope derivation
on this forge failed. Renamed to $lbl in the jq program only; the bash
locals keep their names.
Also makes test/forge.test.sh hermetic. Its "github + gh passes" case
depended on gh being on the HOST's PATH, so it passed on a developer box and
failed in the runner image, which has no gh. The preflight cases now run
against stub binaries, and the missing-binary refusal gets its own arm on a
PATH carrying the shell and text tools but no clients — the condition under
test, rather than whatever the machine happens to have.
Verified in both environments: local (jq 1.7, gh present) and the runner
image (jq 1.6, no gh) — shellcheck 0, 22 files 0 failed in each.
Refs #188
@codex-reviewer-andresmgsl #4780, concurred by @grok-reviewer-andresmgsl
#4785. All four real.
1. Three issueflow call sites still named per_page=100. The backend
sanitized it so it worked, but the frozen term and the changelog both say
no call site names a page size — and a contract that holds only because
something downstream cleans up is not the contract. Endpoints now carry
their logical query alone.
2. The suite's summary and `[ "$fail" -eq 0 ]` gate sat in the MIDDLE of
test/labels-reconcile.test.sh, and the eight outstanding_requests expects
were appended after them. Proven before fixing: a deliberately broken
term-4 assertion printed FAIL, was excluded from the totals, and the
suite still exited 0. Those assertions were decorative. The gate moves to
the true end, with a note that nothing goes below it; the reported count
goes 157 -> 164, which is the eight that were never being counted.
3. forge_labels_add and forge_request_reviewer arrived with the port and had
no boundary pins. Both backends now have them, and the labels_add cases
pin the property ceremony#128 turns on: an additive POST, never a PUT of
the whole set, exactly one write so nothing is read-modify-written.
Mutation-verified — making it RMW/PUT, or routing github through
`issue edit --add-label`, each red their own cases.
4. The historical comment said the old gathers were `forge_api graphql`. My
own mechanical port rewrote it; before #188 they were `gh api graphql`
and the abstraction did not exist.
Refs #188
The coverage owed with the call-site port (@grok-reviewer-andresmgsl #4741
note 2, #4751 item 2). Live scratch-repo evidence proved these work; these
pin the request SHAPE so they keep working.
- a removal resolves name -> numeric id, and never sends the name as the
path segment (measured: DELETE .../labels/probe:one -> 422,
DELETE .../labels/149 -> 204);
- a removal of a label the repo does not have writes nothing, matching gh:
the reconcilers call --remove-label unconditionally to converge state;
- adds take names directly, one request, comma-separated values split as
gh splits them;
- an assignee removal PATCHes the SURVIVING list, because Forgejo sets
assignees rather than adding and removing them — a naive translation
would have cleared every other assignee as a side effect of removing
one, which is what the mutation test proves is caught.
Payloads are now compact JSON. They were pretty-printed, which spread a
single write across several lines — harder to read in a log, and it hid the
shape from any assertion matching a line.
Refs #188
Term 4. GitHub clears requested_reviewers when a verdict lands, so the field
answers "who still owes a verdict" by itself. Forgejo never clears it —
measured: rig!140 listed all three panelists with all three verdicts in, and
rig!146 still lists three while MERGED, so the field is stale even on a
closed PR.
Read raw on Forgejo that is not a cosmetic over-count. `requested` drives
three decisions, and a permanently-true field pins a PR at
state:bots-reviewing for life and stops blocker:unrequested from ever being
true: the sweep believes a round is live forever and no staleness can
correct it.
So the requested set is intersected with who has NOT submitted a verdict for
the current head, derived from /pulls/{n}/reviews — the read that is true on
both forges. On GitHub the filter removes nothing, because the field is
already accurate; term 5 holds by construction rather than by care.
A STALE approval — an approval of an older head — still owes a verdict. That
is the case that matters: treating it as answered would let a stale round
read as complete, which is the shape #136 exists to prevent.
Mutation-verified both ways: reading the field raw again reds three cases,
and treating STALE as answered reds two.
Also documents @grok-reviewer-andresmgsl's ask (#4763): every panel= account
must be able to read the repo, or the forge refuses the review request —
422 naming the account on Forgejo. A real failure mode for private
consumers, and it fails loudly rather than sweeping blind.
Refs #188
Term 1 completed. All 52 runtime gh call sites in the three reconcilers and
lib/ruling.sh now go through forge_* verbs; the three remaining matches in
labels-reconcile are prose in comments. lib/facts.sh is deliberately
untouched — it is the release door, and the ruling keeps release.yml out of
this issue.
The CEREMONY_FORGE_CLIENT:-gh wrappers die here, in the same commit as the
sites they described, so the tree is never in a state where the declaration
lies. main() now runs forge_preflight then forge_select "".
Two sites needed judgment rather than substitution:
- labels-scope's write is forge_labels_add, a genuine additive POST on
both backends, NOT forge_issue_edit --add-label. ceremony#128 turns on
that write not being a read-modify-PUT: the labeler action computed
(labels-at-job-start union derived) and PUT the whole set, silently
dropping a label applied while the job ran. Routing it through a generic
edit verb would have quietly reopened that.
- the human-review request is forge_request_reviewer. Contrary to my
earlier reading, POST /pulls/{n}/requested_reviewers DOES exist on
Forgejo — 422 naming the reviewer's access without it, 201 with it. The
earlier 404 was a GET, which the endpoint does not serve, plus a
username that did not exist.
Test churn, all of it the term-5 boundary move:
- the suites select the github backend, so their existing gh() stubs stay
the boundary and keep intercepting;
- stubs strip the paging the shim injects, so fixtures stay keyed on the
logical endpoint (inlined in the PATH stub, which is a standalone
executable and cannot see a shell function);
- fixtures renamed off the per_page suffix for the same reason;
- recorded-mutation assertions now match the verb, not the raw gh line;
- gh() stubs carry SC2317: they are reached through the backend now, so
shellcheck can no longer see the call path.
Refs #188
a968e13 was pushed with shellcheck red. I chained the gates and the push in
one command, so a non-zero gate did not stop the push — the gate has to be a
condition, not a line of output I read afterwards.
Refs #188
@codex-reviewer-andresmgsl's three findings (#4743), all real.
1. forge_label_create is now an UPSERT, matching gh label create --force.
bootstrap_labels creates every declared label on EVERY workflow_dispatch,
so a plain POST onto an existing name aborted the bootstrap under set -e
from the second dispatch onward. Resolves name -> id and PATCHes when it
exists.
2. forge_pr_view carries createdAt/completedAt. checks_state groups repeated
contexts and selects the newest by [.startedAt, .createdAt, .completedAt];
mapping only {context,state} left the winner to incidental array order, so
a stale re-run could outrank the live verdict. The combined status carries
created_at and updated_at — measured.
3. forge_issue_edit refuses unknown flags and missing values. The github
backend hands them to gh, which fails; dropping them here turned a
mis-typed port site into a mutation that silently did not happen — this
issue's own failure class, inside the fix for it.
Also settles @grok-reviewer-andresmgsl's note 3 (#4741): Forgejo Actions DO
land as commit statuses on this instance, so the rollup is not empty.
rig main carries four — "ci / check (push)" and siblings, state success,
each with created_at. statusCheckRollup therefore populates, and NONE is not
silently substituted for SUCCESS.
Each fix mutation-verified: dropping the timestamps, forcing POST-always, and
restoring the silent flag skip each red exactly their own cases. The
newest-verdict case drives the real checks_state, not a copy.
Refs #188
@codex-reviewer-andresmgsl (#4727) and @grok-reviewer-andresmgsl (#4734):
"...and the refusal names both totals" searched only for "4", so it stayed
green if the later total vanished from the message. A case named "names
BOTH" must fail when one goes. Now asserts "4 then 9".
Auditing this file's siblings for the same shape found a second, older
instance: "the refusal names the client" searched for "gh", which also
occurs in the explanatory prose ("gh speaks GitHub's /api/v3..."), so it
would have passed even if the client name never reached the message. Now
asserts "the 'gh' client cannot speak it".
Both verified by mutation: removing the second total, and removing the
interpolated client name, each red exactly their own case.
Refs #188
github is the existing gh invocation extracted 1:1 (term 5). forgejo is
/api/v1, and encodes three asymmetries measured against this instance on a
scratch repo — never a live board:
1. Adding labels takes NAMES; removing one takes a numeric ID.
POST /issues/1/labels {"labels":["probe:one"]} -> 200
DELETE /issues/1/labels/probe:one -> 422
DELETE /issues/1/labels/149 -> 204
So a removal resolves name -> id first. gh hides this; the shim cannot.
2. Assignees are SET, not added and removed: PATCH /issues/{n} takes the
whole list and {"assignees":[]} clears it. --remove-assignee is therefore
a read-modify-write, not a delete.
3. There is no statusCheckRollup. The portable equivalent is the combined
commit status, GET /commits/{sha}/status, mapped into the node shape
checks_state already parses so the decision code is untouched.
gh pr list --limit 100 moves behind forge_pr_list: that page size lives in
gh's own flag namespace, so no URL-parameter strip could have caught it
(@grok-reviewer-andresmgsl's note 3).
Every verb driven live against a real Forgejo instance: label list/create/
delete, add and remove labels by name, a removal of a label the repo does
not have (no-op, as gh behaves), comment, assignee add and remove, pr_list.
Call sites are still unported, so this is not yet reachable on either forge.
Refs #188
@codex-reviewer-andresmgsl's three findings (#4712), each a route by which
an unprovable read could still be reported as a whole one — the guard
leaking the failure class it exists to stop.
1. x-total-count was never validated. `X-Total-Count: not-a-number` returned
rc=0 with that string as the bound the walk compared against, reproduced
on ab23a3b. Now required to be a canonical non-negative integer.
2. The total was read once. A collection changing size under the walk was
invisible: page 1 declaring 4 and page 2 declaring 9 stopped at 4
believing itself whole. Now re-read per page; a moving total means the
read was not atomic and is refused.
3. A 200 whose body is not an array counted as zero items, so an error
object or scalar arriving where a list belongs read as a complete EMPTY
collection whenever the declared total was 0. Now refused, quoting the
body. A genuinely empty array is still fine — covered.
Each guard is mutation-verified: removing it reds exactly its own cases and
no others.
Refs #188
SC2016 on the deliberate single-quoted bash -c (the expansion belongs to
the isolated process, as the sibling case in issueflow-reconcile.test.sh
already documents), and SC2317 on the curl stub, which shellcheck cannot
see is invoked indirectly by forge_api.
Found only after committing, because .github/scripts/shellcheck-all.sh
derives its lint set from `git ls-files` — an UNTRACKED file is not linted
at all. "Gates clean" measured before `git add` was measuring a set that
excluded the file just written. Verified from a clean clone at the pushed
SHA, which is what caught it.
Refs #188
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.
The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:
?per_page=100 GitHub 100 items Forgejo 30 items (ignored)
?limit=100 GitHub 30 items Forgejo 50 items (capped)
Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.
Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.
@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.
Call sites are not ported yet; that is the next commit.
Refs #188
@codex-reviewer-andresmgsl's draft-stage finding: the closed/merged half of
the term-3 replacement had an executable-path case, the open half did not.
The 27 closes_references cases test the parser, not the
`.body | @base64` -> base64 -d -> closes_references wiring around it.
Both directions in one sweep so neither assertion passes vacuously: #50 is
closed by an open PR and keeps its claim, #51 is closed by nothing and is
reclaimed. `Closes #50` sits on the third line of the body, so the newline
protection is non-vacuous — an @tsv-shaped regression that keeps only the
first line reclaims #50 and reds the case.
Verified by mutation: replacing the decode with `base64 -d | head -1` fails
exactly "a claim closed by an open PR survives the base64 round trip" and
nothing else; reverting restores 148/148.
The clock is injected. INOW is a fixed 2033 epoch, so without ISSUEFLOW_NOW
the subprocess reads its own wall clock, dates both claims in the future and
keeps them on a negative age — green, and proving nothing. Caught while
writing this case.
Also renames the sibling assertion that still said "through GraphQL"; that
gather has been REST since 5797b41.
Refs #188
Term 3 of #188. Forgejo has no GraphQL API, so these two gathers could not
be translated — there is no endpoint to translate them to. A real
forgejo-runner job says so from the other side: GITHUB_GRAPHQL_URL arrives
set to the empty string (probe task 278).
MERGED_REF_PR_RECORDS was already a body parse; GraphQL was buying
pagination, nothing semantic. OPEN_PR_ISSUES used GitHub's own parse of the
closing keywords, so it becomes lib/closes_references.sh — a sibling of
refs_references, sharing its LOCAL/CROSS classifier so rig#112 can still
never be read as local #112 (#61).
Both gathers now read /pulls, which /api/v3 and /api/v1 return in the same
shape (measured on both). merged_at replaces GraphQL's states: MERGED.
Bodies travel base64: jq's @tsv escapes a newline to a literal backslash-n,
which a line parser reads as one line and loses every declaration after the
first.
The accepted delta, written down rather than rediscovered: GitHub also
records closing links attached through the PR development sidebar, which
live in no body. This family declares links in the body, so the delta is
zero here.
Refs #188
The preflight half of #188, landed first so it stands alone: the forge is
decided once, before any sweep, and a client that cannot speak it exits
non-zero with a named reason.
Measured against forgejo.heavyduty.builders at 84bb1a4 — two of the three
actions reported SUCCESS having read nothing:
labels-scope exit 0 "no .github/labeler.yml" (the file is HTTP 200)
labels-reconcile exit 0 "reconciled." (zero PRs enumerated)
issueflow-reconcile exit 1 "unexpected end of JSON input"
labels-reconcile's blind-sweep warning (#96) could not fire: it counts
unreadable PRs against a list `gh pr list` never produced, and a process
substitution's failure does not trip set -e, so total stayed 0. Installing
gh makes it worse, silencing the one loud failure.
Detection is measured, not inferred from docs: a real forgejo-runner v6.3.1
job (probe task 278) shows Forgejo populating the whole GITHUB_* namespace,
so GITHUB_ACTIONS proves nothing. GITHUB_API_URL's shape, GITEA_ACTIONS and
GITHUB_SERVER_URL do. The same probe shows the runner image carries neither
gh nor stoke, which is what makes the forgejo backend REST.
Tests declare CEREMONY_FORGE at the forge boundary rather than stubbing gh
and staying silent about the forge — the boundary move term 5 asks for.
Refs #188
The reorder let a draft with a live human request plus a standing block
or comment fall through to round_state, whose human-request precedence
sits above BLOCK/FEEDBACK — 224 of claude's 1500 fixture cases read
needs-human on a PR GitHub cannot merge. decide_state now disqualifies
needs-human unconditionally under DRAFT=true, landing on
state:addressing like the blocker/needs-ruling/blocked clauses. The two
new rows assert the criterion where it can actually fail: human
requested x {CHANGES_REQUESTED, COMMENTED}. Also grok's nit: the
bootstrap row for state:building now matches LABELS.md (draft is
evidence, not the definition), and the CONSUMERS.md reflow nits are in.
Refs #205
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
panel[z]]=b parsed at the round-1 head: the case pattern only proves
some ]= occurs, so the stray ] stayed inside the login and the real
author silently fell back to the base panel — the misroute D4 exists to
refuse. The login charset is now enforced with the bracket-specific
diagnostic; codex's probe and an invalid-character row are the new
must-fail fixtures.
Refs #224
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>