The third child of #197, written immediately after performing the sync it
describes, while the findings are still first-hand.
docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions,
each with the issue that decided it, so they are not re-argued every sync. The
parts that are not obvious from the outside, and that the 0.6.0 sync paid to
learn:
* THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream
moved a region, so a function upstream ADDED to a file this tree owns
arrives with no conflict and no question. Reviewing the hunks cannot find
it — four reviewers read the same diff and each found a different subset.
That was eight runtime `gh` call sites in three files and two file types.
* THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer
whose consumers auto-merged, and those consumers degrade to empty rather
than erroring, so nothing goes red. Three such seams in one sync.
* VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three
different reasons: shellcheck-all lints TRACKED files so a new file's first
lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6
exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a
test problem but a guard accepting an unreadable read.
* TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce
together, and two green PRs did produce a red tree in this sync.
* AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge
run was green and had done nothing.
.upstream-ref records the carried commit in machine-readable form beside the
CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING
file is named in the inventory — offline, comment-aware, and refusing rather
than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so
a seventh consumer is silent and a seventh decider is not.
docs/CONSUMERS.md now states that two ceremonies answer to the same version
number and how a consumer says which one it pinned.
Must-fail, both from the issue's test plan: scattering a forge_detect branch
into an unlisted file reds the guard; blanking .upstream-ref reds it too.
test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0,
actionlint, self-ref, marker, vendored and changelog-armed all clean.
Refs #200
@codex-reviewer-andresmgsl's second review, both points taken.
The refs action goes back to `forge_preflight || exit 1`. 97e63ac had it exit 0
with a notice so the PR check would not be red, and that conflated two
different questions: "this action cannot produce a verdict" is the ACTION's
contract and must stay a refusal, while "this check should not block the
board" is the CALLER's decision. The caller now carries it —
refs-guard.yml skips unless github.server_url is github.com, mirroring
forge_detect positively. A skipped check is a green head; an action that
reports success it did not earn is not. The leaked preflight_err temp file
goes with the revert.
The workflow guard asked the wrong question. `command -v gh` alone passes the
moment a Forgejo runner image happens to ship gh, and then dispatches against
a forge that cannot serve it — the client/forge mismatch forge_preflight
exists to prevent. It decides the FORGE first now, mirroring forge_detect
positively, and the binary second. The source guard splits to match: a
declaration guarded only by binary presence is reported, with a fixture that
fails on exactly that shape.
The warning text was also wrong on the facts, as noted: issue-event sweeps ARE
this caller's event-driven wakes, so they are precisely what is lost. It now
says the hourly scheduled sweep survives and every event-driven wake through
this caller does not, until #205.
Point 1 of that review — jq 1.6 accepting an empty payload — was already fixed
in 728102a, pushed before the review landed.
Verified under the runner's jq 1.6 as well as 1.7: 28 test files, 0 failed
both ways. shellcheck 0.10.0 (CI's pin), actionlint, self-ref, marker,
vendored, changelog-armed all clean with every file tracked.
Refs #198
@codex-reviewer-andresmgsl's blocker 1 is right and the filename exemption was
the wrong shape. It exempted the whole FILE — any later `gh` call anywhere in
labels.yml would have ridden in free — and it let the merge ship a step that
dies with `command not found` on every sweep on this forge, which #197's bar
does not permit.
The declaration mechanism already existed; a workflow simply could not reach
it. It can: `CEREMONY_FORGE_CLIENT: gh` in the step's env is the same
declaration actions/refs-not-closing carries, and the refusal that a script
gets from forge_preflight is inline here because a workflow has no shell to
call it from. The dispatch now warns by name, cites #205, and exits 0 rather
than reddening every sweep for a known gap.
So the guard needs no exemption list at all. It now requires the pair —
declared AND refusing — and reports a declaration that carries no refusal,
which is a permission slip for `command not found`.
That predicate was wrong on its first write, and its mutation test caught it:
`refuses_when_unavailable` matched the word `forge_preflight` inside
labels.yml's own comment explaining that it has NO forge_preflight to call. A
guard reading prose as evidence is the blind sweep again, in the guard written
to forbid it. Comments are stripped now, as gh_calls already stripped them.
Blocker 4: the nudge strips a trailing slash from the server URL. Reverting the
strip reds two cases.
Blockers 2 and 3 were already fixed in 97e63ac, before either review landed.
test/run.sh 28 files 0 failed under CI's env; shellcheck 0.10.0 (CI's pin),
actionlint, self-ref, marker, vendored and changelog-armed all clean, with
every file tracked this time.
Refs #198
The first head's `Refs guard` failed on this PR, correctly: spec 4's
CEREMONY_FORGE_CLIENT=gh declaration made forge_preflight refuse by name on
this forge. But that workflow runs on every pull request here, so the
declaration as first written turns every future PR red until #199 lands —
blocking the board for a gap that already has its own issue.
Refusing and scheduling are different questions. This action must never
produce a verdict from a graph it did not read, and it does not: on a forge it
cannot speak it now says so by name, cites #199, states that no verdict was
produced, and reaches the forge zero times. A preflight failure for any other
reason stays fatal, and on a forge it CAN speak nothing changes.
Also: five SC2016 findings in test/no-runtime-gh.test.sh. They were invisible
locally because shellcheck-all.sh lints TRACKED files and the guard was still
untracked when I ran it — a new file is exactly the case that check cannot
see. Verified this time against CI's pinned shellcheck 0.10.0 with the file
committed.
test/run.sh: 28 test files, 0 failed, under CI's CEREMONY_REQUIRE_* env.
shellcheck, actionlint, self-ref, marker and vendored guards all clean.
Refs #198
`git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`.
18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and
`lib/forge-forgejo.sh` conflict in none and come out byte-identical.
The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF
carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md`
keep this forge's; CHANGELOG keeps both sides and names the upstream commit
this tree carries.
The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole
functions to files this tree already owned, so `git merge` took its side
without raising a conflict — and with them, EIGHT runtime `gh` call sites
that #188 had removed. Seven are ported onto the shim: two reads and four
comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT
read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot
declare a client for and whose Forgejo equivalent this instance answers with
500 rather than a 4xx — named with its reason rather than ported on a guess.
test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the
diff could not: four reviewers reading it each found a different subset, and
the contract suite stubs `gh`, so a reintroduced call site passes it.
Three seams the resolution decides are silent when resolved wrongly, and each
now has a case that fails on the wrong one: the merged record's `merged_at`
third column (without it every sort key ties and the highest PR number comes
back), the open gather's one-BODY-row-per-line feed (a whole decoded body as
one record loses every declaration including the first), and the whole-board
read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged.
The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would
drop every `Closes #N` link on the open side and reclaim a claim the PR was
holding — the existing base64 round-trip case is red without it.
actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather
is GraphQL, which Forgejo does not serve at all. #199 ports it.
test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean.
Refs #198
`claimed` + `needs-ruling` with no assignee posts `claimed-unassigned`
before the tail, so the top read is the only reason the ruling nudge
below still sees the real quiet. Probe 110 asserts both outputs of one
sweep and reds when that read drifts below the post.
Recovered from ed588a2 by sha — the branch ref was deleted and the push
crossed the merge — then re-measured at fd22bd2, behind #293. 486 -> 488
on this file.
Refs #307
`.github/labels.conf` named five identities and every one of them 404s on
this instance — a GitHub-shaped team that was never minted here. Both
consumers of that roster were inoperable in consequence: `panel=` becomes
the required-verdict set, so a review round could never converge, and
`triage-actors=` is the arrival author gate, so every issue was a stray
mint owing `needs-triage` that nobody the conf recognized could clear.
Measured on #191: stamped `needs-triage` four seconds after mint by the
arrival path working correctly, then unclosable for the rest of the day.
The mapping is @andres's ruling, recorded on #191. `cluade` holds triage
and reviews but does not build; `codex` builds; `kimi` reviews; `grok`
comes off the bench; the human row is `andres`.
CONTRIBUTING's roster table moves with the conf, including the approval
count: panel-minus-author resolves to two on this roster, not three,
because the only builder is itself on the panel. The rule is unchanged and
still stated as panel-minus-author — only the number it currently comes to
is named honestly.
test/labels.test.sh now holds the conf and the table to the same set in
both directions. It cannot reach the half that actually broke — two files
agreeing with each other and neither with the forge — but it does catch a
roster edit that touches one file and not the other, which is how a
deliberate swap becomes a silent one.
Not touched, deliberately: drills/*.md, which record runs that really
happened under the old names; REVIEWER.md, whose old-name hits are
citations and a past-event anecdote rather than roster definition.
Refs #195
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The family-scoping fixture asserted that a foreign marker does not
SILENCE the flag, which a family-blind grep satisfies too. The property
that is load-bearing is the other one: a foreign family late on the
thread must not make the flag re-post. Made family-blind, the sweep now
reds.
D3b says a claimed non-member is flagged whether or not it has an open
PR, and the fixtures covered that for the collision flag only — the
window side, which is the flag the 18:11Z correction was about, had no
case at all.
check() matches its expectation as a substring, so a bare
`issueflow-reconcile` row was satisfied by `issueflow-reconcile.test`
too: the multi-extension rule stayed green under a normalization that
strips only the last extension. keys_of brackets each key, and the
no-em-dash row asserts its emptiness through grep rather than through an
expectation check() cannot make.
Beside them, the cases the two fixes are named for: a self-folding + title
answers its key once and never chains an issue to its own number, and
`needs-triage` and a label-less issue are outside both flags.
B1: deliverable_keys answered a multiset, so a `+` title whose segments
normalize to one key made collision_flags find the issue adjacent to
itself and chain it to its own number — the comment asked an issue to
declare `Blocked by` itself, and two such carriers corrupted the chain
between them. The keys are deduped where the set property belongs.
B2: window_in_scope excluded only blocked/epic/post-merge, admitting
`needs-triage` and label-less issues, so the sweep could add
`needs-triage` to an issue and then tell it about a mint-time membership
call in the same pass. Both flags now call one unblocked_claimable
predicate — #293 D2 corrected gives one gloss on `unblocked` and D3b
says D3 uses it.
The window log line says "unblocked", not "ready": D3b corrected exactly
that wording, and the flag fires on `claimed` too.
Triage ruled all three questions (18:11Z) and corrected the corpus: at the
10:28:54Z mint #257 was `ready`, not `claimed`, and #253 was `claimed`
with NO open PR — #285 was not created until 10:49:16Z. The morning board
takes that shape, which also makes the six `ready` non-members come out as
six, and puts the blocked twin on the board rather than only in a decision
probe.
D2's parenthetical is struck: unblocked = open and not blocked, both flags,
one definition. D3b is corrected in the same direction — a non-member
`claimed` WITH an open PR is flagged too, because a non-member holding a
builder and a review round is #292's competition realized, not a mild case
of it. Nothing in the implementation moves: neither flag ever consulted PR
liveness.
Three cases added, so the fixtures pin the rulings and not the prose:
- both carriers `claimed` with their own PRs open -> still flags. The
ninety-three minutes from #285's creation to its merge are exactly when
the struck parenthetical went silent on a live collision, which made the
flag's firing a property of someone's workflow rather than of the board.
- a fifteen-member declaration with every member closed -> D3 dormant, and
the release issue never flagged as its own non-member. A gate declaration
never empties; the precondition is its OPEN members, read off the board.
- flagged -> resolved -> recreated unchanged -> silent, asserted as D4's
stated boundary rather than left accidental.
Plus today's board — the `blocked` sink, a `claimed` gate member, two
`blocked` issues — which draws nothing. Verified live as well as in
fixtures: a DRY_RUN sweep of this branch against heavy-duty/ceremony's real
board draws zero flags of either kind.
Two advisory flags on the issue-flow sweep, the mechanical backstop for
#288's collision rule and #292's window rule. Both are prose today, and
both failed silently on the same morning: #284 was minted `ready` into a
file another issue held claimed with a PR in flight, and six `ready`
non-members raced an emptying gate. #262 measured the pattern — the same
class of rule, once in a guard, produced zero misses.
Comments only (D1): no label write, no state change, no new label. The
sweep never guesses intent; it states the board fact and triage resolves.
The collision key is the title's em-dash prefix NORMALIZED, because the
2026-08-04 miss spelled one deliverable two ways — `actions/issueflow-reconcile`
against bare `issueflow-reconcile` — so exact-prefix matching would have
missed the pair it was written for. One leading path segment comes off,
then every extension; a `+`-joined title matches on any segment.
The flag asks for a CHAIN, not a fan (#288 D3): within one key each issue
names the newest open carrier below it, so the declaration it asks for
releases exactly one successor per close.
A standing window is a release issue whose gate still holds an OPEN member.
The board read IS the open set, so membership decides openness with no
extra call, and an all-closed gate is the emptied gate the release's own
blocked -> ready promotion answers — which is why a `ready` release leaves
the flag dormant instead of flagging the whole board.
Dedup is the declaration echo's, extracted into state_marker /
state_echo_needed and scoped per family (D4): the marker is keyed to the
offending state's value and compared against that family's last word on
the thread, so a state that changes always speaks.
Fixtures replay the 2026-08-04 morning board whole and the post-ruling
board beside it: the first draws exactly four collision flags and six
window flags and writes not one label; the second draws none.
Closes#293.
lib/attention.sh had #267 D4's premise exactly — both reconcilers source
it, nothing release-side does — and [scope:release-flow] alone was a
wrong answer of the class that decision exists to correct. The sweep
workflow pair joins beside its trigger pair (detached in #209), the
shared-lib tests take scope:labels alone (a test inherits no lib/**
glob), and D4's seven enumerated rows land one each. No catch-all, by
decision: both directories span all four scopes. Each of the 13 new map
rows is protected by its own assertion — deleted alone, each reds
exactly its case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The state-machine fixtures bound BOT1/BOT2/BOT3 to REQUIRED_BOTS by index off
the shipped .github/labels.conf, so three fixtures silently required a
four-member panel=. Shrinking it to three left the third slot unbound and
set -u aborted the file before assertion 1: 217 assertions became 0, on main
and on every branch cut from it.
The fixtures now write their own conf, in test/labels.test.sh's shape, at all
three load sites (top of file, the #205 re-drafted-round block, and the
mutant_blockers subshell). One live-file case survives as a property — the
shipped conf parses and recuses each member from its own panel — with no
index and no expected size, and a copy whose panel= names nobody proves it
still has teeth.
Refs #304
Claiming a needs-ruling issue dated it through the assigned timeline
event, silencing the 7-day escalation nudge at exactly the moment a
builder started working through it. The ruling block now reads
last_issue_comment_activity (D1); the reclaim clock keeps the assignment
(D2) because there the assignment IS the claim; post-merge hands its
evidence read to the ruling block instead of reading again (D6, D7); the
claimed branch reads both clocks at its top, before anything it posts.
LABELS.md and lib/ruling.sh now say what each surface's clock reads
(D4, D5). The #257-era order compositions move to the comments read —
the timeline is no longer an input the issue clocks take, and its
unreadability no longer holds unrelated writes hostage; that narrowing
is pinned rather than implied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The D3 guard loop passed the action path and its test path to derives in
one call. derive_labels emits scope:guards when either matches, so any one
of the six rows could be deleted with the case still green — three
assertions standing in for six rows. Split into one assertion per path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The self-guards job checks out the merge of this branch with main, so
changelog.d/253.md from #285 reaches changelog_fragment_problem there and
nowhere else; the branch alone is green. Merged rather than rebased
because the round's reviewers track head SHAs.
Refs #262.
awk runs END on the way out of an exit from a main rule, so the length
row printed mid-file was followed by the citation row it outranks — the
internal protocol line landing inside the human-facing excerpt. Found by
claude-bot and kimi-bot in #262's first round, independently and with the
same reproduction.
The guard is the reported flag the empty-heading walk in this same
predicate already uses. The fixtures are the axis 57.md could not reach:
its over-bound entry is last, so only END's flush can print. 58.md puts
one before another bullet, 59.md before a heading and after a misplaced
cite. With lib/changelog.sh alone reverted they red, which is what the
green suite was hiding.
Refs #262.
Both findings are @codex's on !193 (#1583), and both are real.
The asset name travels as a QUERY VALUE, and the artifact-hook contract
permits any file the consumer drops in RELEASE_ASSETS_DIR. Raw
interpolation meant `release asset.tgz` made curl reject the URL outright
(exit 3), and '&', '#', '+', '%' silently changed the name or the query's
shape. `gh release create` handled all of those, so a 1:1 port had to.
Encoded through one boundary — jq's @uri, since jq is already a hard
dependency of this backend and a hand-rolled sed class is how the next
unescaped character gets through. Six backend cases cover it: the encoder
on a space and on the delimiters, uploads under both names, the created
release id in the path, and the multipart attachment. Mutation-checked:
dropping the encoder fails exactly the two name assertions.
docs/CONSUMERS.md's artifact-hook recovery still told operators to "run
`gh release create` by hand" and described the hook as running "before
`gh release create`" — on a Forgejo runner that is precisely the failure
this PR fixes. It now names the forge-neutral tag-door recovery first and
shows both clients for the manual path, without regressing the GitHub
guidance.
1035 assertions, 22 suites, shellcheck-all and actionlint clean.
Refs #191
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scoped to the fixtures the new rule actually binds: a fragment whose
predicate complaint is already its name, a smuggled heading, a dangling
heading or the 300-character bound is left alone, so the diagnosis it
tests is still the one it draws. The section-predicate fixtures are
untouched (D4).
The computed-length fixtures keep their measured lengths: the cite is
seven characters, so an entry that must measure exactly 300 builds 293
of the run and lets the cite carry the rest. 33.md's cite lands on the
last continuation line, which is the wrapped-citation case.
Refs #262
'- Fixed entry.' is shared between the dangling-heading fragment fixture
and the section-predicate fixture, so the global replace crossed D4's
line. Next commit filters to fragments that actually red on the cite rule.
Refs #262
Both panel blockers on c63a550.
@kimi found the one that mattered: facts.sh got the REPO fix, release.yml's
own four call sites did not. A workflow `run:` shell carries no `set -u`, so
an unset REPO expands empty and the verb addresses `repos//…` — which 404s,
and the 404 is then read as an ANSWER. Reproduced read-only against this
instance before fixing:
forge_release_exists 0.4.1 -> "no", rc 0
forge_commit_pulls 7fc9afe4 -> "[]", rc 0 (the !189 merge, which HAS a
merged PR behind it)
The first would have let the nothing-exists assert proceed to CREATE; the
second is the drill's original fabricated `labeled=no`, one step after the
fix meant to kill it.
Fixed once rather than at four call sites, as kimi suggested: forge_select
defaults REPO from GITHUB_REPOSITORY, and forgejo_api_base — which every
verb reaches the network through — refuses an empty REPO outright. No fifth
call site can forget it.
@grok and @kimi both blocked on the same AC gap: the backend suite did not
cover the five new verbs, so the two measured asymmetries had no offline
coverage. test/forge-backends.test.sh now has 15 cases for them — singular
/pull wrapped to an array, 404 as an empty array, 500 refusing, release
present/absent/unreadable, POST /tags vs /git/refs, the publish body, and
the REPO-empty must-fail. Mutation-checked: reading the plural path fails
one case, dropping the REPO guard fails the two must-fails.
1029 assertions, 22 suites, shellcheck-all and actionlint clean.
Refs #191
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 0.4.1 drill measured both doors dead on Forgejo. lib/facts.sh gathered
`released` with `gh release view` and `labeled` with `gh api .../pulls`, and
release.yml tagged and published with `gh` — none of which exist on the
runner image. The merge door therefore read labeled=no for a correctly
labeled, correctly merged ceremony PR and refused it as "a bare push";
the tag door cleared every gate and died at `gh release create`.
Both are ported onto lib/forge.sh. Two asymmetries were measured against
the live instance and its swagger rather than assumed:
* GitHub serves an ARRAY of PRs at /commits/{sha}/pulls; Forgejo serves a
single OBJECT at /commits/{sha}/pull and 404s on the plural. Both verbs
emit the array shape, so facts.sh carries one jq expression.
* GitHub creates a tag by POSTing to /git/refs; Forgejo serves that path
GET-only and creates tags at /tags. A 1:1 port of the gh call would
have 404'd forever.
The behaviour change is the second half of the bug. Any failure used to
become a definite `no`, which is safe for row 4 and catastrophic for row 5:
it is how a missing binary became "this was not a release ceremony". Now a
completed read that finds nothing is still `no` and still fail-closed, and a
read that did not complete refuses and emits no fact at all.
Four new cases in test/facts.test.sh cover exactly that, and a mutation back
to the old fail-closed-on-error behaviour kills all four and nothing else.
1014 assertions, 22 suites, shellcheck and actionlint clean.
Refs #191
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/VENDORED.txt and actions/docs-sync/docs-sync.sh entered the tree in
the same commit and are byte-identical at every tag — blobs 10c20a3c and
ba426479 at 0.1.0 through 0.5.0 — and 0.1.0's copy of the tool is already
manifest-driven (MANIFEST="docs/VENDORED.txt", L75). Citing 0.5.0 told the
0.1.0-0.4.1 tail, which is exactly the population this section is written
for, that the manifest was unavailable at its pin, so it would keep the
hardcoded list: #251's failure mode reproduced by the document that exists
to abolish it. The same file already said 0.1.0 at L131-L133.
Also make the guard's tracked-ness skip announce itself. It degrades to
"not asserted" wherever the tree is not a git work tree root, and doing
that in silence is the shape this script's own header argues against, so
the skip now prints on both output paths, green and red, with a test row
each way.
Round 1: claude blocking point, and claude nit 3.
One CI step beside the self-ref pin, and test/vendored.test.sh covering
both directions: the manifest -> tree scan (missing, symlink, directory,
empty, ../ escape, absolute, untracked) and the closed-world root rule
(neither list, vendored, exempted, prose is not an input, no recursion
below the root), plus the real tree unmodified and the RELEASES.md
regression both ways.
The one-off `grep -Fx RELEASES.md` row at test/docs-sync.test.sh is
deleted (#251 D4): two spellings of "the manifest is right" is the drift
the manifest exists to prevent. Its intent is now a guard case, which the
next doctrine file inherits for free.
Refs #251
"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
The nudge rode `last_issue_activity`, which counts `assigned` timeline
events because assignment is the claim the 48-hour reclaim protects.
`post-merge` has no claim: an assignee there is the invalid composition
the flag beside it reports, so counting the assignment let a broken board
buy the item another 7 days of silence — this issue's failure direction
taken backwards.
One computation, two clocks over it: `issue_activity_at` is the body,
`last_issue_activity` keeps the reclaim and ruling clocks byte-identical,
and `last_issue_comment_activity` is the evidence clock. Both clocks are
read before this branch posts anything, the ruling one included — read
after, it would date the issue by the evidence nudge's own comment and
silence the ruling nudge, which is the self-silencing the branch already
guarded against in the other direction.
Refs #254
The two 'must fail loudly' cases from the plan: an unparseable body still
nudges and the nudge quotes none of it, and a grep-level pin that no second
7-day constant appears in the sweep.
Refs #254
Covers the must-nudge pair (8 quiet days; post-merge + needs-ruling both
speak), the must-not set (6 days, fresh comment, every other queue state),
self-rate-limiting proven by sweeping again a day later rather than by
asserting a marker's absence, and zero writes across every probe.
Two existing probes move: #36's 'no comment' assertion described the
starvation this issue ends, and #67 gets recent activity so its precedence
count stays the assertion doing the work.
Refs #254