Skip to content

docs(agents): the Playwright specs have been in CI since 2026-08-08 - #768

Merged
mforce merged 1 commit into
mainfrom
docs/767-playwright-ci-coverage
Sep 11, 2026
Merged

mforce merged 1 commit into
mainfrom
docs/767-playwright-ci-coverage

Conversation

@mforce

@mforce mforce commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Closes #767

AGENTS.md's #394 rule and its decision record both state that tools/simulation/k6/ and the
Playwright specs in tools/simulation/ui/ are uncovered by CI. Half of that stopped being true on
2026-08-08, and it is the half a reader is most likely to act on.

The evidence

.github/workflows/e2e-smoke.yml runs on: pull_request, path-filtered to src/**, web/**,
tools/simulation/**, deploy/**, the three root image-build inputs and the workflow itself. Its own
header dates the change:

The quick suite runs ON PULL REQUESTS (owner call, 2026-08-08, revising the …)

A write-contract change lives in src/ by definition, so the Playwright specs run for exactly the
class of change this rule governs. Roughly 28 specs across 14 files, about three minutes. Only slow
(the real 15-minute token-expiry spec) and canary stay dispatch-only, and a docs-only PR skips the job.

.github/workflows/k6-baseline.yml is still workflow_dispatch: only, so k6 is genuinely
uncovered
, exactly as the rule says. Everything about its tolerated-status list letting a broken
write pass against a 100% green baseline is unchanged, and it is the sharper trap of the two.

Observed rather than inferred: on #766 the check "Playwright smoke over the simulation fixture" ran
and passed, and that run included worker-sale-allocation.spec.ts, which carries no slow or canary
tag. The decision record was the more specific and more wrong of the two — it said the specs were
"workflow_dispatch only, #385".

Why it is worth correcting

The error points the safe way: it makes you verify by hand something CI already covers, so nothing
has shipped broken because of it. The cost is misdirected effort. On #727 the Playwright half of the
#394 caller read was done by hand under the belief that a green baseline would hide a break, when CI
would have caught it. It also flattens the one caller that genuinely still needs reading into a list of
two, which makes it easier to skip.

It is a trap in the other direction too. Anyone who checks the workflow, sees Playwright is covered and
concludes the whole bullet is stale will stop reading the k6 callers as well.

What changed

AGENTS.md — the bullet now separates the two, names the mechanism rather than the state (since the
state has moved once already), and keeps the rule's durable half: verify by reading, because CI can
tell you a spec broke but not that a caller's intent changed.

docs/decisions/394-write-contract-callers.md — a dated amendment rather than a rewrite. What the
record said was true when it was written, and a decision record that quietly reflects later facts stops
being a record. The original paragraph keeps its one factual correction (k6-baseline.yml named
explicitly, the false Playwright clause removed) and the amendment explains what moved and when.

Not in scope

Whether the path filter should also cover a docs-only PR that changes a spec's expectations. It should
not, but that is a separate argument from this correction.

AGENTS.md's #394 rule and its decision record both said tools/simulation/k6/
and the Playwright specs in tools/simulation/ui/ were uncovered by CI. Half of
that stopped being true: e2e-smoke.yml runs the quick suite on pull_request,
path-filtered to src/**, web/**, tools/simulation/** and deploy/**, so a
write-contract change — which lives in src/ by definition — does run them.

k6 is still workflow_dispatch only, and everything about its tolerated-status
list hiding a broken write is unchanged.

The error pointed the safe way, so nothing shipped broken. It cost misdirected
effort: on #727 the Playwright half of the caller read was done by hand under
the belief a green baseline would hide a break. It also flattened the one
caller that still needs reading into a list of two.

The decision record gets a dated amendment rather than a rewrite, because what
it recorded was true when written.

Closes #767
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 392ad4d3-46db-48aa-be88-14deee68ba0d


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mforce
mforce merged commit 68ee612 into main Sep 11, 2026
9 checks passed
@mforce
mforce deleted the docs/767-playwright-ci-coverage branch September 11, 2026 20:20
mforce pushed a commit that referenced this pull request Sep 12, 2026
🤖 I have created a release *beep* *boop*
---


## [0.1.0](v0.0.4...v0.1.0)
(2026-09-12)


### ⚠ BREAKING CHANGES

* log in by farm code, with per-account email identity
([#532](#532)) (#564)

### Features

* **accounts:** add Account.Slug (farm code), suspend/reactivate,
list-accounts verb
([#531](#531))
([3fe9754](3fe9754))
* **accounts:** provision additional farms
([#581](#581))
([006f298](006f298))
* add Aspire local development AppHost
([#567](#567))
([2c9e6b9](2c9e6b9))
* add configurable worker sale allocation
([#619](#619))
([0955095](0955095))
* add searchable entity pickers
([#642](#642))
([60d2053](60d2053))
* **api:** provision-account takes an optional --timezone at creation
([#603](#603))
([#694](#694))
([a0aee39](a0aee39))
* **audit:** show the sales-line audit payload as a readable Details
column ([#745](#745))
([#749](#749))
([d26d389](d26d389))
* **auth:** add ApplicationUser.StepUpLogoutEpoch column
([#338](#338))
([#554](#554))
([18306ee](18306ee))
* certify over-cap simulation fixture bands
([#633](#633))
([a67b2e1](a67b2e1)),
closes [#627](#627)
* **cli:** rename-account verb to change a farm code
([#732](#732))
([#733](#733))
([4b70559](4b70559))
* **customers:** edit existing customer details
([#625](#625))
([#626](#626))
([062a55c](062a55c))
* **jobs:** single-runner leader gate for the durable job worker
([#271](#271))
([#555](#555))
([4148f9b](4148f9b))
* let owners change user email addresses
([#605](#605))
([842347b](842347b))
* log in by farm code, with per-account email identity
([#532](#532))
([#564](#564))
([68adb62](68adb62))
* **ratelimit:** distributed IP-keyed auth limiters
([#544](#544))
([#558](#558))
([ec14972](ec14972))
* **ratelimit:** distributed per-account report concurrency cap with
local-ceiling fallback
([#545](#545))
([#559](#559))
([1522e4e](1522e4e))
* **sales:** mark discounted lines, total the discount, and show it in
the Orders list ([#723](#723),
[#724](#724))
([#741](#741))
([1a07441](1a07441))
* **sales:** record list, old and new price in the order-line audit
payload ([#722](#722))
([#742](#742))
([97c866f](97c866f))
* **sales:** refuse an over-ceiling confirm from a Sales user
([#727](#727))
([#766](#766))
([8c0792a](8c0792a))
* **sales:** show what each order still owes, and filter the list to
unpaid ([#771](#771))
([ca59d68](ca59d68))
* **sales:** snapshot the list price on the order line and show the
discount ([#734](#734))
([cffed5e](cffed5e))
* **sales:** snapshot the product name and unit in the order-line audit
payload ([#747](#747))
([#748](#748))
([0481c06](0481c06))
* scope Worker reads to assigned flocks
([#388](#388))
([#611](#611))
([5884a9a](5884a9a))
* shared-state ports with Redis + in-process fallback
([#543](#543))
([#552](#552))
([f767fa9](f767fa9))
* suspend-account / reactivate-account operator verbs
([#534](#534))
([#573](#573))
([d0be26c](d0be26c))
* **tenancy:** write-side tenant guard + single-assignment TenantContext
([#546](#546))
([#561](#561))
([f371f1d](f371f1d))
* **web:** dashboard rework — capture-status tiles, 14-day trend, stock
as a stacked bar
([#654](#654))
([396ba23](396ba23))
* **web:** date-range filters on audit and expenses, and the stock lot
filter gets its bounded toolbar
([#666](#666),
[#667](#667),
[#653](#653))
([94b188f](94b188f))
* **web:** elevation hierarchy and sentence-case labels
([#651](#651),
[#652](#652))
([#661](#661))
([28db4c7](28db4c7))
* **web:** Expenses and Audit keep a clear-filters control while rows
are still showing
([#679](#679))
([#697](#697))
([b859982](b859982))
* **web:** expenses filters by a date range like its sibling screens
([#667](#667))
([f13858f](f13858f))
* **web:** key the farm brand palette per farm
([#586](#586))
([#600](#600))
([7183a43](7183a43))
* **web:** let operators forget remembered farms
([#598](#598))
([577d94e](577d94e))
* **web:** one-line provenance, bounded date filters, and empty states
that invite action
([#653](#653),
[#655](#655))
([#668](#668))
([80b53f4](80b53f4))
* **web:** prefill the farm code from ?farm= and remember it
([#535](#535))
([#588](#588))
([b7f5cc6](b7f5cc6))
* **web:** split authenticated routes into lazy chunks
([#620](#620))
([5089271](5089271))
* **web:** the audit log filters by a date range, and says which window
is empty ([#666](#666))
([63027e0](63027e0))
* **web:** typeset numbers as numbers and refresh the Help glossary
([#650](#650),
[#657](#657))
([af4fe11](af4fe11))


### Bug fixes

* **api:** order same-instant audit events by a durable monotonic key
([#700](#700))
([8fcf084](8fcf084))
* **api:** print the farm code from bootstrap-admin
([#589](#589))
([#594](#594))
([34032ac](34032ac))
* **audit:** show the price a line sold for, not its list price
([#759](#759))
([e6b37d0](e6b37d0))
* **audit:** store catalog enums by name and guard the add-item
transaction shape
([#751](#751))
([23609ff](23609ff))
* **auth:** reject invalid account claims
([#622](#622))
([8d6c7fe](8d6c7fe))
* **auth:** require step-up for durable user access
([#360](#360))
([#607](#607))
([f767dce](f767dce))
* **ci:** bound the npm audit calls and give the web job room to finish
([#686](#686))
([153b7a8](153b7a8))
* **ci:** escalate the audit bound to SIGKILL, so it actually bounds
([#686](#686))
([a0c8f4e](a0c8f4e))
* **ci:** fail closed on invalid vulnerability config
([#621](#621))
([1690db8](1690db8))
* **ci:** lockfix covers the two AppHost lock files, derived from the
sln
([efb05e6](efb05e6))
* **ci:** lockfix covers the two AppHost lock files, derived from the
sln
([8986d77](8986d77))
* **ci:** remove invalid XML comment from nuget.lockfix.config
([#541](#541))
([5f1bc0a](5f1bc0a))
* **ci:** the advisory vuln gate no longer blocks on an unusable report
([#686](#686))
([aaf6934](aaf6934))
* **ci:** the advisory vuln gate no longer blocks on an unusable report
([#686](#686))
([64f1f53](64f1f53))
* **i18n:** tl help text names the saleable flag and unit-system setting
what their labels call them
([#688](#688))
([#696](#696))
([bfd24d7](bfd24d7))
* **infra:** AccountId must be a non-nullable Guid or both tenant write
layers refuse ([#673](#673))
([#695](#695))
([2470c4e](2470c4e))
* require step-up for flock scope changes
([#609](#609))
([4151f89](4151f89))
* **sales:** keep a line's discount markers agreeing while its price is
edited ([#752](#752))
([#753](#753))
([c159b4b](c159b4b))
* **sales:** say which kind of missing list price a line has
([#774](#774))
([489180e](489180e))
* scope legacy logout to selected farm
([#624](#624))
([fae8d82](fae8d82))
* **seed:** drain the daily-entry lock sweep so deep simulation fixtures
validate ([#644](#644))
([730fa23](730fa23)),
closes [#638](#638)
* **tenancy:** AccountId is a concurrency token, so the database refuses
a detached cross-tenant write
([#562](#562))
([4d1dfa3](4d1dfa3))
* **tenancy:** AspNetUserRoles carries a tenant column, so a role write
naming another farm's user is refused
([#670](#670))
([fc0552a](fc0552a))
* **tests:** bump the image-pin allow-list counts for the AppHost
LocalPorts tests
([#593](#593))
([58d3056](58d3056))
* **tests:** the OTLP collector survives a lost port race and ignores
traffic that is not an export
([#672](#672),
[#676](#676))
([#677](#677))
([965c737](965c737))
* **web:** a scoped audit view filtered to nothing names both the record
and the range ([#666](#666))
([41bbfe1](41bbfe1))
* **web:** an abandoned dialog attempt's success no longer hijacks the
replacement on Customers, Daily Entry, Flocks, Grades and Products
([#703](#703))
([#705](#705))
([85605db](85605db))
* **web:** an abandoned dialog attempt's success no longer hijacks the
replacement on Inventory, Expenses, History and Stock
([#703](#703))
([#706](#706))
([60a4997](60a4997))
* **web:** an abandoned edit's success no longer hijacks the dialog that
replaced it on Users
([#703](#703))
([#710](#710))
([778faab](778faab))
* **web:** an abandoned order attempt's success no longer hijacks the
dialog that replaced it
([#702](#702))
([522c699](522c699))
* **web:** capture screens open on the flock you last used, and
assigning one no longer guesses
([#646](#646))
([#699](#699))
([7f8f317](7f8f317))
* **web:** constrain dialog session helpers to declared scopes
([#715](#715))
([389e3c8](389e3c8))
* **web:** date validation gets one boundary table instead of one case
per review round
([#666](#666))
([215f830](215f830))
* **web:** keep a paged window and an item panel on the user's newest
intent ([#645](#645))
([d81bccf](d81bccf))
* **web:** keep Sales order panels closed after pending writes
([#711](#711))
([f0f7492](f0f7492))
* **web:** keep Sales panels closed after pending Open reads
([#716](#716))
([620411f](620411f))
* **web:** make login take the cross-tab cookie lock so a racing refresh
cannot restore the wrong session
([#648](#648))
([ff18beb](ff18beb))
* **web:** make the entity picker read as a search field and focus it on
open ([#736](#736))
([66ef667](66ef667)),
closes [#735](#735)
* **web:** page truncated customer and movement tables with usePagedList
([7cfe4d6](7cfe4d6))
* **web:** reconcile Sales line edits with refreshed orders
([#717](#717))
([d7dd2c9](d7dd2c9))
* **web:** the audit date filter accepts low-numbered years, and its
empty state covers every narrowing
([#666](#666))
([af52d25](af52d25))
* **web:** the audit date filter rejects impossible dates, and its
history guard actually guards
([#666](#666))
([8d51846](8d51846))
* **web:** the expense range bounds are not capped at today, which the
month-end default exceeds
([#667](#667))
([7e01864](7e01864))
* **web:** the help text calls the expiry field what the field calls
itself ([#666](#666))
([2fd1f3c](2fd1f3c))
* **web:** the stock lot date range sits in the bounded toolbar
([#653](#653))
([43dec5e](43dec5e))


### Refactoring

* **web:** extract SalesPage's dialog-write wrapper into a shared
useDialogAction hook
([#703](#703))
([#704](#704))
([60ee9d9](60ee9d9))


### Documentation

* add k6 preparation steps to the dev-database fixture runbook
([#643](#643))
([a4f1f09](a4f1f09))
* add runbook for loading the simulation fixture into a dev database
([#639](#639))
([2d143b8](2d143b8))
* **agents:** a PR closes its issue from the body, not the title
([#744](#744))
([39be13c](39be13c))
* **agents:** drop the commit and push gate, and require screenshots on
UI changes ([#757](#757))
([6225172](6225172))
* **agents:** find guards by grepping registry readers; amend issues a
PR overtakes ([#580](#580))
([fe3fde8](fe3fde8))
* **agents:** the Playwright specs have been in CI since 2026-08-08
([#768](#768))
([68ee612](68ee612))
* **aspire:** record the second local database and pin the AppHost
dashboard ports ([#623](#623))
([713b941](713b941))
* compress AGENTS.md to one paragraph per rule, and draw the two orders
that matter ([#551](#551))
([997ae8a](997ae8a))
* item 7 names each screen's actual initial filter value
([#666](#666))
([70a53d8](70a53d8))
* multi-farm tenancy decision record and AGENTS/GLOSSARY sync
([#537](#537))
([#601](#601))
([2c34771](2c34771))
* name the scoped filtered-empty key and state the
[#653](#653) relationship
plainly ([#666](#666))
([0e93dac](0e93dac))
* note that a PackageReference in Directory.Build.props is invisible to
the dependency graph
([4845724](4845724))
* **plans:** commit the
[#722](#722) and
[#745](#745) design records
([#754](#754))
([c942fcd](c942fcd))
* record [#579](#579) as
won't-fix — suspension is immediate for use, not issuance
([#582](#582))
([7a3be40](7a3be40))
* record the [#508](#508)
audit ordering key and the tracked-file guard lesson
([#701](#701))
([08964e9](08964e9))
* **runbooks:** add procedure to rename the default farm's code after
upgrade ([#731](#731))
([2f6e242](2f6e242))
* screenshots of the running SPA in the README
([#550](#550))
([711488a](711488a))
* **sim:** commit the dashboard screenshot, capture the palette matrix,
and record the
[#651](https://github.com/mforce/cluckwork/issues/651)/[#652](https://github.com/mforce/cluckwork/issues/652)
conventions ([#660](#660),
[#662](#662),
[#663](#663),
[#664](#664))
([#665](#665))
([930ea30](930ea30))
* specify searchable entity picker
([#641](#641))
([91d4300](91d4300))
* split the README into audience-scoped docs and adopt repo-template
scaffolding ([#548](#548))
([b3f3fcf](b3f3fcf))
* surface Aspire local development workflow
([#568](#568))
([a343baa](a343baa))
* **web:** record the per-screen idempotency-key policies and runWrite's
refresh contract
([#703](#703))
([#707](#707))
([8bee651](8bee651))
* **web:** the date-cap help text covers every stocked item, not only
feed ([#666](#666),
[#667](#667))
([c8433c5](c8433c5))
* **web:** the help text claims only what is true of recording, and says
nothing about filter caps
([#666](#666),
[#667](#667))
([e2f63d1](e2f63d1))
* **web:** the help text describes the date-range filters that shipped
([#666](#666),
[#667](#667))
([c3275b7](c3275b7))
* **web:** the help text stops describing a cap the filters no longer
have ([#666](#666),
[#667](#667))
([49654cd](49654cd))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
mforce added a commit that referenced this pull request Sep 12, 2026
…uests (#783)

Skips `Image build + Trivy scan` and `Web typecheck, test, and build` on
pull requests that change nothing but documentation. `Build and test` is
untouched, because four of its guards are load-bearing on a
markdown-only change.

Closes #782

## The question is inverted, on purpose

The gate does not ask "which jobs does this change need". That is a
hand-maintained list of what someone thought of, which is the shape
`AGENTS.md`'s guard rules tell you to avoid. It asks **"does this pull
request contain literally nothing but documentation"**, so a path nobody
has classified is code by default and the full suite runs.

That also resolves the issue's decision 3, which warned that deriving
one shared trigger condition for both jobs would be the easy mistake. It
would be, for a *positive* trigger set. For this predicate a single
shared condition is correct by construction, because "contains nothing
but documentation" is safe for both jobs at once.

## Fail-closed in one direction only

A wrong `false` costs four minutes of runner time. A wrong `true` skips
the image build and the Trivy scan on a change that needed them. So
every failure path answers `false`:

- The classify step short-circuits to `docs_only=false` on any event
that is not `pull_request`, without consulting git.
- It runs without `set -e`, and a git failure, a node failure or an
unreadable stdin all land on `docs_only=false` with the step still
green.
- The jobs test `needs.changes.outputs.docs_only != 'true'`, not `==
'false'`. If the gate produces no output at all, the jobs run.
- `!cancelled()` is what lets that condition be evaluated when the gate
job itself failed.

## `publish` cannot be starved of an image

`publish` declares `needs: [build-and-test, web, image]`, and per #351 a
merge that produces no image leaves a release draft that can never be
promoted. Two separate protections, because the first one alone was not
enough:

The gate never fires outside `pull_request`, so a push to `main` runs
`web` and `image` exactly as before. And `changes` carries
`continue-on-error: true`, because `publish`'s `if:` uses no status
function and therefore carries an implicit `success()` over its whole
ancestor chain — so a `changes` job that failed for any reason would
have skipped `publish` on `main` even though `web` and `image` ran and
passed. That hole was introduced by adding `needs: [changes]` and is
closed by making the gate unable to fail.

The classifier's self-tests therefore run in their own
`classifier-self-test` job with no dependents, since a
`continue-on-error` job cannot fail a run and a guard that cannot fail a
run is not a guard.

`ci.yml`'s diff is 134 added lines and **zero removed**. `publish`'s
`needs` and `if:` are byte-identical.

## Three corrections found before this was pushed

The diff went to an adversarial reviewer first, per the "Writing a
guard" rule. It broke the first design in three places, each verified
against the repository rather than accepted on argument.

**`specs/**` is not documentation here.**
`web/src/routes/helpGlossary.test.ts:23` reads
`../specs/product/GLOSSARY.md` from disk and fails when a spec term is
renamed out from under the in-app glossary (#657). That test runs in the
`web` job, one of the two this gate skips, so a specs-only pull request
would have skipped the guard written for specs-only pull requests.
Carving out that single file would work today and fail silently the
first time a second web test reads a second specs path, so the whole
tree is code.

**`--name-only` hides a rename's source.** `diff.renames` defaults to
true, so `git mv src/Cluckwork.Domain/Common/Result.cs docs/Result.cs`
arrives as the single path `docs/Result.cs` and classified as
documentation while a source file was deleted. Measured on this
repository, not reasoned about. `--no-renames` is now in the workflow,
an end-to-end test performs that exact `git mv`, and a second test reads
`ci.yml` and asserts the flag is on the classifying `git diff`, because
the module and the workflow each held a copy of that contract.

**The gate reopened the publish hazard**, covered above.

## Verification

16 `node:test` cases, and 13 mutations applied by script, run, and
restored with a byte-identical diff check. Every mutation went red and
every test went red under at least one. They include `every` to `some`
(the "are any docs changed" bug the issue names), dropping the
empty-list check, dropping the trailing slash so `docsomething/x.cs`
matches `docs/`, letting any `*.md` count as root documentation, and
removing `--no-renames` from `ci.yml`. One earlier mutation survived,
and the code was fixed rather than the claim: the unreadable-stdin catch
was unreachable through an async iterator, so the CLI now reads fd 0
with `readFileSync`.

Against real history: PR #768 answers `true`, PR #779 answers `false`
(it is the mixed case, carrying `AGENTS.md` and two `docs/` files beside
lock files and tooling), and release PR #542 answers `false` because
`version.txt` is not root markdown. Six of the seven documentation-only
PRs the issue measured answer `true`; the seventh is #718, ten
`graphify-out/` files, excluded deliberately.

The push path was demonstrated rather than argued: the classify step's
`run:` block was extracted from the parsed YAML and executed with the
event name forced, and `push` and `workflow_dispatch` both write
`docs_only=false` and exit 0 without touching git.

## One question the issue asked that could not be answered

It named "establish which status checks are required" as the blocking
first step. It could not be read: this repository has no rulesets and
classic branch protection returns 403 for a personal access token. It
turns out not to matter, and the decision record says why. A job skipped
by `if:` reports as *skipped*, which satisfies a required check, whereas
a workflow skipped by `on.pull_request.paths` leaves its check in
`Expected` forever and blocks the merge. The mechanism chosen here is
safe either way, so the unknown is dissolved rather than deferred.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **CI Improvements**
- Documentation-only pull requests now skip the web and image CI jobs,
reducing validation time.
- Changes are classified conservatively; ambiguous or failed detection
continues to run the affected checks.
  - Build and test validation remains enabled for all changes.

- **Documentation**
- Added a decision record describing documentation-only pull request
handling, its scope, and safeguards.
  - Updated the decisions index with the new record.

- **Tests**
- Added comprehensive coverage for documentation-change detection and CI
behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mforce <cleyva@clvc.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AGENTS.md #394 says the Playwright specs are not in CI; they have been since 2026-08-08

1 participant