59 KiB
Changelog
The curated history of the ceremony itself. Each release's section is
published verbatim as that release's body (lib/changelog.sh extracts it),
so entries say what changed, cite the issue, and stop — at most 300
characters each, guard-enforced on the PR that writes the fragment (#167);
a genuinely long change ships several short entries, never one long one.
The citation is guard-enforced too, and it closes the entry: one (#N)
group, then the final . and nothing after it (#262). Sections published
before that rule keep their prose; the guard reads fragments only.
Entries arrive as fragments — one changelog.d/<issue>.md per PR, never
an edit to this file — and the release PR assembles them into the next
section here (bin/changelog-assemble, #112).
This tree is heavy-duty/ceremony on
forgejo.heavyduty.builders, and it tracks the upstream tree's version
numbers (#197 D2). Two trees therefore answer to the same number, differing
by the forge-compatibility delta. This tree carries upstream through
8c3a4d1 (upstream 0.6.0, merged by #198); the 0.4.1 section below is
this forge's own release, not upstream's, and upstream 0.6.1 through 0.6.3
were adopted by port rather than merge. Upstream's separate 0.4.1 section
is deliberately not carried — the tag published here is the one this section
is the body of. Each sync updates this line (docs/UPSTREAM-SYNC.md, #200).
0.6.2 — 2026-08-24
Changed
-
upstream-0.6.1was ported in #229: CONTRIBUTING routes the vendored set throughdocs/VENDORED.txt— upstream#316 and upstream#311; BUILDER.md scopes the green-check precondition to its act, upstream#330; RELEASES.md adds the post-merge gate-member split, upstream#329 (#246). -
upstream-0.6.2was ported in #229: BUILDER.md orders parked claims so an operator-owned remainder parks the claim, never the handoff — upstream#336 (#246). -
upstream-0.6.3was ported in #230: release windows read## Memberswith no gate fallback, carriers leave their own gates, stale board flags stay silent — upstream#343 and upstream#327 — and the parser accepts CommonMark rows (#246). -
Upstream logic was ported onto this forge's Forgejo-adapted issue-flow reconciler, test, and CONTRIBUTING; those files were never overwritten with upstream bytes (#246).
-
Upstream's drill-record fixes and the upstream
0.7.0–0.7.4line are deferred to the next sync campaign (#246). -
No upstream ancestry moves in this release:
.upstream-refremains8c3a4d1from upstream0.6.0, merged by #198;upstream-0.6.3is the content baseline, not a merge-base (#246). -
Release windows now read membership from a dedicated
## Membersrecord, with CommonMark-bounded rows and no fallback to predecessor gates (#230). -
Forge consumers now receive the upstream 0.6.1 and 0.6.2 doctrine for vendored-set routing, review-round signalling, operator-owned remainders, producer liveness, and post-merge release edges (#229).
-
docs/RUNNER-PROBES.mdrecords the delivered 0.6.1 consumer exercise in the standing Forgejo runner venue (#217).
Fixed
- Forgejo drafts and fast-forward conflict-check windows no longer surface as merge conflicts when the API has not distinguished one (#236).
- Forgejo review requests no longer count as verdicts, while its blocking and comment states now grade like their GitHub equivalents (#235).
0.6.1 — 2026-08-09
Added
-
test/labels-bootstrap.test.shpins the bridge at every hop: the declared boundary, both gate sites as the identity, no expression readinggithub.event.inputs, and the caller and stub pass-throughs byte-exact (#215). -
The same test drives the four value paths — schedule-empty,
no,yes, invalid — through the shipped expressions into the action's real validator (#215). -
The taxonomy bootstrap keys on the
BOOTSTRAPinput, never the event name. It testedGITHUB_EVENT_NAME = workflow_dispatch— correct while an operator's manual dispatch was the only dispatch there was, inert-by- construction from #209 on, when every machine wake became a dispatch event (#215). -
The venue drill caught that: with the bridge delivering
noperfectly, drill runs 16/17 still bootstrapped, because the script never read the input the whole chain existed to deliver (#215). -
test/labels-reconcile.test.shpins the regression pair exactly: aworkflow_dispatchevent withBOOTSTRAP=no(or unset) creates and deletes nothing; onlyBOOTSTRAP=yesbootstraps (#215). -
A gather-level case drives the real board read against a Forgejo-shaped fixture — every entry carrying the key. The existing discriminator cases assert
jqexpressions in isolation and passed throughout this regression (#210). -
A source pin forbids
has("pull_request")on this surface, because the rule was stated in a comment and violated forty lines below it. It strips comments, so the #188 warning that explains the trap is allowed to stay (#210). -
All three sites are covered behaviourally, not only by the pin: the board gather, the release-body gather through an observable window flag, and the per-issue payload check (#210).
-
test/forge-backends.test.shpins each backend's path and field, because a stubbedforge_apicannot catch a wrong path — which is how this shipped and why a live sweep was what found it (#209). -
test/labels-dispatch.test.shextracts the shipped step and executes it against a recording stub, asserting the method, endpoint, ref andinputs.bootstrapactually sent (#205). -
That test also drives the failure path: any non-204 still fails the job, so the misconfiguration alarm the trigger exists to be cannot decay into a warning (#205).
-
An unset
GITHUB_API_URLrefuses before any request instead of defaulting toapi.github.com. Guessing sent this forge's dispatch to GitHub and reported success — the "Never 'probably github'" rule, and the same unset-environment refusal #201 established for docs-sync (#205). -
A dispatch that never reaches the forge names the failure. Letting
set -ecarry curl's exit code out did fail the job, but with a bare status and no sentence (#205). -
docs/CONSUMERS.mdand both caller comments describe the REST dispatch, and the manual bootstrap command carries a forge-neutral form beside theghone — a cross-forge runbook that directs this forge to a missing binary is wrong even where the surrounding prose is right (#205). -
docs/RUNNER-PROBES.mddocuments the standing runner-probe venue,heavy-duty/ceremony-runner-probe— the place runner-only facts are measured on demand, ruled as option A by the operator (#202). -
drills/README.mdcross-links it beside the disposal rule, so the exception is visible where the dangerous habit lives (#202). -
The runbook states that the drill disposal rule does not apply to it. Archiving it defeats its purpose, and that is exactly how the three existing drill repos each became unavailable (#202).
-
It records that a probe must run as an Actions job under the workflow token: the same call answers 500 there and 204 under a PAT, so a probe run any other way produces a confident wrong answer (#202).
-
Creating the repo is recorded as the operator's step, measured rather than assumed: a fleet identity gets 403 on org repo creation and 201 in its own namespace (#202).
-
It carries an executable two-layer arming procedure: an immutable candidate code SHA and an armed workflow commit on top of it. A single layer is self-referential — rewriting a workflow makes a new commit, and a commit cannot contain its own object ID (#202).
-
Callers are pinned by layer: composite actions to the candidate code SHA, reusable workflows to the armed SHA, which is the only revision whose inner checkout points at the fork (#202).
-
The arming gate asserts what each carrier IS, not only that the old literal is gone: every
repository:equals the fork, everyCEREMONY_SELF_REFvalue equal the candidate code SHA, and callers match the layer they belong to (#202). -
It enumerates the carriers from the tree rather than encoding a count, and distinguishes ceremony's internal self-checkouts from the consumer checkouts that must stay
${{ github.repository }}(#202). -
Both published snippets are ShellCheck-clean when extracted and linted directly, not merely as part of the repository sweep (#202).
-
The checker validates the MANIFEST against the target it was given, so a manifest that describes a wrong arming consistently — wrong fork, or the armed SHA where the candidate belongs — refuses instead of matching a tree rewritten to the same wrong value (#202).
-
The manifest is generated from the PRE-arming tree, which is the only order that enumerates the carriers that must change (#202).
-
Both published snippets were driven against a constructed candidate/probe pair: deletion, both role swaps, wrong owner, wrong SHA, wrong path, a deleted caller class and an extra carrier all refuse, and the armed control passes (#202).
-
The manifest records complete caller coordinates, so a path swapped under the right owner and SHA is caught (#202).
-
Generator and checker share one domain — ceremony callers — so a third-party
actions/checkoutis neither manifested nor reported as unrecognised (#202). -
Probe results are written to an issue in the probe repo and carried to the ceremony issue by a human, so the probe holds no path that can write to the live board (#202).
-
test/docs-sync.test.shdrives the fetch path, which had no coverage at all: every existing row passes--source, which overrides the fetch entirely (#201). -
A stubbed
curlrecords the requested URL and serves a tarball, so which forge a pin resolves against is a tested decision rather than plumbing (#201). -
docs/UPSTREAM-SYNC.md— the recurring upstream sync as a runbook: the standing resolutions, which side wins each and the issue that decided it (#200). -
It names the step the 0.6.0 sync nearly shipped without: auditing what the merge brought in that did not conflict.
git mergeasks no question about a function upstream added to a file this tree owns (#200). -
It records that the same mechanic applies to state, not just to call sites: a resolved region can silently remove a producer whose consumers auto-merged, and every one of those consumers degrades to empty rather than erroring (#200).
-
It says to verify with the runner's tooling, because "green locally" was wrong three times in one sync — untracked files, a pinned linter, and a pinned
jqwhose empty-input exit code differs (#200). -
It says every branch open across a sync is stale afterwards — Forgejo never re-tests an open PR when main moves, so a prior approval is evidence about a tree that no longer exists (#200).
-
It says to audit post-merge runs by executed steps rather than colour, and to inventory what the sync changed about workflow triggers and jobs first (#200).
-
.upstream-refrecords the upstream commit this tree carries, in machine-readable form beside the CHANGELOG's prose (#200). -
test/upstream-delta.test.shfails the PR that scatters a forge decision into a file the inventory does not name. Discovery is derived from the tree, so a compositeaction.ymlor a.yamlworkflow is seen without anyone remembering to add a glob (#200). -
Discovery is git's, not the filesystem's:
ls-files, so the tarballsci.ymlextracts into the checkout and any developer cache are not parsed as source (#200). -
It refuses when the recorded commit is missing, absent from the object store, or not an ancestor — three distinct refusals, none of them a skip.
ci.ymlfetches that exact object so the test reads local evidence without CI omitting it (#200). -
Its mutation cases drive the real check against a constructed tree, so replacing the guard with
return 0reds five of them (#200). -
docs/CONSUMERS.mdstates that two ceremonies answer to the same version number, and how a consumer says which one it pinned (#200). -
lib/issue_references.sh— the LOCAL/CROSS classifier, moved out ofactions/issueflow-reconcile's executable so a second caller can use it without sourcing a reconciler, which would run one (#199, #61). -
test/refs-not-closing.test.shdrives the action's boundary on both backends with stubs at the transport, proving one fixture yields the same verdict on each — including a closing keyword that appears only in a commit message (#199). -
This tree carries upstream ceremony through
8c3a4d1(upstream0.6.0):lib/attention.sh,lib/read.sh,actions/refs-not-closing, the guarded reads, and the ruling and window rules (#198). -
test/no-runtime-gh.test.sh— the forge-portability guard: no runtimeghoutsidelib/forge-github.shunless the file declaresCEREMONY_FORGE_CLIENT=gh(#198). -
CHANGELOG.mdnames the upstream commit this tree carries, so a drill record can say which0.6.0it exercised (#197, #198). -
test/labels.test.shholds the conf's roster andCONTRIBUTING.md's roster table to the same set, in both directions, so a roster edit that touches one file and not the other goes red instead of drifting quietly (#195). -
test/forge-backends.test.shpins the replacement contract: preserve unrelated labels across a combined add+remove, an absent removal as a successful no-op, the empty set as a full clear, andforge_labels_addstillPOST-only, per ceremony#128 (#192). -
test/labels-reconcile.test.shdrives a failing write throughmain()— the swallow was in the loop, where a fixture-level probe cannot reach (#192).
Changed
-
The review panel restores
kimi-reviewer-andresmgslalongside GLM. The four-identity panel now requires three cross-vendor approvals after the PR author recuses (#224). -
The review panel names
glm-reviewer-andresmgslin place ofkimi-reviewer-andresmgslwhile that identity is unavailable. The panel stays three, so a PR still requires two cross-vendor approvals (#222). -
test/labels.test.sh's table-side roster mutation names an identity the table actually carries. Naming a departed one would mutate nothing and the case would pass while proving nothing — #195's rot class one layer down (#222). -
This forge's release line runs
0.4.1 → 0.6.1: versions 0.5.0 and 0.6.0 arrived here by merge from the read-only upstream and were never released on this forge (#220). -
The
## 0.6.0section this changelog carries is upstream's — its entries describe upstream's work under upstream's issue numbers. The forge port's own work ships first in 0.6.1 (#220). -
docs/RUNNER-PROBES.mdrecords the venue's first delivered drills — the #192 asymmetry re-observed on demand under the workflow token, the dispatch route's 204 under both identities, and #215's boundary finding — each with the probe-issue URL it is recorded in (#202). -
Two venue lessons join the runbook where the next probe author will look: findings must be written to issues because the venue's log route 404s for non-admin reads, and report content sent to the forge must never contain a credential expression or value (#202).
Fixed
-
The sweep's
bootstrapvalue crosses theworkflow_callboundary as a declared input, explicitly passed by the caller — the one channel measured to work; the called workflow did not see the caller's event inputs as an implicit substitute on this instance (#215). -
Before the bridge,
github.event.inputswas empty inside the called workflow, so every dispatch-woken sweep bootstrapped: ~20 label upserts on each board event (#215). -
The caller maps an empty top-level value to
noexplicitly, so a cron-woken sweep can never bootstrap; the declared input also defaults tono, so a consumer that passes nothing gets the safe path (#215). -
The gate feeds the declared input to
labels-reconcileunchanged, so an invalid value meets the action's ownyes|norefusal instead of being silently coerced (#215). -
docs/CONSUMERS.md's published sweep stub carries the same pass-through — without it every consumer inherits the defect ceremony fixed for itself (#215). -
issueflow-reconcilesees this forge's issues again. The board gather usedhas("pull_request"), and every Forgejo entry carries that key — so it selected zero rows on every sweep while printingreconciled.(#210). -
Three sites take
.pull_request == null, the discriminator the file's own comment already specified and that one of its four call sites already used (#210). -
post-mergetransitions can fire again: they could not, because the sweep saw no issues to transition (#210). -
blocker:unrequestedis judged on this forge again. The head-commit date was read fromrepos/{o}/{r}/commits/{sha}, which Forgejo answers 404 — so every sweep degraded and left the blocker unjudged (#209). -
forge_commit_atis a verb on both backends: GitHub serves a single commit at the bare path with the date nested, Forgejo atgit/commits/{sha}with it under.created. The caller asks for one timestamp and knows neither shape (#209). -
.github/workflows/labels.ymlwakes the sweep over REST instead ofgh workflow run, so a board event reconciles within seconds on any forge rather than waiting up to an hour for the scheduled sweep (#205). -
The workflow-dispatch endpoint has the same shape on both forges, so that step no longer decides one: the
CEREMONY_FORGE_CLIENT=ghdeclaration and both inline refusals are gone rather than ported (#205). -
The dispatch supplies its
refexplicitly, because REST has no default branch wheregh workflow runhad one, and refuses without it (#205). -
It takes that ref from the repository, never from
GITHUB_REF_NAME— on apull_request_targetrun that is<n>/merge, which is not a branch (#205). -
A failed dispatch names the endpoint, the ref and the status, and says that an empty
500body from Forgejo means the workflow name or the ref did not resolve — a bare status sends the reader after a server fault that is not there (#205). -
actions/docs-syncfetches the doctrine mirror from the forge named byGITHUB_SERVER_URLinstead of a hard-codedgithub.com(#201). -
The same pin ref names a different tree on each forge, so a consumer's mirror was verified against a tree it never pinned — and with HTTP 200, so
--checkreported drift that could not be fixed (#201). -
A fetch that cannot name its forge now refuses instead of guessing: no
GITHUB_SERVER_URLand no--sourceexits naming the variable, having reached for no network (#201). -
A failed fetch names the URL it actually tried, and asks whether the ref exists on that forge rather than in the abstract (#201).
-
actions/refs-not-closinggathers over REST through the forge shim instead of one GraphQL query, so it produces a real verdict on Forgejo — which serves no GraphQL surface at all — rather than refusing (#199). -
The closing set is parsed by
lib/closes_references.shover the PR body and every commit message, unioned. Forgejo honours closing keywords in commit messages, so a body-only port would miss a PR that closes an issue from a commit subject (#199). -
The
hasNextPagerefusal is carried onto the paginated commit read: an incomplete gather refuses instead of returning a partial verdict, reusing the backend'sx-total-countcompleteness proof (#199). -
A failed read never reaches the parser. An unread body parses to an empty closing set, which is a passing verdict the action never earned (#199).
-
.github/workflows/refs-guard.ymlno longer gates the job on the forge. A portable action behind a github-only gate is a guard that passes by never running (#199, #198). -
The
CEREMONY_FORGE_CLIENT=ghdeclaration is gone, and the contract test asserts its absence: an opt-out with noghbehind it is a standing permission slip (#199). -
Eight runtime
ghcall sites arrived with the merge outside every conflict hunk, in functions upstream added to files this tree already owned. Seven are ported onto the shim; the eighth is named with its reason (#198). -
The open-PR gather reads
Refs, not only closing keywords. Reading one side for closing links and the other forRefsis what released a live claim in crew#321, and this tree carried that shape (#198). -
The merged record gains
merged_at, sopost_merge_pr_for_issueanswers the PR that merged last rather than the highest-numbered one. Without the column every sort key ties and the old order returns silently (#198). -
The open gather feeds
open_pr_issuesone record per physical body line. A whole decoded body as one record loses every declaration including the first, and reclaims a claim a live PR was holding (#198). -
The post-merge nudge links the issue on the forge in play rather than a hard-coded
github.com(#198). -
actions/refs-not-closingreports and skips on a forge it cannot speak, naming the client and #199, instead of standing red on every PR. It reaches the forge zero times, so no verdict is produced either way (#198). -
.github/workflows/labels.yml's sweep dispatch declares the client it speaks and decides the FORGE before the binary, so a Forgejo runner that happens to shipghcannot dispatch against a forge that cannot serve it. #205 ports it to REST (#198). -
actions/refs-not-closingfails closed on a forge it cannot speak, and.github/workflows/refs-guard.ymlcarries the scheduling decision — the action never reports a success it did not earn (#198). -
issue_payload_validrefuses an empty payload on jq 1.6 as well as 1.7.jq -eexits 4 on empty input under 1.7 and 0 under 1.6, and this instance's runner carries 1.6 — so the guard #247 D3 added to refuse an unreadable read was accepting one here (#198). -
The post-merge nudge strips a trailing slash from the server URL, so a forge URL carrying one does not render
//owner/repo(#198). -
.github/scripts/release-path.shnameslib/forge.sh: #191 put the shim on the release doors' executable path here, so a doors-unchanged record that omitted it was measuring the wrong set (#198). -
.github/labels.confnames identities that exist on the forge this repo lives on. All five it named before were absent, sopanel=could never converge a review round andtriage-actors=made every issue a stray mint nobody could normalize (#195). -
CONTRIBUTING.md's roster table matches the conf: three identities, the human row isandres, and the approval count states what panel-minus-author actually resolves to on this roster rather than a stale three (#195). -
Label removal on Forgejo is a full-set
PUT, not a per-labelDELETE. The workflow token gets HTTP 500 on everyDELETE .../labels/{id}on this instance, so the state machine could only ever ADD labels (#192). -
Every
state:*transition that needs the previous state cleared, and everyblocker:*that should lift, can now actually clear. They were inert (#192). -
A label edit that fails is fatal to
labels-reconcile, matchingissueflow-reconcile. One cause had two contradictory policies (#192). -
A failed write reaches the sweep's exit code: per-PR tolerance is kept for READS, but a sweep that could not write exits non-zero and its output carries no
reconciled.token at all (#192). -
Every label mutation goes through one checked helper, so clearing
merge-nextor eitherstaleedit fails the sweep too — not only the primary state edit (#192). -
A preserved label keeps the id the issue payload already carried, so preservation does not depend on a repository-wide list that has nothing to do with the issue (#192).
-
A removal that changes nothing writes nothing, rather than replacing the set with itself and opening a race for no state change (#192).
-
Every failure diagnostic on the forgejo backend names the verb as well as the path and the status. A read used to say
HTTP 500 from 'repos/…', which cannot be told from a failed write of the same path (#192). -
The diagnostic names what was attempted and that it did not happen, instead of blaming a missing label and telling the operator to bootstrap — a cause it had not established (#192, #101).
-
An add-label the repo does not carry refuses before any write, so a replacement
PUTcan never drop a label nobody asked to remove (#192).
0.6.0 — 2026-08-05
Added
- The issue-flow sweep's
claimed-branch ruling pre-read is pinned: an unassigned claim underneeds-rulingmust draw its board diagnostic and its ruling nudge in one sweep, so a read that drifts below the diagnostic reds instead of silently costing the escalation 7 days (#284, #307). - The issue-flow sweep now flags a collision the board never declared: two
open, unblocked issues whose titles name one deliverable draw a comment
naming the newer's owed
Blocked byedge. Keys normalize, soactions/xandxare one deliverable (#288). - The sweep now flags an unblocked non-member during a standing release
window, naming the window's invariant.
claimedcounts, PR in flight or not. The gate is read from the release issue's ownBlocked bydeclarations, and an emptied gate leaves it dormant (#292). - Both flags are advisory: comments only, no label write and no state change, deduped against each family's last word on the thread so a standing state re-sweeps silently (#293).
- The fragment guard now requires each entry to end with its issue
citation: one
(#N)group — local,repo#Norowner/repo#Nreferences separated by,— then the final.and nothing after it (#262). - The refusal distinguishes an entry carrying no reference at all from one whose reference is present but not terminal, and names the shape to write in both (#262).
- The 300-character bound still outranks the citation across the whole fragment, and the outranked problem stays out of the message it lost to: one fragment, one diagnosis, wherever in the file it sits (#262).
- BUILDER.md now describes a fix round that rides a draft: the draft phase stays the builder's, ready-for-review is the builder's own act, and where a draft suppressed the checks green is proven at the flip (#258).
- REVIEWER.md now reads a draft carrying
state:addressingas a fix round in progress rather than abandonment (#258). - A
post-mergeitem with no comment for 7 days now draws one nudge from the issue sweep: the wake evidence is owed. A starving criterion used to be found only when someone happened to run the right read (#254). - Label churn does not reset that clock, and neither does an assignment: on
post-mergean assignee is an invalid composition, not activity, and it must not buy the item another 7 days of silence (#254). - The nudge names the triage actor from
triage-actors=, not the human reviewer:post-mergeis triage's completion queue, so the starved wake condition is triage's to answer (#254). - It links the item and parses nothing from the body — which criterion starved is prose, and the machine never judges prose (#254).
- Like the ruling nudge it carries no idempotency marker on purpose: the comment is itself activity, so the rule self-rate-limits to one nudge per 7 quiet days. Comment-only — no path here writes a label (#254).
- Release epics now announce release initialization when their declared dependency gates clear (#253).
- The issue sweep now echoes an issue's parsed
Blocked byset as a comment whenever that set changes, so a readable-but-wrong declaration is visible in one sweep instead of days later, when a human happens to run the parser by hand (#252). - The echo's marker carries the parsed set itself: an unchanged parse never re-posts on a 15-minute cron, and a changed one always speaks. Comment-only — no path here writes a label (#252).
- CI now refuses a root
*.mddeclared in neitherdocs/VENDORED.txtnor the guard's short exemption list, so a new doctrine file can no longer reach a tag undeclared and stay invisible to every consumer'sdocs-sync(#251). - The same guard reads the manifest the other way: every entry must resolve to
a regular, non-empty, tracked file — no symlink, no directory, no
../escape (#251). - Document the optional, operator-ruled release-epic flow for governed repositories. (#248).
- Guard documentation availability markers against missing issue citations and release candidates that already ship the cited work (#238).
- The label and issue-flow sweeps now comment once per episode when
attentiontargets a pull request or an unassigned issue, without retargeting the demand or changing labels or assignees (#232). - Pull requests that promise
Refs #Nnow fail a read-only, body-edit-aware guard if GitHub would close N through a keyword or sidebar link (#218).
Changed
README.mdis rewritten whole from the current tree: the front page names the governance repo ceremony now is, routes todocs/CONSUMERS.md,AGENTS.md,LABELS.mdandRELEASES.mdrather than restating them, and keeps the operator's release runbook as its core, re-measured (#311).- Standing release windows are dependency DAGs: every mint is placed in the window or behind it, and only current sources are
ready(#292). - TRIAGE.md now requires unconditional collision-edge chains when open issues carry the same deliverable, keeping the ready queue concurrently claimable (#288).
- TRIAGE.md now states its rules with bare record cites: the label-race and lifted-hold incident narratives leave the normative text while their operational rules remain complete (#282).
BUILDER.mdstates its rules and cites their record bare: the incident narratives, the links into issue comments and the cross-repo issue cites leave the normative text, which no rule leaves with them (#281).- CONTRIBUTING.md now keeps vendored doctrine self-contained: state the rule, retain at most one sentence of why, cite the local record bare, and leave the incident narrative in that record (#280).
- BUILDER.md's green ruled term now says which entry to read before it says what an entry means: a check's word at a head is its newest entry by start time, and a cancelled entry is not that word while the same check carries a non-cancelled one at that head (#276).
- A check whose every entry at the head is cancelled is unchanged — nothing
survived to be its word, so it never reported and is not green — and the
collapse mirrors
checks_state's carve-out rather than adding a class (#276). - BUILDER.md's step 1 now rules the checkless head: no checks configured is nothing to wait for, and the request goes out straight away — stated once, in the ruled-term paragraph, with the draft-round restatement removed (#272).
README.mdandRELEASES.mdderivescope:docs, and thechangelog-assembled,docs-syncandrunner-isolatedactions and tests derivescope:guards; all five were mapped nowhere. The docs block matched a literalREADME, which this tree does not carry (#267).lib/read.shandlib/ruling.shderivescope:labelsbesidescope:release-flow. Both reconcilers share them, and a mixed file wears both labels rather thanlib/**being re-carved into a row per file (#267).- TRIAGE.md now tells every epic author to put its progress checklist under
the literal
## Task listheading, because any other heading is silently invisible to the completion sweep (#266). - TRIAGE.md now scopes the no-assignee board bug to flagging an unassigned issue, while still directing triage to repair ownership instead (#264).
BUILDER.mdandCHANGELOG.mdstate the citation as guard-enforced rather than as house style, beside the 300-character bound it now sits next to (#262).- Four fragments in flight gained a terminal citation; published sections are untouched, so no shipped prose is re-opened (#262).
- BUILDER.md's green ruled term now names its field: greenness is read from
each check's
conclusion, never itsstatus, and stale means a check of a superseded head — not a same-head node whosestatuslags its own conclusion (#260). - Consumer guidance: re-vendor tooling reads the pin's
docs/VENDORED.txt, never a hardcoded list, so a new doctrine file propagates at the next ordinary pin bump with zero list edits (#251). - Define the doors-unchanged drill record and an executable release-path list, so a release may reuse live evidence only when its door bytes are unchanged since the last rehearsed tag (#237).
Fixed
- A roster edit no longer reds the whole suite: the labels-reconcile
state-machine fixtures name their own panel instead of binding
.github/labels.confby slot (#304). - Shrinking
panel=to three had left that binding's third slot unbound, andset -uaborted the file before its first assertion — 217 assertions became 0, onmainand on every branch cut from it (#304). - The one case still reading the shipped roster asserts a property, not a
size: it parses, and each member is recused from its own panel. Any
panel=of one or more members leavestest/run.shgreen (#304). lib/attention.shlocates as label machinery beside its two shelf-mates —[scope:release-flow]alone was a wrong answer of the class #267 measured — and the map learns the sweep workflow pair, the shared-lib tests, and seven enumerated test/guard surfaces (#302).- Claiming a
needs-rulingissue no longer buys its escalation another 7 quiet days: the issue-side ruling clock reads comments alone — an assignment is the claim clock's fact — and LABELS.md now names what each surface's clock reads (#284). scope:release-flowno longer rides every pull request:changelog.d/**is out of its path map. Doctrine makes every behavior change write a fragment, so the glob labelled 20 of the last 20 PRs while 3 touched a release surface.CHANGELOG.mdstays, as only the release PR edits it (#267).- The issue-flow reconciler and its test now derive
scope:labels, the scope that already names the taxonomy they reconcile (#267). - Abort issue-flow reconciliation when the board read fails instead of reporting a complete pass over an empty or partial result (#257).
- The issue sweep no longer derives label writes from a read that failed. An
HTTP 504 whose body is GitHub's JSON error object passed every guard and
emptied the label set, so a healthy epic was written
needs-triageand the pass reported success (#247). - A failed comments read no longer reclaims a live claim. Swallowed, it dated
the issue by
created_atand unassigned the builder under a comment asserting 48 hours of silence about an issue commented on seconds earlier (#247). - A failed comments read no longer reads as "no marker", which re-posted the comment the marker exists to suppress (#247).
- Every read inside the per-issue subshell is checked explicitly, on its
status and on its payload shape; the issue is left exactly as it is and the
sweep continues. A partial pass names its skipped issues after
reconciled.(#247). - A per-issue pass is now atomic: its writes and its log lines commit only once the pass completes. A skip could previously land after an earlier mutation, reporting an issue as untouched when a label had already been written or removed (#247).
- The issue-flow sweep now reads an issue's deliverable as the
RefsPR that merged last, not the one numbered highest — merge order is not number order, and the old rule spent the transition marker on the wrong PR (#242). - Preserve active claims when an open local pull request links them with
Refs #N. (#241). blocker:unrequestedno longer fires while a head's checks are pending or red: the review round forbids requesting there, so the one blocker that demanded an act flagged builders for complying. Pending is CI's move, red isblocker:ci-red's (#236).blocker:unrequestednow waits for the round to settle — the head and the newest verdict must have stood forRECONCILE_UNREQUESTED_GRACE(default 300s) — so a sweep landing between a push and its re-request no longer flags a round in motion (#236).- LABELS.md no longer claims nothing in
actions/clears or readsattention: the reconciler has done both since the derivedclaimed→post-mergetransition shipped. The amended text keeps the hand-set rule and admits the one clear and the diagnostic read (#231). - Triage now puts
attentionon the assigned issue that owns a claim, never on its pull request, and treats an unassigned issue as a board bug rather than a demand (#230).
0.5.0 — 2026-08-03
Added
labels.confaccepts optionalpanel[<login>]=rows: the required set for a PR authored by that login is the row minus the author; other authors keeppanel=. Consumers gain the row at their next pin bump — adding it before that bump is a parse failure that takes the label board down (#224).
Changed
- Doctrine: third-party actions never hold a write-capable token by default — repo-owned scripts in write-capable jobs, established publisher plus full-SHA pin for the exception, SHA pins everywhere. Canonical in REVIEWER.md, short form in BUILDER.md; consumers adopt at the pin bump (#216).
Fixed
ruling_escalation_rowselects the setter's best-shaped in-window comment, ties broken to the earliest, instead of the earliest outright — a whole-round reply landing seconds before the escalation is no longer graded in its place (crew#293).- The escalation selector and
ruling_shape_decisionshare one field-presence matcher, and an undecodable body column scores 0 instead of erroring the sweep. - Five stale unreleased markers in
docs/CONSUMERS.mdnow name their tags: fragment mode,changelog-assembledandrunner-isolatedat0.2.0; the additive labeler at0.3.0; the two-caller split at0.4.1(#221). - The marker convention now names its clearing owner: the release PR that ships machinery clears, in that same PR, every marker its assembled section makes false (#221).
- A standing non-approving verdict now outranks draft in
decide_state: a re-drafted PR mid-round readsstate:addressing, a live panel request on a draft surfaces asstate:bots-reviewing, and a draft with no round history still readsstate:building(#205).
0.4.1 — 2026-08-04
Added
-
forge_release_exists,forge_commit_pulls,forge_tag_create,forge_release_createandforge_pr_createon both backends, so the release path names no client (#191). -
The forgejo backend serves one PR object at
/commits/{sha}/pullwhere GitHub serves an array at/pulls; both verbs emit the array shape, so the call site carries one expression (#191). -
Forgejo creates tags at
POST /tags— it serves/git/refsGET-only, so GitHub's ref-POST would have 404'd there forever (#191). -
forgejo_api_baserefuses whenREPOis empty. Every verb interpolates it and every call reaches the network through there, sorepos//…— whose 404 reads as "no release" and "no PRs" — is now impossible (#191). -
Release asset names are percent-encoded. The hook contract permits any filename, and the name travels as a query value: a space made curl reject the URL and
&/#/+/%silently renamed the asset (#191). -
lib/forge.sh— the forge selector:forge_detectnames the forge from the runner's own environment,forge_clientnames the client it needs, andforge_preflightrefuses loudly before any sweep when the two disagree (#188). -
The reconcilers and
labels-scoperun that preflight first, so a GitHub-shaped client on a Forgejo instance is a named refusal instead of a sweep that reads nothing and reports success (#188). -
lib/closes_references.sh— the closing-keyword parser, sibling ofrefs_references, so "which issues does this PR close" is answered from a PR body rather than from GitHub's GraphQL API (#188). -
lib/forge-github.shandlib/forge-forgejo.sh— one call surface, two backends, selected byforge_select; no forge branching at the call sites (#188). -
The forgejo backend proves each paginated gather complete against the server's
x-total-countand refuses loudly when it cannot — a missing header is a refusal, not a pass (#188).
Changed
-
docs/CONSUMERS.md's artifact-hook recovery no longer tells operators to rungh release createby hand — on a Forgejo runner there is nogh. It names the forge-neutral tag-door path first, with both clients shown (#191). -
issueflow-reconcilegathers open and merged PRs over REST instead ofgh api graphql. Forgejo serves no GraphQL at all, so the two queries were replaced rather than translated; both forges returnnumberandbodyfrom/pullsin the same shape (#188). -
forge_apiowns the page size, because each forge silently ignores the other's parameter:per_page=100reads 30 items on Forgejo andlimit=100reads 30 on GitHub, both HTTP 200. No call site names one (#188). -
Outstanding review requests are derived from the reviews on the current head rather than from
requested_reviewers, which Forgejo never clears — read raw there, a PR would sit atstate:bots-reviewingforever (#188).
Fixed
-
The release doors run on a Forgejo consumer.
lib/facts.shandrelease.ymlgathered and published throughgh, which the runner image does not ship, so the merge door readlabeled=nofor a correctly labeled ceremony PR and the tag door died at the publish (#191). -
A release fact that could not be read is no longer reported as a definite
no. A completed read finding no label is stillnoand still fail-closed; a read that did not complete refuses and emits no fact (#191). -
labels-scopeno longer fails to compile its jq program on jq 1.6, which the Forgejo runner image ships:labelis a reserved word in jq's grammar, so$labelis a syntax error there and every scope derivation died before reading the config (#188). -
labels-reconcileandlabels-scopeno longer exit 0 on a Forgejo consumer having read zero facts — measured onheavy-duty/rig, where the sweep printedreconciled.over an empty PR list and scope reported "no labeler.yml" for a file that exists (#188). -
forge_timelineprojects Forgejo's label events (.type/.body/.user.login) into the GitHub shape (.event/.actor.login) so the ruling ladder reads the same board on both forges (#188). -
forge_pr_activityno longer calls/pulls/{n}/commentson Forgejo (HTTP 404); inline review comments come from reviews withcomments_count > 0(#188). -
CI installs shellcheck before linting, matching actionlint — the Forgejo runner image does not ship it (#188).
0.4.0 — 2026-07-29
Added
changelog.d/shape— an optional one-line sentinel,flatorgrouped, that pins the fragment set's shape and outranks the newest-published-section inference; absent, the inference binds unchanged (#182).- Add
post-mergeissue state for mergedRefswork awaiting triage-owned verification. changelog_fragment_problembounds every entry at 300 normalized characters, red on the PR that writes the fragment; the armed guard and the assembler inherit the one definition (#167).- BUILDER.md and CHANGELOG.md state the bound and the split rule: a long change ships several short entries, never one long one (#167).
Changed
- Labels automation docs now make sweep cadence a consumer-owned tradeoff, retain hourly as the engine-less default, and document manual dispatch as the operator's immediate full-board sweep (#203).
labels— the reconcile cron relaxes from*/15to hourly (#199), cutting a private consumer's schedule-triggered full-board sweeps ~4× at GitHub's 1-minute billing floor.labels— the hourly cron is the sweep's only wake for transitions no subscribed event carries — a verdict landing, blocker:ci-red, a blocker:conflict when another PR merges, the time-based stale/reclaim — so it bounds their latency to ≤1h, delaying no event-carried transition (#199).labels— the caller'sissues:trigger narrows to[opened, closed, edited, reopened](#199), the actions that carry a queue-state change the cron cannot wait a cadence for. The churn/validation actions — labeled/unlabeled/assigned/unassigned — come off; the PR handoff wake is unaffected.labels— each caller trigger now carries a comment saying why it is subscribed, and reconcile keepscancel-in-progress: false(#199) — cancelling a sweep mid-board is the race that guard exists to prevent.CONTRIBUTING.mdnow points toBUILDER.mdfor the shared PR flow instead of restating doctrine that can drift, while retaining ceremony's roster and other repo-specific facts (#198).- Builder doctrine makes each whole-round reply the durable Round log record mirrored by the engine, leaving handoff as a mechanical facts-only step instead of a newly composed summary (#196).
FLEET.mdremoves its duplicate bench roster, records crew as a general operator-configured tool, and advances its whole-file audit stamp tocrew@eaeb302with every surviving crew link re-pinned (#193).FLEET.mdkeeps the registry's authorization rule and its crew#16/crew#66 provenance, while replacing duplicated mechanism and path claims with a pinned pointer to crew's registry header (#192).BUILDER.mdgates both review-request points on a green check at the head, carries crew#45's argued exception for failures outside the PR, and states the ruled classification: cancelled and stale are not a green head; skipped and neutral are (#189).BUILDER.mddocuments CI-red recovery in pickup precedence: a red head of your own PR is picked up before claiming another issue, is never a parked claim, and follows crew#17's recovery path (#189).FLEET.mdwrites the ci-red wake into the duty order between resume and build, now as deployed engine rather than on paper: the reconciliation stamp advances to the crew SHA carrying crew#64 (#189).FLEET.mddescribes the build wake's check gate as the engine implements it: a green head, or one with no checks configured, opens a round; a red head and an unfinished one are held and reported separately (#189).FLEET.mdcorrects the attention wake to the crew#66 ruling: the query is cross-repo, the action is registry-bounded, and an out-of-scope demand is reported and escalated to the operator rather than worked. It no longer claims attention is exempt from the registry (#189).FLEET.mddistinguishes an attention session that dies before acking, which relaunches, from one that completes without acking, which is a decline a ledger keeps from re-firing (#189).BUILDER.mdre-requests by head, not by verdict: a push while answering a round stales every approval, so every panelist is re-requested; only an unchanged head re-requests the non-approvers alone (#190).- FLEET.md's duty-loop mechanism is a pointer to crew's shared engine; the
wake lists follow the engine's duty order, the roster keeps the as-built
bench beside
fleet.roster's target, and the reconciliation stamp names crew@01fb49c(#187). - Ceremony's changelog is grouped from this release forward: the pending
fragments carry
###headings under agroupedsentinel (#182). - BUILDER.md: a park declaration stands until its facts change — a nothing-changed resumption posts nothing; only a no-open-PR park owes a refresh, inside the 48-hour reclaim window (#178).
- Private-repository label callers document
actions: readalongside checks and statuses for workflow-run check-rollup nodes (#173).
Fixed
- FLEET.md no longer says a review request outside the registry is
authorization:
repos.txtis the scope for the review queue, out-of-scope requests are logged and never acted on, and the attention wake is stated as the one registry-independent exception, by design (#187). blocked_reference_recordsunions everyBlocked byclause in the body instead of binding to the first marker occurrence — a repeated declaration no longer promotes on its first sentence alone, and earlier prose that merely mentions being blocked no longer hijacks the parse (#184).decide_state()refusesstate:needs-humanwhile the hand-setblockedlabel stands — the PR falls tostate:addressing, exactly parallel to theneeds-rulingexclusion; never emitted byblockers()(#180).
0.3.0 — 2026-07-24
- Make
changelog-armedreject fragment shape drift on the PR that introduces it. - A directive hold now has a written ending, not just a beginning: BUILDER.md's shape 5 says the hold ends where it began — on the labels — with the hold owner's most recent queue-label event governing over any stale prose, the timeline read (
gh api .../issues/{n}/timeline) named as the move before standing down or up on a hold, a claim against stale prose required to cite the events it read, and a refused claim given its two exits. TRIAGE.md now requires re-reading label events before asserting label-borne state in prose, and makes correcting a lifted hold's stale body header triage's move in the same tick. On 2026-07-24 the unranked signals split two builders reading one board (#149, #151); both acted defensibly — the doctrine, not the builders, lacked the rule (#154). - Doctrine names the second
Closes #Nexception: a same-repo PR whose authorizing issue marks an acceptance criterion post-merge usesRefs #N, and triage closes the issue by hand on the evidence — merging #143 auto-closed #137 with exactly such a criterion unmet, and no role had been told otherwise. TRIAGE.md now requires a post-merge criterion to carry its own mechanism (post-merge, triage closes,Refs #N), REVIEWER.md listsRefs #NbesideCloses #NandPart of <owner>/<repo>#Nand stops treating the reference-only PR as a defect, and CONTRIBUTING.md points at BUILDER.md as the rule's one home (#151). - FLEET.md — the Reviewers wake describes the deployed sweep, not the
gh searchtrigger the bench replaced: the pulls-APIrequested_reviewerssweep across the org plus the named bot forks is source 1, therepos.txt/search poll an adds-only backstop, and the two are merged and deduplicated by (repo, PR) before acting. Only the notifier'sneeds-rulingqueue remains on paper;repos.txtis the registry only on the triage box; and the Status block now stamps the crew ref the file was last reconciled against (#149). - REVIEWER.md now carries the review mechanics every box had been re-deriving from an incident: the queue comes from the API and not the search index, every write is one-shot per (reviewer, PR, head), heads are reviewed in throwaway checkouts, a pinned consumer's config is verified at its pin, and a verdict names the checks its box could not run (#145).
- The
docs/CONSUMERS.mdlabels-caller stub lists the sameissues:types as ceremony's own caller —editedandreopenedincluded — so a consumer adopting the stub wakes when an issue body'sBlocked by #Ndeclaration is edited, and when a closed issue re-enters the queue wearing labels derived at close. The two lists drifted apart inside PR #32; a parity test now pins them together, red if either file drops a type or the lists diverge. Adopting the widened list is a stub edit riding the pin bump to the first tag carrying this change (#144). labels-reconcile— a queue-cancelled duplicate check is discarded when its context holds a real verdict, so a sibling PR's eviction no longer reds a green PR; an all-cancelled context still blocks (#139).blocker:unrequestednow clears the moment the panel is asked: the labels caller (and thedocs/CONSUMERS.mdstub) listens onreview_requestedandreview_request_removed, so the one event that falsifies the label — or makes it true again — wakes the reconcile sweep instead of waiting for an unrelated push or the advisory cron. Thescopejob skips both events: they change no paths, and running the labeler on them widens the #130 clobber window. Adopting the new triggers is a stub edit riding the pin bump to the first tag carrying this change (#137).drills/README.mdno longer tells the builder to delete the scratch repo — a step no fleet identity can perform, becausedelete_repois deliberately absent from bot tokens. The builder's end state is archive (archived: true, inside thereposcope); the delete is the operator's, and cleanup gates nothing — not ready-for-review, not the panel, not the merge. The drill record now names the scratch repo byowner/nameand states the disposal its author actually observed, never one that has not happened: both 0.2.0 drills hit the missing-scope wall independently, one stalling a release draft on an impossible 403, the other shipping a record asserting a delete that never ran (#135).lib/facts.sh— a repository's first push tomain(a root commit with no first parent) now readsbase_ver=(none)and lets decide's table govern, instead of dying at exit 128 before establishing a fact; the no-base path skips the base fetch andgit show, and an unresolvable head still fails loudly (#134).- The changelog rule now explains why release PRs write no fragment and how entry-worthy changes land instead (#131).
actions/labels-scopereplacesactions/labeler@v5in the labels workflow's scope job: labeler wrote the whole label set (PUT) even undersync-labels: false, silently removing any label applied while it ran — #128 lost itsreleasethat way — so the scope job now derives from the same.github/labeler.ymlmapping (thechanged-files/any-glob-to-any-fileshape, block or flow; anything else refuses loudly) and its only write is an additivePOST. The reconcile sweep also warns — never sets — when a non-draft PR is release-shaped (bare version differing from its base) but carries noreleaselabel (#130).
0.2.0 — 2026-07-24
test/changelog-assembled.test.sh— keep the trio interaction aligned with fragment mode: a dropped entry makes armed red too, while a hand-edited section leaves assembled as the sole red (#126).actions/changelog-assembled— a release PR's stamped section must be byte-for-byte what the fragments it consumed assemble to, replayed from the merge base; inapplicable trees pass with a NOTICE (#116).changelog-armed— treatchangelog.d/as the arming, validate every development fragment, and require bare releases to consume the directory into their exact publishable section (#115).lib/changelog.sh+bin/changelog-assemble— read thechangelog.d/fragments, assemble one release section (canonical group order, one shape per repo), and consume exactly what was published (#114).- BUILDER.md — the directed hold is the parked claim's fifth shape, its attention demand is acknowledged in the declaration comment, and its board bookkeeping covers in-flight work; TRIAGE.md no longer excludes it (#113).
- Ceremony adopts
changelog.d/— a PR writes one fragment per issue instead of editingCHANGELOG.md, the release PR assembles the section, and## Unreleasedis gone (#112). - BUILDER.md — the handed-off PR is the parked claim's fourth shape, its handoff is its declaration, and shape 2 covers the round awaiting its first verdicts (#109).
labels-reconcile— warn once per sweep when a repository lacks labels declared by the pinned core taxonomy (#105).LABELS.md— drop the vendored scope-table enumeration; the per-repo set lives in.github/labels.confand the repo's own CONTRIBUTING (#104).labels-reconcile— a degraded mergeability/checks read now logs gh's actual stderr (collapsed, bounded) beside the byte-identical counted line, and the blind-sweep warning leads with the observed reason instead of asserting the permissions cause (#101).- Changelog publication — count entries instead of bytes, refuse dangling grouped headings, and seed grouped re-arms with Added/Changed/Fixed (#98).
labels-reconcile— grant callers private-repo check reads and warn when an entire PR sweep is blind (#95).labels-reconcile— the bootstrap now retires the six GitHub defaultsLABELS.mdpublishes as deleted, tolerating both an already-absent label and a refused delete (#93).issueflow-reconcile— a triage-authored issue arrival stands down with exit 0 instead of killing the run before the sweep (#91).- FLEET.md — the assignee's
attentionwake: one role-independent trigger ahead of every per-role list, one acked session per demand; a spec on paper untilduty.shpolls it (#86). attentiondoctrine — define its assignee-owned pickup, ack, queue and clock semantics across labels, triage, and builder roles (#85).attention— add the issue-only, hand-set assignee-demand flag to the core label taxonomy (#84).- One issue at a time counts build work in flight: the parked claim's three shapes, its declared-never-inferred comment, and triage's duty to name a directed hold as a park (#77).
- FLEET.md — the operator notifier's
needs-rulingqueue (one tracked message per item, edited in place across the rungs) and triage's past-24h wake condition; a spec on paper until an operator updates the box (#74). - The sweep observes the escalation contract: a malformed escalation is named field-by-field, and the ladder's 12h/24h rungs each draw one comment to the flag-setter — comment-only, per-episode, both surfaces (#73).
- Ruling doctrine — define every human-owned trigger, the fixed escalation shape, and the 0–24h builder-to-triage ladder (#72).
issueflow-reconcile— nudge once when anoffsiteflag outlives every visible cross-referenced PR (#69).offsite— protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68).issueflow-reconcile— keep cross-repo references out of local dependency decisions and require triage to resolve cross-repo blockers by hand (#61).actions/runner-isolated— apull_request-triggered job may never run on a self-hosted runner (#58).- Cross-repo doctrine: the panel is the PR's repo's roster, a review request is authorization but not panel membership, and
Part of <repo>#Nreplaces theCloses #Nthat cannot cross repos (#57). - The sweep's
needs-rulinginvariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52). needs-ruling— the cross-cutting flag for a pending human decision, excluded fromstate:needs-humanand from the staleness sweep (#51).
0.1.0 — 2026-07-22
lib/version.sh— one version abstraction,fileandpackage-jsonbackends (#3).lib/changelog.sh+bin/changelog-section— the one canonical changelog-section extractor (#4).actions/changelog-armed— the version-keyed arming guard (#5).actions/changelog-monotonic— shipped release headings are append-only: no deletion, no duplication (#6).actions/drill-recorded— a release tree must carry its drill record (#7).lib/decide.sh— the merge door's five-state decision, pure and exhaustively tested (#8)..github/workflows/release.yml+lib/facts.sh— the reusable two-door release workflow (#9)..github/workflows/labels.yml+actions/labels-reconcile— label taxonomy bootstrap and PR-state reconciliation (#10).- Ceremony adopts its own ceremony:
VERSION, this changelog, the drill doctrine, the self-callers, and the self-guards in CI (#11).