feat(exit-certificate): split Step G into G1/G2 with off-chain LER + shadow-fork verification#1633
Conversation
…l Step G replay
The parallel Anvil shadow-fork replay in Step G emits BridgeEvents in a
non-deterministic order, so the certificate's BridgeExits no longer match the
exit-tree deposit order. Recover the canonical order from the shadow-fork and
reorder BridgeExits to match the computed NewLocalExitRoot.
- Add Options.DepositOrderSource ("events" default, or "bridgesync") to select
how the order is recovered, with validation in LoadConfig.
- step_g_events.go: read BridgeEvent logs directly from the replayed fork blocks
(lightweight, no full L2 sync).
- step_g_bridgesync.go: reuse the production bridgesync component as an
alternative source.
- step_g_order.go: shared reorder logic keyed by DepositCount.
- Expose StepGResult.ShadowForkFirstBlock so later steps can scan from the
fork head.
- Persist step-g-reordered-certificate.json (single + all-steps modes) and have
Step I prefer it over the pre-G ordering.
- Silence expected context.Canceled noise when cancelling the worker pool after
a real failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…optional shadow-fork verification Reworks Step G (NewLocalExitRoot) and renames several options. Step G split: - G1 (step_g1.go): lite-syncs the L2 bridge history from genesis up to the target block into a persistent lite DB (new bridgesyncerlite package, which reads BridgeEvent logs in parallel and builds a bridge exit tree byte-for-byte compatible with bridgesync). Resolves the shadow-fork block. - G2 (step_g2.go, formerly step_g.go): computes NewLocalExitRoot. By default (verifyNewLocalExitRootUsingShadowFork=true) it spins up the Anvil shadow-fork, replays every bridge exit, reorders the certificate to the on-chain deposit order, and verifies the lite exit tree root against the contract's getRoot(). With the option false it computes the root off-chain from the lite tree (G1 bridges + certificate exits) without Anvil. Removed options.depositOrderSource (events/bridgesync modes) and the production bridgesync-based recovery (step_g_bridgesync.go deleted); deposit order now comes from the replay's BridgeEvents (shadow-fork) or the certificate order (off-chain). New option: - options.verifyNewLocalExitRootUsingShadowFork (default true). Renamed options to the ignore* convention: - abortOnGenesisBalance -> ignoreGenesisBalance (polarity inverted) - continueOnTraceError -> ignoreOnTraceError - continueIfBalanceMismatch -> ignoreBalanceMismatch - (lite syncer) allowUnsupportedEvents -> ignoreUnsupportedL2Events Lite DB files renamed to step-g1-/step-g- prefixes; G2 works on a copy so G1's DB stays intact. Tests, docs (CLAUDE.md, README), config examples and scripts updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d of aborting A receipt timeout during the shadow-fork replay (block not mined within the poll window, typically a slow remote fork backend under load) previously fail-fasted the whole Step G2 run. Now such exits are deferred and recovered after the send/collect phase drains. - Bump receiptPollTimeout 120s -> 300s. - Mark timeouts with the errReceiptTimeout sentinel so collectors can tell them apart from reverts/hard RPC errors (which still abort). - Defer timed-out exits to a list instead of aborting; retry them once Anvil is idle via retryDeferredExit. - retryDeferredExit loops unbounded until the exit mines: re-poll the current tx first, and only re-send the bridgeAsset if the receipt is still absent. The re-poll-before-resend ordering prevents double-counting a tx that did mine (which would diverge the reconstructed tree from the contract's getRoot). Exits only on success, a revert, or context cancellation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es reordered cert Config files can now be JSON or TOML, selected by file extension (.toml parsed as TOML, anything else as JSON). TOML is normalized to JSON internally so both formats share one parsing/validation path, including signerConfig and agglayerClient. Added parameters.toml.example (commented) and converted the config-examples to TOML. Step I (single-step mode) now always reads step-g-reordered-certificate.json instead of falling back to the capped/Step-E certificates, so the final certificate always matches the computed NewLocalExitRoot. Also refines the Step G1 fetch-progress ETA to measure throughput over a trailing window rather than the lifetime average. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o address LoadConfig now errors when exitAddress is missing or set to the zero address (0x00..00), since SC-locked funds can only be recovered by signing from an address whose private key the operator controls. Adds TestLoadConfig_MissingExitAddress and TestLoadConfig_ZeroExitAddress, and updates the existing config tests/fixtures and the docs (README, CLAUDE.md, config-examples, parameters.toml.example) to mark exitAddress (and l2RpcUrl/l2BridgeAddress in the examples) as required. Closes agglayer/pm#348 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that exitAddress is mandatory and rejects the zero address, the example configs ship it commented out so the operator must uncomment and set an address whose private key they control before running. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- errcheck: explicitly discard the failFast return value at the three call sites that intentionally continue/break (it already records the replay error and cancels the context). - lll: wrap the replay-start log line under 120 chars. - mnd: extract the uint256 bit width (256) into a named constant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @joanestebanr's task in 2m 14s —— View job PR Review Complete ✅Reviewed PR #1633: Split Step G into G1/G2 with off-chain LER + shadow-fork verification 🚀 What's NewThis PR significantly reworks Step G by splitting it into G1 (lite-sync L2 bridge history) and G2 (compute NewLocalExitRoot), adds a new 🐛 Bug FixesNone 📋 Config Updates
|
There was a problem hiding this comment.
Pull request overview
This PR refactors the exit-certificate tool’s Step G (NewLocalExitRoot computation) into a two-phase pipeline (G1/G2) to improve performance and optionally allow an off-chain computation path, while also updating configuration handling (including TOML support) and renaming several options to an ignore* convention.
Changes:
- Split Step G into Step G1 (lite-sync L2 bridge history into a persistent DB) and Step G2 (compute/verify
NewLocalExitRoot, reorder certificate by deposit order, and persist reordered output). - Add
bridgesyncerlite(paralleleth_getLogssync + persisted leaves + exit-tree build) and wire it into Step G1/G2. - Update config parsing/validation: TOML support via
.tomlextension, makeexitAddressrequired/non-zero, rename multiple options (abortOnGenesisBalance→ignoreGenesisBalance, etc.).
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/exit_certificate/worker.go | Suppress noisy context.Canceled logging from worker pools after fail-fast cancellation. |
| tools/exit_certificate/types.go | Update Step F comment for renamed option; add StepG1Result. |
| tools/exit_certificate/step_g1.go | New Step G1: lite-sync L2 bridge history into a persistent sqlite DB and emit shadow-fork block info. |
| tools/exit_certificate/step_g2.go | New Step G2: compute NewLocalExitRoot via shadow-fork replay (default) or off-chain lite-tree path; includes replay pipeline + verification. |
| tools/exit_certificate/step_g.go | Remove legacy monolithic Step G implementation (replaced by G1/G2). |
| tools/exit_certificate/step_g_order.go | New helper to reorder certificate exits by replayed depositCount. |
| tools/exit_certificate/step_g_order_test.go | Tests for certificate reordering by deposit count. |
| tools/exit_certificate/step_g_events.go | Off-chain lite-tree construction from certificate exits; DB-copy + replayed leaf insertion + tree build. |
| tools/exit_certificate/step_f.go | Rename mismatch-continue option and update messaging accordingly. |
| tools/exit_certificate/step_f_test.go | Update tests for renamed Step F option. |
| tools/exit_certificate/step_b.go | Apply ignoreGenesisBalance semantics (polarity inversion) in Step B1. |
| tools/exit_certificate/step_a.go | Apply ignoreOnTraceError option rename in Step A1. |
| tools/exit_certificate/step_a_test.go | Update tests/docs for renamed trace-error option. |
| tools/exit_certificate/scripts/reproduce_sc_locked.sh | Update config option naming and Step G file references. |
| tools/exit_certificate/scripts/configuration_based_on_kurtosis.sh | Update config option naming and include new ignore flags. |
| tools/exit_certificate/run.go | Add g1/g2 steps, g alias expansion, and persist reordered certificate output for Step I. |
| tools/exit_certificate/run_test.go | Update step-range parsing tests for g1/g2 and g alias behavior. |
| tools/exit_certificate/README.md | Update docs for JSON/TOML config, new Step G split, option renames, and exitAddress validation (but still needs alignment with Step I/G outputs). |
| tools/exit_certificate/parameters.toml.example | New TOML example config with annotated fields and defaults. |
| tools/exit_certificate/parameters.json.example | Update JSON example for renamed/new options. |
| tools/exit_certificate/config.go | Add TOML support, enforce exitAddress required/non-zero, add new options, rename ignore flags. |
| tools/exit_certificate/config-examples/zkevm-mainnet.toml | Convert mainnet example config to TOML and update required fields/options. |
| tools/exit_certificate/config-examples/zkevm-mainnet.json | Remove JSON example variant (replaced by TOML). |
| tools/exit_certificate/config-examples/zkevm-cardona.toml | Convert Cardona example config to TOML and update required fields/options. |
| tools/exit_certificate/config-examples/zkevm-cardona.json | Remove JSON example variant (replaced by TOML). |
| tools/exit_certificate/config-examples/README.md | Update examples README for TOML focus and new required fields. |
| tools/exit_certificate/config_test.go | Add tests for exitAddress validation and TOML parsing/round-tripping of signer config. |
| tools/exit_certificate/cmd/main.go | Update CLI --config help text to reflect JSON/TOML support. |
| tools/exit_certificate/CLAUDE.md | Update internal docs for Step G split, option renames, and TOML support. |
| tools/exit_certificate/bridgesyncerlite/types.go | New lite syncer types + leaf hashing compatible with canonical bridgesync tree. |
| tools/exit_certificate/bridgesyncerlite/syncer.go | New lite syncer core: persist leaves, build exit tree, DB-only mode support. |
| tools/exit_certificate/bridgesyncerlite/syncer_test.go | Tests for hashing compatibility, persistence, and tree build correctness. |
| tools/exit_certificate/bridgesyncerlite/migrations.go | DB schema/migrations for lite syncer bridge table + shared tree tables. |
| tools/exit_certificate/bridgesyncerlite/downloader.go | Parallel log downloader with forbidden-event detection and trailing-window ETA. |
| tools/exit_certificate/.gitignore | Ignore parameters.toml in addition to JSON and output artifacts. |
The doc comment described the off-chain (no-Anvil) path as the default, but defaultOptions sets verifyNewLocalExitRootUsingShadowFork=true, so the default is the Anvil shadow-fork verification path. Swap the description so the documented default matches the dispatch in the code. Addresses PR #1633 review comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
liteForkNextDepositCount loaded the entire genesis→fork bridge history into memory (GetBridges) just to read the last leaf's deposit count, an avoidable O(n) read/alloc that dominated off-chain mode startup on mainnet-scale histories. Add BridgeSyncerLite.NextDepositCount, which runs a single MAX(deposit_count) aggregate query (O(1)), and use it instead. Adds TestNextDepositCount. Addresses PR #1633 review comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p G/I docs - config: reject a malformed exitAddress with a clear "not a valid hex address" error via common.IsHexAddress, instead of letting common.HexToAddress silently coerce it to the zero address and surface the misleading zero-address error. Adds TestLoadConfig_InvalidExitAddress. - README: Step G now documents the G1/G2 outputs (shadow-fork block, lite DBs, reordered certificate); Step I documents that it always reads step-g-reordered-certificate.json with no Step E/F fallback. Addresses PR #1633 review comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
syncLiteToBlock loaded every persisted bridge into memory via GetBridges just to log the count — hundreds of thousands of leaf allocations on mainnet-scale runs for no functional benefit. Add BridgeSyncerLite.CountBridges (single COUNT(*) query, O(1)) and use it for the log line. Adds TestCountBridges. Addresses PR #1633 review comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d return copyFile dropped out.Close() errors when io.Copy succeeded but the deferred flush failed (e.g. a full disk), so a partially-written lite DB copy could be reported as success. Switch to a deferred close with a named return that surfaces the close error when the copy itself succeeded (a copy error still takes precedence). Addresses the required issue from the PR #1633 automated review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the 🔴 Required Issue (
The 🟡 suggested improvements (tx helper extraction, constant justifications, richer forbidden-event error, batched config validation, progress-logging atomics) are noted; happy to fold in any you consider blocking — otherwise I'll leave them as follow-ups to keep this PR focused. |
maybeLogProgress mixed atomic Add/Store/Load/CAS on the completed counter and last-log timestamp; an interval-milestone Store could interleave with the gap-based CAS and emit a duplicate progress line. Replace the atomics with a single mutex that guards the counter and the timestamp as a unit, collapsing the two branches into one condition. The lock is uncontended (work between calls is a receipt fetch), so it is not on a hot path. Addresses suggested improvement #5 from the PR #1633 automated review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed 🟡 suggested improvement #5 (Potential Race in Progress Logging) in 8d60595.
Replaced the atomics with a single |
… tools/ from Sonar Add RPC-backed tests (fake JSON-RPC server), parseBridgeEvent/classifyLogs unit tests and error-branch coverage, raising bridgesyncerlite from 41.2% to 84.6%. Remove tools/** from sonar.exclusions so the tool's coverage is actually measured instead of reported as 0.0%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ff-chain LER path Add unit tests for runWorkerPool, the pure Step G2 helpers (event log parsing, revert decoding, transient-error classification, failed-exit persistence) and the off-chain NewLocalExitRoot path (buildLiteTreeFrom- Certificate, buildLiteTreeWithReplayed, lite DB copy/remove helpers and RunStepG2 dispatch) using an in-process JSON-RPC stub. Raises the main package coverage from 31.0% to 37.1%; the remaining gaps are the Anvil shadow-fork and RPC-driven steps that need live infrastructure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover the pure Step B helpers (filterEOAs, padLeft, sumBalances, isEOAResult, unmarshalHexBigInt, buildEOABalances/buildSingleEOABalance, buildAccumulated) and the RPC fan-out functions (classifyAddresses, fetchETHBalances, fetchAllTokenBalances) plus the RunStepB/RunStepB1 orchestration and the genesis-preload guard, driven by an in-process batch JSON-RPC stub. step_b.go functions now sit at 75-100%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract recordProgressTick from reportFetchProgress so the per-tick ETA math is unit-testable without waiting for the real ticker, and add tests for it plus Step G1 helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p G2 and test the replay Introduce forkBackend (Anvil RPC ops: read LER, resolve token, set balance, prepare ERC-20, send bridgeAsset, poll receipt) and forkLauncher (Anvil process spawn) interfaces, with anvilForkBackend/anvilLauncher as the production implementations delegating to the existing helpers. The replay orchestration (replayBridgeExits, retryDeferredExit, resolveTokenAddresses, runStepG2ShadowFork) now depends on these seams instead of a raw anvil URL, so a mock backend/launcher drives them in unit tests without Anvil or a live node. Covers the send/collect pipeline, fail-fast on send error/revert/ missing BridgeEvent, the receipt-timeout defer-and-retry path, token resolution (L2-native, LBT, contract fallback) and the root verification / mismatch branches. replayBridgeExits 0%->92%, retryDeferredExit 0%->100%, resolveTokenAddresses 0%->96%, runStepG2ShadowFork 0%->79%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive checkL2NetworkID, checkNativeGasToken, checkContractPrereqs and RunStepCheck against an in-process JSON-RPC stub that ABI-encodes contract return values by selector. Covers network-id match/mismatch, gas-token present/absent, PP vs FEP, threshold and bridge-address mismatches, the legacy-diagnostics fallback and the combined-failure aggregation. step_check.go 0% -> ~70%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add tests for the L1 deposit scan (fetchL1BridgeEvents/fetchBridgeEventsInRange), isClaimed batching (checkClaimedBatch), L1 head resolution, token name/decimals lookup, the unclaimed-asset summary and the bridge-service cross-check (fetchZkevm/fetchAggkitPendingBridges, checkBridgeServicePendingBridges) using httptest + an in-process JSON-RPC stub. Extend newBatchRPCServer to also answer single (non-batched) requests so singleRPC callers can use it. step_e.go ~30% -> ~70%; package 46.9% -> 55.0%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a security recommendation to use a multisig (e.g. Gnosis Safe) instead of a single EOA for exitAddress, since SC-locked funds can only ever be recovered by signing from that address. Applied to the tool README, config-examples README, and the example config comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive RunStep0 end-to-end against an in-process JSON-RPC stub (NewWrappedToken log scan, SetSovereignTokenAddress overrides, totalSupply batch, native-balance diff, gas-token and WETH lookups) plus unit tests for the event decoders, constant target-block resolution and the empty-supplies path. step_0.go ~7% -> ~85%; package 55% -> ~59%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- step_g2: comment the intentionally-empty send-pool collector - syncer.New: extract openDatabase/dialBridge to cut cognitive complexity - LoadConfig: split into readRawConfig/validateRawConfig/buildConfig - mergeOptions: split into mergeScalarOptions/mergeFlagOptions/mergeAgglayerClient - centralize duplicated output filenames into filenames.go constants Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…const Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hestrators Add httptest JSON-RPC stub tests for the anvilForkBackend RPC functions (setSenderBalance, readLocalExitRoot, callGetTokenWrappedAddress, sendAnvilTransaction, sendBridgeAssetTx, waitForReceipt, fetchRevertReason, ensureERC20Balance, prepareERC20Token) and their backend wrappers. Cover run.go single-step orchestrators: missing-input guards for submit/wait/f/h/i/g1/g2/sign, the Step E l1RpcUrl guard, runSingleG2's empty-certificate EmptyLER path, and resolveLatestBlock. Package coverage 61.3% -> 66.4%. Remaining step_g2 gaps (startAnvil/ waitForAnvil/Start) require the real anvil binary (integration only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace fmt.Sprintf-built read queries (CountBridges, NextDepositCount, GetBridges) with compile-time constant query strings so there is no runtime SQL string formatting — the only interpolated token is the trusted bridgeTableName constant. Clears the SonarCloud dynamic-SQL hotspot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4b96d43
into
feature/exit-certificate-tool
|



🔄 Changes Summary
Speeds up and reworks Step G (NewLocalExitRoot), splitting it into G1 and G2 and adding an off-chain computation path.
Step G split
step_g1.go): lite-syncs the L2 bridge history from genesis up to the target block into a persistent lite DB, using the newbridgesyncerlitepackage (readsBridgeEventlogs in parallel and builds a bridge exit tree byte-for-byte compatible withbridgesync). Resolves the shadow-fork block.step_g2.go, formerlystep_g.go): computesNewLocalExitRoot.verifyNewLocalExitRootUsingShadowFork=true): spins up the Anvil shadow-fork, replays every bridge exit in parallel (send/collect pipeline), reorders the certificate to the on-chain deposit order, and verifies the lite exit tree root against the contract'sgetRoot().=false): computes the root purely from the lite tree (G1 bridges + the certificate's exits, in order) — no Anvil.Step I always uses the reordered certificate
step-g-reordered-certificate.json(run Step G first) instead of falling back to the capped/Step-E certificates, so the final certificate always matches the computedNewLocalExitRoot. (runAllalready flowed the in-memory reordered cert.)Removed
options.depositOrderSource(theevents/bridgesyncmodes) and the production-bridgesync recovery (step_g_bridgesync.go). Deposit order now comes from the replay'sBridgeEvents (shadow-fork) or the certificate order (off-chain).StepGResult.ShadowForkFirstBlockdropped.New
bridgesyncerlitepackageeth_getLogs, persistsBridgeEventleaves and builds the exit tree. Supports a DB-only mode (no RPC) so G2 can insert pre-collected leaves and build the tree without touching Anvil. Aborts on events that invalidate aBridgeEvent-only reconstruction (SetSovereignTokenAddress,MigrateLegacyToken,RemoveLegacySovereignTokenAddress,BackwardLET,ForwardLET) unlessignoreUnsupportedL2Eventsis set.exitAddressis now mandatory —LoadConfigerrors when it is missing or set to the zero address (0x00…00). Configs that previously omitted it (it defaulted to the zero address) now fail. SC-locked value is bridged to this address and can only be recovered by signing from an address whose private key the operator controls.ignore*convention) —abortOnGenesisBalance→ignoreGenesisBalance(polarity inverted: defaultfalse= abort),continueOnTraceError→ignoreOnTraceError,continueIfBalanceMismatch→ignoreBalanceMismatch.options.depositOrderSource; removed theconfig-examples/.jsonvariants (converted to.toml).📋 Config Updates
Config accepts JSON or TOML
LoadConfigselects the format by file extension:.tomlis parsed as TOML, anything else (.json/no extension) as JSON. TOML is normalized to JSON internally (tomlToJSON) so both formats share one parsing/validation path, includingsignerConfig(json.RawMessage) andagglayerClient. Field names are identical in both formats.parameters.toml.example(each field commented with its description + default) and converted theconfig-examples/to TOML (zkevm-cardona.toml,zkevm-mainnet.toml); removed the.jsonvariants..gitignorenow also ignoresparameters.toml.exitAddressvalidationLoadConfignow rejects a missing or zero-addressexitAddress. Docs/examples updated (the field was previously documented as optional, defaulting to the zero address) andexitAddressships commented-out in the example configs so the operator must set their own.New options
options.verifyNewLocalExitRootUsingShadowFork—true(default).trueverifies the LER on the Anvil shadow-fork (requires Anvil);falsecomputes it off-chain from the lite tree (no Anvil, trusts off-chain leaf encoding/metadata).options.ignoreUnsupportedL2Events—false(default). Downgrades the lite syncer's abort on unsupported events to a warning.Renamed options (to the
ignore*convention)abortOnGenesisBalance→ignoreGenesisBalance(polarity inverted: defaultfalse= abort)continueOnTraceError→ignoreOnTraceErrorcontinueIfBalanceMismatch→ignoreBalanceMismatchRemoved
options.depositOrderSource.✅ Testing
go test ./tools/exit_certificate/...passes (incl.bridgesyncerlite,step_g_order_test.go, andconfig_test.gowith the newTestLoadConfig_MissingExitAddress/TestLoadConfig_ZeroExitAddress).go build,go vet,gofmt, andgolangci-lintclean.--step g(G1+G2) and Step I; confirmstep-g-new-local-exit-root.json+step-g-reordered-certificate.jsonare produced and the lite tree root matches the contractgetRoot()in verify mode.LocalExitRoot(shadow-fork verification took 13.5h, with a total of 975,646 bridges generated).🐞 Issues
📝 Notes
--step gruns G1+G2;g1/g2run individually;gexpands tog1,g2in ranges.feature/exit-certificate-tool(the exit-certificate integration branch), notdevelop.🤖 Generated with Claude Code