Skip to content

Prime repo, green test suite, and fix live-trading correctness bugs in the Uniswap path - #2

Merged
KBryan merged 21 commits into
mainfrom
feat/tradingagent
Oct 4, 2026
Merged

KBryan merged 21 commits into
mainfrom
feat/tradingagent

Conversation

@KBryan

@KBryan KBryan commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Started as a /prime bootstrap (AGENTS.md, manifest.yml, baseline) and turned into a correctness pass on everything the baseline surfaced. The test suite goes from 21 failed / 33 passed / 17 silently skipped to 105 passed / 0 failed (3 skips are live-CoinGecko tests), and several bugs on the real-funds trading path are fixed along the way.

Bugs fixed on the trading path — please review these first

Where Bug Impact if REAL_DATA_MODE=true
integrations/uniswap.py WETH and USDC addresses were placeholders (fail EIP-55 checksum; not real contracts). api/routers/trade.py had two more bogus USDC variants. Every swap routes through WETH → every swap targets a non-existent token.
core/tasks.py USDT scaled as 18 decimals (it has 6). USDT amounts off by 10¹².
integrations/uniswap.py (V2) All amounts scaled by flat 10**18. USDC/USDT amountIn and amountOutMin encoded wrong by 10¹² → reverts, or wrong spend.
integrations/uniswap.py (V3) Inherited V2's getAmountsOut on the V3 SwapRouter (function doesn't exist). Every V3 quote reverts; V3 execute_trade would send a guaranteed-revert tx.
api/deps.py RateLimiter declared request_id: str → required query param; also client-controlled. 422 on every /trade/prompt & /trade/execute; bucket resettable by the caller.
integrations/coingecko.py HTTP 429 retried unboundedly. Infinite loop on sustained rate limiting.
core/strategies/momentum.py numpy.bool_ leak; MA "crossover" only fired on the exact bar; current bar's volume averaged against itself. MA trend filter effectively inert; volume confirmation mis-scaled.

Structural changes

  • core/tokens.py — single per-network token registry (address + decimals), EIP-55-validated at import. Replaces three divergent hard-coded tables (which is how the placeholders got in). to_base_units goes through Decimal so int(1.1 * 10**18) float noise can't reach wei.
  • core/contracts.py — same for Uniswap router/factory/quoter addresses.
  • core/execution/adapters.py — registers Uniswap V2/V3 adapters on trade_engine at app startup (previously nothing ever called register_adapter). Unreachable RPC drops that adapter with a warning, not startup. ensure_adapters() retries lazily (locked, 30s cooldown) so the app recovers when the node comes back.
  • GET /trade/quote — new read-only endpoint over trade_engine.get_best_quote(); compares exchanges net of fees. Documented in docs/API_DOCUMENTATION.md.
  • V3 adapter quotes via quoteExactInputSingle across the 0.05/0.3/1% fee tiers; execute_trade on it now refuses explicitly (live V3 swaps continue through core/tasks.py).

Behaviour changes to be aware of

  • Momentum MA filter is now a trend state (short MA ≥ long MA required for BUY), not a single-bar crossover event. Stricter than before — the old filter almost never blocked a trade.
  • Rate limiter keys on verified wallet (fallback: client IP) instead of a client-supplied id. get_optional_user runs inside the limiter, so on trade endpoints the NFT check runs twice per request (second is a Redis cache hit).
  • skale / beam have no token/contract registry; resolving there now raises UnknownTokenError instead of silently using mainnet addresses. Nothing in production creates adapters for those networks today.
  • pytest.ini header fixed ([tool:pytest] → [pytest]), which activated asyncio_mode=auto — the 17 "skipped" tests were async tests never actually running. --cov-fail-under=80 removed (coverage is ~43%; it would have failed every run).

Tooling

  • requirements.txt: pins setuptools<81 and eth-typing<5 (needed by web3==6.12.0), adds pytest-cov, pins the previously unpinned tail, removes duplicates. Fresh install was broken without these.
  • AGENTS.md, manifest.yml, .agent/baseline.md, .agent/maturity.md generated from a real scan; testing conventions (patch <module>.settings, wraps=Web3, AsyncMock for aiohttp, dependency_overrides for Depends) recorded so the wrong-mock-target class of failure doesn't recur.

Test plan

  • pytest — 105 passed, 3 skipped (live CoinGecko), 0 failed
  • New guard tests fail against the old code (placeholder addresses, V2 getAmountsOut not called from V3, etc.)
  • Manual: with a reachable ETHEREUM_RPC_URL, GET /trade/quote?token_in=ETH&token_out=USDC&amount_in=1 returns a V3 quote with amount_out in the right order of magnitude
  • Review the momentum filter change against any live strategy config before merging

Not addressed (pre-existing, noted in AGENTS.md): flake8/black/mypy debt across the codebase, tracked *.backup* files and celerybeat-schedule, no CI, /trade/prompt uses a regex parser rather than the LLM client.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM

KBryan and others added 21 commits September 14, 2026 17:25
…line

Generate agent configuration from a full repository scan:

- AGENTS.md: project overview, navigation, architecture, working
  setup/validation/run commands, risk model, escalation, env vars,
  and known issues
- manifest.yml: fill in stale adws manifest (entry point, per-path
  risk, validate/run/setup commands)
- .agent/maturity.md: Level 2 classification with path to Level 3
- .agent/baseline.md: pytest 21 failed / 33 passed / 17 skipped,
  flake8 756, black 25 files, mypy 111 errors, with root causes
- .claude/settings.json, requirements/agent.txt: framework scaffolding
- .gitignore: agent hook logs and machine-local framework output

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
- pytest.ini: rename `[tool:pytest]` (setup.cfg syntax, ignored by
  pytest) to `[pytest]` so addopts, markers and asyncio_mode=auto apply.
  Drop --cov-fail-under=80: coverage is 41%, so the gate would fail every
  run; coverage is still reported.
- requirements.txt: pin setuptools<81 and eth-typing<5 (web3 6.12 imports
  pkg_resources and eth_typing.ContractName, both removed upstream); add
  pytest-cov; pin the unpinned trailing entries (PyJWT, flower, psutil)
  and remove the duplicate pydantic-settings / celery[redis] / httpx lines.
- integrations/coingecko.py: bound HTTP 429 retries (max_retries=3) and
  raise CoinGeckoError afterwards. Previously the retry recursed forever,
  which hung the suite once the async tests actually ran.
- .gitignore: ignore .coverage.
- Baseline is now 26 failed / 42 passed / 3 skipped (was 21/33/17); the
  17 "skips" were async tests pytest-asyncio strict mode refused to run.
  No previously-passing test regressed. .agent/baseline.md and AGENTS.md
  updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
The suite patched `api.deps.get_current_user` / `get_optional_user`, but
those functions are captured inside `Depends(...)` at import time, so the
patches were never seen and the real NFT gate ran (400 on every
authenticated request). Replace them with FastAPI
`app.dependency_overrides` fixtures:

- `authenticated_user` / `anonymous_user` override the auth dependencies
- `trade_deps` stubs `trade_rate_limiter` and `get_redis_client` so trade
  endpoints don't need a live Redis

Also patch `verify_nft_ownership` in `api.routers.auth` (where it is
called) and retarget the prompt-parser mock to the local
`api.routers.trade.parse_trading_prompt`, which is what the endpoint
actually calls and which returns a dict, not a `TradingInstruction`.

tests/unit/test_api.py: 28/28 passing. Full suite: 12 failed / 56 passed /
3 skipped (was 26/42/3); run time 68s -> 8s since the API tests no longer
time out against a real RPC. Baseline and AGENTS.md updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
`RateLimiter.__call__` declared `request_id: str` with no default, which
FastAPI exposed as a required query parameter: every `/trade/prompt` and
`/trade/execute` call without `?request_id=` returned 422. Worse, the
value was client-controlled, so a caller could reset its own bucket by
changing it.

The limiter now takes the `Request` plus `get_optional_user`, and keys
the bucket on the verified wallet address (`rate_limit:wallet:0x…`),
falling back to client IP for unauthenticated routes.

Add two tests that run the real `trade_rate_limiter` through the app
with only Redis stubbed: no request_id needed, key is the wallet, and
429 at the limit. Suite: 12 failed / 58 passed / 3 skipped.

Also correct the AGENTS.md timing note — the intermittent ~60s is
`TestCoinGeckoIntegration` sleeping on a live 429, not the API tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
…ests

- _check_volume_confirmation returned numpy.bool_ (np.mean comparison),
  so `is True` checks failed; cast to bool. Treat exactly 20% above
  average as confirming (>=), matching the documented threshold.
- _calculate_ma_crossover only returned ±1 on the single bar where the
  averages cross. _determine_signal_type consumes it as a trend filter
  (ma_signal >= 0 for BUY), which made it nearly inert. It now reports
  the alignment state: 1 when short MA is above long MA, -1 below.
- _analyze_token_momentum compared the current bar's volume against a
  history that already included that bar (analyze_market appends before
  analysing). Compare against the history preceding it instead.

tests/unit/test_strategies.py: 19/19. Suite: 9 failed / 61 passed /
3 skipped; every unit test is green. Baseline and AGENTS.md updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
All nine failures were test-side:

- Uniswap: `@patch('integrations.uniswap.Web3')` replaced the class with a
  bare MagicMock, so `Web3.is_address("ETH")` was truthy and
  `to_checksum_address` returned the same mock for every token — every
  swap became "Cannot swap token with itself". Use `wraps=Web3` so the
  static helpers stay real while the constructor still returns the fake
  connection. Give `test_create_uniswap_adapter` the same patch (it was
  dialling a real RPC), and compare the address-passthrough result
  case-insensitively since it is returned checksummed.
- Twitter: `integrations/twitter.py` captures `settings = get_settings()`
  at import, so patching `config.get_settings` afterwards had no effect.
  Patch `integrations.twitter.settings` instead.
- CoinGecko: `response.text()` is awaited; mock it with AsyncMock.

Suite: 70 passed / 3 skipped / 0 failed (exit 0), coverage 43%. Also
record in AGENTS.md that the USDC address in integrations/uniswap.py is
a placeholder, not mainnet USDC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
…t contracts

integrations/uniswap.py hard-coded
  WETH 0xC02aaA39b223FE8C0625C6E8C11028C0C5B9B2dB
  USDC 0xA0b86a33E6441E6C7C7C8C7C8C7C8C7C8C7C8C7C
and api/routers/trade.py carried two variants of the bogus USDC. None of
these are real contracts — they all fail EIP-55 checksum validation —
so with REAL_DATA_MODE every swap (all paths route through WETH) would
have targeted a non-existent token.

Set them to the canonical Ethereum mainnet addresses, matching
core/tasks.py:
  WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

Add test_token_addresses_are_real_mainnet_contracts, which asserts every
entry in UniswapV2Adapter.token_addresses is checksum-valid and pins
WETH/USDC; it fails against the old values. Suite: 71 passed / 3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Token contract addresses were hard-coded in three places that had
drifted apart (integrations/uniswap.py, api/routers/trade.py,
core/tasks.py) — which is how placeholder WETH/USDC addresses reached
production code. Replace them with a single per-network registry:

- core/tokens.py: TokenInfo(symbol, address, decimals) keyed by network
  and symbol; every address is EIP-55-validated at import so a typo or
  placeholder fails on startup. Helpers: get_token, get_token_address,
  token_addresses, is_supported_token, to_base_units, from_base_units.
  Only "ethereum" is registered; skale/beam raise UnknownTokenError
  instead of silently using mainnet addresses.
- integrations/uniswap.py: adapter table comes from the registry.
- core/tasks.py: drop TOKEN_ADDRESSES; resolve addresses and convert
  amounts via the registry. This also fixes USDT being scaled as 18
  decimals (it has 6) and uses Decimal so int(1.1 * 10**18) float noise
  no longer leaks into wei amounts.
- api/routers/trade.py: portfolio token list built from the registry.

tests/unit/test_tokens.py covers checksums, canonical mainnet contracts,
case-insensitive lookup, unknown token/network errors, and exact
base-unit conversion. Suite: 79 passed / 3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
…o core/contracts.py

The Uniswap V3 router address was duplicated between
integrations/uniswap.py (class attributes on both adapters) and
core/tasks.py (UNISWAP_V3_ROUTER). Move all router/factory addresses to
a per-network, per-version registry alongside the token registry:

- core/contracts.py: UniswapDeployment(version, router, factory) under
  UNISWAP[network][version]; get_uniswap(version, network) raises
  UnknownContractError for unregistered combinations. Also hosts the
  shared checksummed() validator, which core/tokens.py now reuses.
- integrations/uniswap.py: adapters declare UNISWAP_VERSION ("v2"/"v3")
  and resolve router_address / factory_address from the registry in
  __init__, so the router is looked up per network like tokens are.
- core/tasks.py: UNISWAP_V3_ROUTER is derived from the registry; the
  dead "not configured" check is removed.

tests/unit/test_contracts.py covers checksums, canonical deployments,
unknown version/network errors and placeholder rejection; the adapter
factory test asserts both adapters take their router from the registry.
Suite: 81 passed / 3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
…uter

UniswapV3Adapter inherited UniswapV2Adapter.get_quote, which called
getAmountsOut with the V2 router ABI on the V3 SwapRouter address — a
function that contract does not have — so every V3 quote would have
reverted on a real chain.

- core/contracts.py: add `quoter` to UniswapDeployment; register the
  mainnet V3 QuoterV1 (0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6).
- UniswapV3Adapter.get_quote: call quoteExactInputSingle for the
  single-hop path on each standard fee tier (500/3000/10000), skip tiers
  whose pool reverts, keep the best output, and report that tier's fee.
  Amounts use core.tokens decimals (1600 USDC is 1600.0, not 1.6e-9).
- UniswapV3Adapter.execute_trade: raise UniswapError instead of
  inheriting the V2 swap calls, which would send a guaranteed-revert
  transaction to the V3 router and burn gas. Live V3 swaps continue to
  go through core/tasks.py (exactInputSingle).

Tests: V3 quote uses the Quoter and not getAmountsOut, probes all tiers
with the right arguments, picks the best-paying tier, raises when no pool
exists, and execute_trade refuses without sending. Suite: 85 passed /
3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
UniswapV2Adapter scaled every amount by 10**18 "for simplicity". For a
6-decimal token that is wrong by 10**12 in both directions: a quote for
1 ETH -> 1600 USDC reported amount_out as 1.6e-9, and execute_trade
encoded 1000 USDC as 10**21 base units and a USDC amountOutMin at 10**18
scale — so any swap touching USDC/USDT would have reverted (or, for
amount_in, tried to spend a billion times the intended amount).

Promote the decimal-aware helpers added for V3 to the base class and use
them in V2's get_quote (amount_in, amount_out) and execute_trade
(amount_in, amountOutMin). Conversions go through Decimal, so float noise
does not leak into wei.

Tests: the V2 quote test now asserts 1600.0 out and 10**18 in; a new
execute test checks a USDC->DAI swap encodes 1000 * 10**6 in and an exact
18-decimal amountOutMin. Suite: 86 passed / 3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
…rtup

TradeExecutionEngine had a register_adapter() hook but nothing ever
called it, so the global trade_engine had no adapters and get_best_quote
always returned None.

- core/execution/adapters.py: register_default_adapters(engine, network)
  builds an adapter for each exchange in config.SUPPORTED_EXCHANGES that
  has an implementation (uniswap_v2, uniswap_v3; sushiswap is skipped)
  and registers it. Adapter construction dials the RPC node, so a failure
  drops that adapter with a warning instead of failing startup. Lives
  outside engine.py because integrations.uniswap imports from the
  engine.
- api/main.py: the lifespan calls it for trade_engine, off the event
  loop via asyncio.to_thread.

Tests cover registration of both Uniswap adapters and the SushiSwap
skip, an unreachable node dropping only its adapter, a network with no
exchanges, an end-to-end get_best_quote through both real adapters on
mocked contracts (V3 wins on net output), and that app startup performs
the registration. Suite: 91 passed / 3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Read-only endpoint exposing trade_engine.get_best_quote(). Compares every
adapter registered for the network net of fees, or a single one when
`exchange` is given. Authenticated, under api_rate_limiter (not the trade
limiter, so quotes do not consume the trade budget).

Query: token_in, token_out, amount_in (> 0), optional exchange, network.
Returns QuoteResponse: exchange, amounts, price, fees, slippage,
gas_estimate, route (token addresses), valid_until, exchanges_checked.

Errors: 400 for identical tokens or an exchange not registered on the
network (the detail lists what is), 422 for a non-positive amount, 503
when no adapters are registered (RPC unreachable) or none could quote.

Tests (8) drive the real engine's selection with fake adapters patched
onto trade_engine: best net output wins, pinned exchange only queries
that adapter, each error path, and auth required. The class stubs the
lifespan's adapter registration so it stays hermetic on machines with a
live RPC. Documented in docs/API_DOCUMENTATION.md. Suite: 99 passed /
3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
…stered

Startup registration fails silently if the RPC node is unreachable, and
until now that left /trade/quote returning 503 until a restart.

core/execution/adapters.py gains ensure_adapters(engine, network): it
returns the registered exchanges, and when there are none it re-runs
register_default_adapters off the event loop. Attempts are serialised
under an asyncio.Lock so concurrent 503s do not each dial the node, and
rate-limited to one per RETRY_COOLDOWN_SECONDS (30s) per network; the
cooldown is stamped before dialling (and by register_default_adapters
itself, so the startup attempt counts). /trade/quote uses it in place of
reading trade_engine.adapters directly.

Tests: no retry when adapters exist, retry when empty, cooldown honoured
and expiring, startup attempt starting the cooldown, five concurrent
callers sharing one attempt, and the endpoint going 503 -> 200 once the
node is back. Suite: 105 passed / 3 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Delete api/deps.py.backup, api/main.py.backup, core/tasks.py.backup,
core/tasks.py.backup6 (stale hand-made copies; git history serves this
purpose) and celerybeat-schedule (Celery beat's runtime state database).
Nothing references them. Ignore *.backup, *.backup[0-9]* and
celerybeat-schedule* so they cannot be re-added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Runs on pushes to main and on pull requests.

test job: matrix over Python 3.11 (Dockerfile) and 3.12 (runtime.txt);
pip install -r requirements.txt; pytest -m "not external" so live
CoinGecko/Twitter tests are excluded and the run is hermetic. The seven
env vars config.Settings requires are set to dummies. Coverage XML is
uploaded from the 3.12 leg.

lint job: flake8 --select=E9,F63,F7,F82 is a hard gate (syntax errors,
undefined names). Full flake8, black --check and mypy run with
continue-on-error so the existing debt (see .agent/baseline.md) is
visible without blocking; flip them to blocking as counts reach zero.

Also drop the `external` marker from test_uniswap_v3_quote_difference,
which is fully mocked, so it runs in CI. Verified both matrix legs pass
locally (104 passed / 6 deselected on 3.11 and 3.12). Badge added to
README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Hooks run on staged files only, so the existing style debt in untouched
files does not block commits; black and the whitespace/EOF fixers clean
up whatever you touch, paying the debt down incrementally.

- pre-commit-hooks v5: trailing-whitespace, end-of-file-fixer, check-yaml,
  check-toml, check-merge-conflict, check-added-large-files (500 KB),
  detect-private-key (this repo handles trading wallet keys),
  debug-statements
- black 23.11.0 (matches requirements.txt / CI)
- flake8 6.1.0 with the same --select=E9,F63,F7,F82 hard gate as CI

Verified: hooks pass on clean files, the flake8 gate passes tree-wide,
and detect-private-key flags a PEM key. Setup documented in AGENTS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Mirrors AGENTS.md's Review Expectations and Escalation Triggers: a
summary, risk-area checkboxes for the paths that need maintainer review
(auth, on-chain execution, address registries, strategies, migrations,
safety-default env vars), a behaviour-changes section, a test plan that
names the CI command, and a checklist with trading- and
migration-specific items.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
Three GitHub issue forms plus a config:

- Bug report — what/repro/area/version/environment/logs, with a reminder
  never to paste .env contents or keys.
- Trading incident — for a live or dry-run trade that did the wrong thing:
  mode (real funds / reverted / dry run), network, intended vs. actual,
  tx hashes and wallet, trigger path, trade id, logs, and containment
  checkboxes (emergency stop, router approvals). Labels bug + trading.
- Feature request — problem, proposal, area, and whether it changes how
  or when real funds move.
- config.yml — contact link routing security issues and leaked keys to a
  private security advisory instead of a public issue.

Adds the `trading` label to the repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
- pyproject.toml: [tool.pytest.ini_options] (migrated from pytest.ini,
  which is removed), [tool.coverage], [tool.black] (line-length 120,
  py311/py312), [tool.mypy] (files, ignore_missing_imports, error codes).
  No [project] table — the app is not packaged; deps stay in
  requirements.txt.
- .flake8: max-line-length 120, black-compatible ignores (E203, W503),
  excludes. flake8 cannot read pyproject.toml, hence the sibling file.
- CI and pre-commit now invoke the tools without inline options (except
  the deliberate flake8 --select error gate). Full flake8 also scans
  tests/ now, which moves its informational count from 756 to 1088.
- Reformat the modules created on this branch at line length 120.
- AGENTS.md / manifest.yml validation commands updated; maturity raised
  to Level 3 with a Level 4 path (pay down debt, CODEOWNERS + branch
  protection, private vulnerability reporting, single trade path).

Verified: pytest reports configfile=pyproject.toml, 104 passed with the
CI selection; flake8 error gate passes; black/flake8/mypy pick up their
settings with no flags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMkjaJypc9mcgLZp9cfyM
@KBryan
KBryan merged commit dfae2d1 into main Oct 4, 2026
3 checks passed
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.

1 participant