Commit b4a6d42
Finish Gnosis registration prep + chain-scoped observeOnly (directive-100/101) (#54)
* feat(mech-adapter): finish Gnosis registration prep + chain-scoped observeOnly (directive-100/101)
register-live.ts gains --chain (base default/gnosis), reusing CHAINS from
directive-97/98 rather than hardcoding Base's RPC/chain id. The
passphrase-gated signing path is untouched -- this only changes which
chain's RPC/contracts a Forces-run signed call targets. Fresh dry-run
confirms it resolves to the same already-verified create() target
(predicted serviceId 3789 on real Gnosis state).
config.ts/main.ts: loadChainId()/loadMechIdentity() let the production
entry point run against a chain other than Base without touching Base's
default behavior -- verified byte-identical when no new env vars are set
(same chain id, same hardcoded mech/multisig addresses, same RPC). On any
non-Base chain, mech/multisig addresses are required via env and fail
closed if missing, since they don't exist until that chain's real
ServiceRegistry lifecycle actually completes.
New grey-mech-adapter-gnosis.service is the actual fix for directive-100
Sec1.3's finding: MECH_ADAPTER_OBSERVE_ONLY was one flag on one shared
process. Entirely separate unit, own EnvironmentFile, own observeOnly
value -- flipping Gnosis's can never touch Base's live one. Not installed
on the VPS yet: real MECH_ADDRESS/MECH_MULTISIG_ADDRESS don't exist until
the pending registration lands, and loadMechIdentity() fails closed
without them, so there's nothing to meaningfully start yet.
vitest run: 91 passed, 2 skipped, 0 failed. tsc --noEmit clean on all
three gated configs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* fix(mech-adapter): urgent -- register-live.ts had no way to resume a real serviceId across restarts (directive-103)
D-101's --chain gnosis support hardcoded EXISTING_SERVICE_ID = undefined
for any non-Base chain. Real, live consequence: Forces' terminal closed
between two runs, the script forgot serviceId 3789 (real create(), tx
0xb780b849...) and created a second, orphaned service 3790 (tx
0x8c6d25b0...) instead of resuming. No funds harmed (create() sends no
value), but a real gap.
Adds registrationResume.ts's resolveExistingServiceId: a --service-id
<n> flag (validated against getService before trusting it -- fails
closed on NonExistent), plus a balanceOf-based safety check when no
default/flag is given. ServiceRegistryL2 supports balanceOf but not
owner-indexed enumeration (tokenOfOwnerByIndex reverts, confirmed live),
so the check can only report a count -- enough to abort the exact
silent-duplicate-create scenario that produced 3790, not enough to
recover which id to resume, hence the flag is still required. Base is
unchanged: hardcoded default 635n still wins when no flag is passed.
FINAL SUMMARY output now says RESUMING vs CREATING A NEW SERVICE
explicitly, and the abort happens before the passphrase prompt.
Both real paths proven per directive-103's own instruction, via mocks
(no real chain touch) in test/registrationResume.test.ts, then
independently re-checked against real current Gnosis state: omitting
--service-id now correctly aborts (balanceOf=2, would have prevented
the real 3790 duplicate); --service-id 3789 correctly resolves to
resume.
3790 is a real, confirmed orphan -- left untouched per directive-103
Sec2, no cheap way to reclaim/delete it and no further cost from
leaving it in PreRegistration.
Next real command for Forces: pnpm register:live -- --chain gnosis --service-id 3789
vitest run: 97 passed (91 + 6 new), 2 skipped, 0 failed. tsc --noEmit
clean on all three gated configs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* fix(mech-adapter): urgent -- deploy() and createMech hardcoded Base's multisig/factory regardless of chain (directive-104)
Real, live consequence: Forces ran register-live.ts --chain gnosis
--service-id 3789 for the deploy step. Pre-flight simulateContract
reverted UnauthorizedMultisig(0x22bE...B83) -- caught before broadcast,
no funds touched, service 3789 unchanged at FinishedRegistration.
Root cause: mechAdapter.ts:709 read SERVICE_REGISTRY_ADDRESSES
.gnosisSafeMultisig directly, Base's hardcoded constant, regardless of
which chain the adapter is actually running on. D-97/98's
chain-parameterization covered the client constructors but missed this
direct import inside mechAdapter.ts's deploy path.
Audit (per this directive's own instruction) found a second, not-yet-hit
gap of the same shape: mechAdapter.ts:722's createMech step read
MARKETPLACE_ADDRESSES.factories[paymentType] directly -- would have
broken the very next real step past deploy, the same way.
Fix: ServiceRegistryClient now exposes gnosisSafeMultisig (resolved from
its own chainId at construction, mirroring how it already resolves
serviceManagerProxy/serviceRegistryL2). MarketplaceClient now exposes
getFactoryAddress(paymentType), same pattern, failing closed if the
requested payment type has no factory on that chain (Gnosis has 3
factories, not Base's 5 -- confirmed D-97). mechAdapter.ts reads both
from the injected clients instead of importing the bare Base constants,
which are no longer imported into this file at all.
Both paths proven (Base unchanged, Gnosis now correct) per directive-104
Sec2's own instruction: test/chainParameterization.test.ts -- client-level
resolution for both chains, plus two mechAdapter-level tests proving
runDeployStep/runCreateMechStep actually read from the injected clients
(using deliberately distinctive fake values that would fail if the code
regressed to importing the hardcoded constants again).
Also independently re-confirmed against real, current Gnosis state (read-
only, no key material): the same deploy() call that reverted now
simulates cleanly, predicting multisig 0x5587335a6Fa1Dc7C421f2b87D91C7E9def095872
-- notably, identical to Grey's already-deployed real Base multisig,
plausible given deterministic CREATE2 Safe-proxy addressing, flagged
plainly rather than silently noted.
vitest run: 104 passed (97 + 7 new), 2 skipped, 0 failed. tsc --noEmit
clean on all three gated configs.
Next command for Forces is unchanged: pnpm register:live -- --chain gnosis --service-id 3789
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* fix(infra): point Gnosis systemd unit at isolated staging checkout, not shared main (directive-106)
Base's live grey-mech-adapter.service and all other grey-* units share
one checkout at /opt/grey/grey, still on main pre-arc. Repointing this
unit's WorkingDirectory at a separate, isolated checkout
(/opt/grey/grey-gnosis-staging, checked out to this same PR branch)
means deploying it touches zero shared state -- no merge required, no
risk to Base's live process on any future restart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* fix(mech-adapter): main.ts never injected a chain-aware marketplaceClient (directive-106)
Third instance of the D-104 bug class, caught live during the Gnosis
staging deploy: MechAdapter.start() called numMechs() against Base's
marketplace proxy address while running on Gnosis's own RPC ("returned
no data"). Root cause: MechAdapter's constructor default
(createMarketplaceClient(rpcUrl), no chainId) silently resolves to 8453
whenever no marketplaceClient is injected -- main.ts never injected one,
on any chain, since it was first written.
Fix: main.ts now constructs createMarketplaceClient(config.rpcUrl,
undefined, chainId) explicitly and passes it in. register-live.ts was
already correct (constructs both clients with chainId explicitly,
directive-103).
vitest run: 104 passed, 2 skipped, 0 failed. tsc --noEmit clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* fix(mech-adapter): requiredAddress()/loadMechIdentity never checksum-normalized addresses (directive-106)
Fourth real bug caught live in the same Gnosis staging deploy:
MechAdapter.start()'s checkMech() call threw "Address must match its
checksum counterpart" -- the real MECH_ADDRESS env value (copied
directly from D-105's mail field) was shape-valid but not in strict
EIP-55 checksum case. requiredAddress() only ever validated hex shape
via regex, never checksum, so a wrongly-cased address slipped through
silently and only failed deep inside viem's own strict enforcement, far
from where the real problem (a manually-transcribed env value)
originated.
Fix: requiredAddress() now runs every resolved address through viem's
getAddress(), which accepts any validly-shaped input and normalizes to
the correct checksum -- safe for the many already-correct addresses
already in production, and closes this specific gap for any future
env-provided address regardless of how it's capitalized. Also fixed
loadMechIdentity's Base-override path, which bypassed requiredAddress
entirely.
vitest run: 106 passed (104 + 2 new), 2 skipped, 0 failed. tsc --noEmit
clean on both gated configs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* docs(mech-adapter): author Gnosis's off-chain offering-set metadata (directive-107 e3-g2)
service-config-gnosis.json/mech-payload-gnosis.json mirror Base's real
tool/pricing content (prediction_market_research/resolution_evidence_
compiler, CACHE_ONLY 0.65x), noting the real 3 payment types Gnosis
actually has (NATIVE/TOKEN/NVM_NATIVE, not Base's 5 -- directive-97).
Descriptive only, explicitly marked as such in both files: service
3789's real on-chain configHash stays Base's own, permanently, per
Forces' directive-106 decision (GREY_DID is one cross-chain identity
anchor by design, and ServiceRegistryL2.update() requires
PreRegistration state -- already closed for 3789 regardless). Added
the same note to GREY_MECH_REGISTERED_TOOLS's own doc comment in
config.ts, the closest thing this codebase has to a documented offering
set for a human reader, so nobody mistakes the permanent Base hash for
a bug later.
No new IPFS pin yet -- would need the VPS's Filebase credentials,
which this pass didn't touch; these files are ready to pin whenever
that's wanted, but pinning isn't required for anything real to work
since the on-chain hash never references them.
tsc --noEmit clean, vitest run 106 passed unchanged (comment-only
config.ts change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* Revert "docs(mech-adapter): author Gnosis's off-chain offering-set metadata (directive-107 e3-g2)"
This reverts commit bac3221.
* fix(mech-adapter): urgent -- register-live.ts's createMech step never encoded a real delivery rate (directive-110)
Real, live consequence: Grey's Gnosis mech (0x1A235555..., real
serviceId 3789) is permanently unable to receive a valid payment.
maxDeliveryRate() reads back byte-for-byte identical to
GREY_MECH_PAYLOAD_HASH -- the IPFS metadata hash, reinterpreted as a
uint256, is astronomically large. Exact same bug as directive-51 on
Base, which D-53/55 only ever worked around (a second, corrected mech)
without fixing this script -- so it reproduced identically the first
time this script's createMech step actually ran on a second chain.
Root-cause fix, register-live.ts: a required-when-needed --delivery-rate
<wei> flag; the createMech payload is now genuinely
encodeAbiParameters(['uint256'], [deliveryRateWei]), never the raw
hash. If the resolved next step turns out to be createMech and no
--delivery-rate was given, this now fails closed with an explicit error
before the REGISTER prompt -- extracted as registrationResume.ts's
resolveMechPayload so the encoding is independently unit-tested
(test/mechPayload.test.ts), not just provable by re-running the real
script.
Fork-proved the corrected registration for real (Hardhat fork of
current Gnosis mainnet state, hardhat.config.cts's pin bumped past the
real deploy()/createMech() lifecycle so the fork sees service 3789
already Deployed): MechMarketplace.create(3789, NATIVE_FACTORY,
abi.encode(uint256(130000000000000000))) succeeds via
hardhat_impersonateAccount (no private key), and the resulting mech's
maxDeliveryRate() reads back exactly 130000000000000000 (0.13
xDAI/ETH, Forces' confirmed real price) -- test/fork/
correctedGnosisMech.forkcheck.ts, run via the new
test:fork:gnosis-corrected-mech script. This does NOT execute the real
signed call -- that stays Forces' passphrase-gated action.
config.ts: renamed/documented the broken address as
GNOSIS_MECH_ADDRESS_ORIGINAL_INERT (mirrors GREY_MECH_ADDRESS_ORIGINAL_
INERT's own permanent doc-comment discipline), and prepared (not yet
applied) the audit list of every place referencing it as if it were
live, to update once the real corrected mech exists.
vitest run: 110 passed (106 + 4 new), 2 skipped, 0 failed. tsc --noEmit
clean on all three gated configs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoiqDXzQQeejwguqcQFzBL
* fix(mech-adapter): resolve step+payload before preflight simulate (BION-DIRECTIVE-111)
register-live.ts computed the real createMech payload only AFTER calling
MechAdapter.registerAsMechStep to learn the next step -- but that call both
determines AND simulates the step in one shot, so the preflight simulate ran
with the stale '0x' placeholder. Forces' real run hit exactly this and
aborted safely at pre-flight, before any confirmation prompt.
Extract nextStepForState(state) into registrationResume.ts as the single
source of truth for the state->step mapping (mechAdapter.ts now uses it
internally too, so the two call sites can't drift). register-live.ts now
reads real service state, derives the step, and resolves the real
ABI-encoded payload via resolveMechPayload -- all before constructing params
or ever calling registerAsMechStep -- so the preflight simulate runs with the
correct payload on the first attempt.
Add test/fork/registerLivePreflightOrdering.forkcheck.ts: a real Hardhat
fork proof against Gnosis service 3789 (still Deployed) exercising the same
real shared functions in the same real order, including a negative control
that proves simulating with the old placeholder actually reverts on this
same forked state -- demonstrating this proof would have caught the D-111
bug, unlike D-110's fork proof, which never exercised the buggy call order.
vitest: 110 passed, 2 skipped, 0 failed. tsc --noEmit clean on all three
gated configs. New fork proof: 2 passed. Existing D-110 fork proof: 2 passed,
unaffected.
* feat(mech-adapter): add real GNOSIS_MECH_ADDRESS constant (BION-DIRECTIVE-112)
Forces ran the D-111-corrected register-live.ts for real: tx
0x62c919b2ff77016fc42fea9123db6e7c884c1a9f008070733c3946c28fd1e747, service
3789's createMech step. Desktop independently re-confirmed on-chain:
maxDeliveryRate() reads back exactly 130000000000000000 wei (0.13 xDAI) --
this mech is real, correctly priced, and payable.
Adds GNOSIS_MECH_ADDRESS = 0xf482B2Abbd0230b960320096B7b132fABc66830b,
same doc-comment discipline as GREY_MECH_ADDRESS. Corrects the systemd
unit's now-stale comment naming the superseded, permanently-unpayable
GNOSIS_MECH_ADDRESS_ORIGINAL_INERT as "the real mech" -- points at
config.ts instead of hardcoding an address that can go stale again.
tsc --noEmit clean, vitest 110/110 passing.
* feat(mech-adapter): build the real self-test request script (BION-DIRECTIVE-113)
Forces authorized the self-directed test request (real request -> deliver
-> settle cycle against the live, corrected Gnosis mech). Adds
scripts/self-test-request.ts -- same operator-runs-this/Kov-builds-this
split as register-live.ts: interactive-TTY-only passphrase, preflight
simulateContract before any broadcast, typed confirmation before executing
for real. Resolves every real value (mech's own maxDeliveryRate/paymentType,
the pinned request-content hash) before any passphrase prompt or simulate,
per D-111's own ordering lesson. Pins the real request content to Filebase
first (same real pin-and-verify mechanism responsePinner.ts already proves
for response content) so the live adapter can actually fetch it. After a
real submission, watches getRequestStatus() for Delivered, then fetches and
prints the real delivered response content -- proof of the whole cycle, not
just a landed transaction.
Real, fork-proof-discovered finding: MechMarketplace.request() enforces
responseTimeout in [60, 300] seconds (OutOfBounds otherwise) -- not
documented anywhere, found by the fork proof reverting on a 900s guess.
300s is therefore both the real protocol max AND coincides with the live
adapter's own default poll interval (config.ts's loadPollIntervalMs) --
flagged as a real, load-bearing timing risk this creates, not glossed over.
New test/fork/selfTestRequest.forkcheck.ts: real Hardhat fork proof against
the real, live GNOSIS_MECH_ADDRESS -- the exact live-read values + derived
content hash + request() call the script makes, succeeding for real and
recording a real pending (RequestedPriority) request; plus a negative
control proving the [60, 300] responseTimeout bound. Bumped the Gnosis fork
pin (hardhat.config.cts) past the real corrected-mech creation tx so the
fork can see GNOSIS_MECH_ADDRESS at all.
vitest: 110 passed, 2 skipped, 0 failed. tsc --noEmit clean on all three
gated configs. All three Gnosis fork proofs (D-110, D-111, D-113): 6 passed.
* fix(mech-adapter): self-test-request.ts must route around the known-broken gateway (BION-DIRECTIVE-114)
Forces ran pnpm self-test:request twice for real; both failed identically
at Filebase pin-verification (ResponsePinVerificationError) before any
keystore/passphrase interaction. Root cause, confirmed live, not assumed:
the script built createFilebasePinner without an explicit gatewayBaseUrl,
so it silently used the hardcoded default, gateway.autonolas.tech -- whose
IPFS-resolution backend was already found genuinely broken once before
(BION-DIRECTIVE-57, 2026-08-13) and is still broken today (re-confirmed
live during this directive: a curl against that same old, definitely-still-
pinned D-57 CID timed out completely, 20s, zero bytes, while ipfs.io
resolved the identical content in 0.2s).
Not a propagation-delay issue a wider retry budget would fix -- the gateway
doesn't respond at all, so retrying more just wastes more time against a
dead backend. Production (main.ts's loadGatewayOverride) already has the
real, proven fix: route both gateway-dependent legs (pin-verify and
request-content-fetch) through MECH_ADAPTER_PIN_VERIFY_GATEWAY_URL, set to
https://ipfs.io on the live Base unit. self-test-request.ts now does the
same -- reads the same env var, defaulting to https://ipfs.io directly
(rather than the broken gateway) if unset, since this standalone script has
no other safety net.
New test/gatewayOverride.livecheck.test.ts (gated behind
GREY_MECH_LIVE_GATEWAY_CHECK=1, skipped by default): a real, live (not
stubbed) network test proving the actual fix -- the broken default gateway
really fails against known-good, already-resolvable content (D-57's own
real pinned CID), and the override really resolves the identical content.
A genuinely fresh pin-and-verify isn't provable from here without real
Filebase credentials (deliberately never held by this codebase/Kov, same
as every other real secret) -- this is what CAN be proven for real without
them, and it directly validates the mechanism the fix depends on.
vitest: 110 passed, 4 skipped (2 new, gated), 0 failed. Live gateway check
run explicitly: 2 passed. tsc --noEmit clean on all three gated configs.
Real, separate, unresolved question this surfaced: whether
/etc/grey/mech-adapter-gnosis.env also sets MECH_ADAPTER_PIN_VERIFY_GATEWAY_URL
-- if not, the live Gnosis adapter itself (not just this script) is exposed
to the same broken-gateway failure the next time it needs to fetch a real
request's content. Flagged for Forces/Desktop to check directly; no VPS
access from here to verify.
* fix(mech-adapter): createSafeDeliveryClient hardcoded Base chain, breaking Gnosis delivery (BION-DIRECTIVE-115)
Real, live production bug, caught by the "First real Gnosis mech
transaction watch": the live grey-mech-adapter-gnosis.service picked up
BION-DIRECTIVE-113's self-test request and tried to deliver it via the
Safe's execTransaction, but every attempt failed at eth_sendRawTransaction
with "invalid chain id for signer: have 8453 want 100".
Root cause: createSafeDeliveryClient hardcoded viem's `base` chain object
for both its public and wallet clients, completely independent of the
chainId actually in use -- so on Gnosis, the raw transaction it signed
always embedded chainId 8453 regardless of which chain rpcUrl pointed at.
This was the one client in the package the D-97/98/104/106
chain-parameterization pass never touched (serviceRegistryClient.ts and
marketplaceClient.ts were fixed; this one wasn't), and safeDeliveryClient's
own anvil fork test only ever forks Base, so nothing caught it until a real
delivery on Gnosis actually needed it.
Fix: createSafeDeliveryClient now takes an optional chainId (default 8453,
Base's own real delivery path completely unaffected unless a caller
explicitly passes 100 -- same "behavior-identical unless explicit opt-in"
discipline the earlier chain-parameterization fixes established), and
resolves its clients' chain object from CHAINS[chainId] instead of the
bare `base` import. main.ts now passes the real chainId through. Added a
`chainId` diagnostic property to SafeDeliveryClient's own interface so this
is independently testable without a real network call or an anvil fork
(same posture as ServiceRegistryClient.gnosisSafeMultisig/
MarketplaceClient.getFactoryAddress) -- that property's absence is exactly
why this bug went unnoticed.
New tests in chainParameterization.test.ts: Base (8453, default) and
Gnosis (100) both construct with the correct chain id.
vitest: 112 passed (2 new), 4 skipped, 0 failed. tsc --noEmit clean on all
three gated configs.
* fix(mech-adapter): self-test-request.ts Step 5 used the wrong content-fetch function (BION-DIRECTIVE-116)
Step 5 (fetching the delivered response to print real proof of the whole
cycle) called fetchRequestContent on RESPONSE content -- that function
validates for {prompt, tool} fields specifically (real REQUEST shape,
requestContent.ts's own RequestContent interface), but a real
prediction_market_research response is shaped completely differently
({market, status, analysis, lastAnalysed, note}) and has neither field, so
it always failed to parse and threw "not a valid request document" -- on
every real run, right after a real successful delivery.
Added fetchArbitraryContent: same real gateway-fallback shape (direct CID,
then /metadata.json) minus the request-specific shape validation, since
response content just needs to be valid JSON, of any shape. Verified live
against the actual real delivered content from D-115's now-confirmed
delivery (requestId 0x40eb64b8...fa7769, dataHash 0xac3f3041...ff10f1):
resolves correctly via the metadata.json fallback, real body confirmed
matching the expected prediction_market_research response schema.
tsc --noEmit clean on all three gated configs. vitest: 112 passed, 4
skipped, 0 failed -- no regressions (this fix only touches the script, no
shared library changed).
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>1 parent 47ba769 commit b4a6d42
22 files changed
Lines changed: 1788 additions & 61 deletions
File tree
- adapters/mech-adapter
- scripts
- src
- test
- fork
- infra
- deploy
- systemd
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
36 | 40 | | |
37 | 41 | | |
38 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
18 | 23 | | |
19 | 24 | | |
20 | 25 | | |
| |||
0 commit comments