Commit Graph

52 Commits

Author SHA1 Message Date
jgrusewski
b4d0fee1e0 infra: add 50Gi PVC for DevPod home directory
Persistent block storage for Claude Code, cargo registry, sccache
cache, and shell config. Survives pod restarts and node scale-down.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:12:18 +01:00
jgrusewski
c6247ea9f8 fix(ci): update Dockerfiles for restructured repo + migrate to SCW registry
- Update COPY paths in all 3 Dockerfiles for crates/bin/services/testing layout
- Migrate service image builds from internal GitLab registry to SCW CR
- Update Kaniko auth to use SCW credentials (nologin + SCW_SECRET_KEY)
- Remove --insecure-registry flags (SCW CR is HTTPS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:09:52 +01:00
jgrusewski
3bfb9611b5 fix(ci): move PVC mount to /mnt/training-data (avoids Redis /data conflict)
Redis image WORKDIR is /data and entrypoint runs chown recursively.
PVC mounted at /data/training was read-only, causing chown to fail
with exit code 1, crashing the Redis sidecar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:54:28 +01:00
jgrusewski
0f4631de78 fix(ci): mount training-data PVC and support .dbn.zst in data loader
- Mount training-data-pvc at /data/training in build pods via runner config
- Update real_data_loader to search per-symbol subdirectories (Databento layout)
- Support .dbn.zst (zstd-compressed) files alongside raw .dbn
- Add FOXHUNT_DATA_DIR env var override for CI PVC path
- Extract decode_ohlcv_bars helper (generic over reader type)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:02:55 +01:00
jgrusewski
9cd9fba859 fix(ci): use nvidia RuntimeClass instead of pod_spec for GPU access
Replace pod_spec strategic merge patch (silently not applied) with
runtime_class_name="nvidia". The nvidia RuntimeClass uses the
nvidia-container-runtime which injects GPU drivers, nvidia-smi, and
/dev/nvidia* devices into all containers automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:04:18 +01:00
jgrusewski
f3335d87bb fix(ci): wrap pod_spec GPU patch in spec.containers (Pod-level patch)
Strategic merge patches are applied to the Pod object, not PodSpec.
The patch needs {"spec":{"containers":[...]}} not {"containers":[...]}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:55:22 +01:00
jgrusewski
be7b8bad18 fix(ci): increase poll_timeout for H100 scale-to-zero, fix build-ci-builder rules
- poll_timeout 180s→600s: H100 provisioning takes ~3-5min from scale-to-zero
- build-ci-builder: only auto-run on push with Dockerfile changes (API
  pipelines evaluate changes=true, causing unnecessary kaniko builds)
- check: add needs:[] to decouple from prepare stage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:48:49 +01:00
jgrusewski
041312acc4 fix(ci): add CUDA_COMPUTE_CAP=90 and GPU resource request for H100 builds
bindgen_cuda calls nvidia-smi to detect compute capability, which fails
without GPU device access. Setting CUDA_COMPUTE_CAP=90 bypasses this for
compilation. Pod spec GPU request ensures NVIDIA device plugin injects
/dev/nvidia* for test-time CUDA execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:08:34 +01:00
jgrusewski
10d81b0703 infra(kapsule): consolidate CI builds onto H100 gpu-training pool
Remove orphaned `ci` pool (GP1-XS, 4 vCPU wasted — nothing scheduled to it).
Remove `ci-build` pool (GP1-M) — builds now run on gpu-training (H100-1-80G:
24 vCPU, 240GB, real CUDA). This eliminates the need for CUDA stubs,
separate test-gpu jobs, and ml crate exclusions.

Pool layout after:
  always-on    DEV1-M        (core services, always on)
  gitlab       GP1-XS        (GitLab CE + runner manager, always on)
  gpu-training H100-1-80G    (CI builds + ML training, scale-to-zero)
  gpu-inference L4-1-24G     (trading inference, scale-to-zero)

Build pod limits bumped to 16 vCPU / 64GB (from 6/12GB) to use H100 capacity.
Runner now has `gpu` tag — all tests including ml crate run in single job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:20:26 +01:00
jgrusewski
aca979dd40 fix(ci): revert ci-build pool to GP1-M — stay within vCPU quota
GP1-XL (48 vCPU) exceeds account quota. GP1-M (16 vCPU, 64GB) fits
within current limits while providing sufficient build resources.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:14:54 +01:00
jgrusewski
ebe51f6109 fix(ci): fix 6 GPU test failures, configure test-gpu runner, upgrade ci-build to GP1-XL
Tests 15-16: from_checkpoint now correctly rejects invalid safetensors data
instead of silently using random weights.
Tests 17-20: construct untrained models directly via ::new() instead of
broken from_checkpoint path — tests inference refusal, not checkpoint loading.

test-gpu job: add kapsule+gpu tags for GPU runner targeting, remove TODO.
CI build pool: GP1-M → GP1-XL (48 vCPU, 256GB) — request quota increase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:14:19 +01:00
jgrusewski
e87b7ffb9e infra(kapsule): downgrade CI pool to GP1-M — GP1-L hits vCPU quota
GP1-L (32 vCPU) creation_error due to project vCPU quota limits.
GP1-M (16 vCPU, 64GB) is 2x the previous GP1-S and within quota.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:25:39 +01:00
jgrusewski
ba4802fb59 infra(kapsule): upgrade CI build pool from GP1-XS to GP1-L (32 vCPU, 128GB)
Autoscaling pool for CI runner pods — scales to zero when idle.
32 cores dramatically improves parallel Rust compilation times.
Also cleaned up the manually-created ci-build-v2 GP1-S pool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:19 +01:00
jgrusewski
20a771578f fix(ci): skip GPU-dependent tests on CPU CI nodes, keep CUDA stub for loading
The backtesting crate links libcuda.so.1 at runtime (via candle CUDA).
GPU-less CI nodes need the stub library so binaries can load, but
model_loader tests that actually use CUDA must be skipped. These will
run on gpu-training nodes separately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:59:29 +01:00
jgrusewski
463291115f fix(ci): add libcuda.so.1 stub for test stage on GPU-less nodes
The backtesting crate dynamically links to libcuda.so.1 via candle.
NVIDIA CUDA dev images include stubs at /usr/local/cuda/lib64/stubs/
but only as libcuda.so (not .so.1). Create the symlink both in the
Dockerfile (for future builds) and inline in the test script (for
the current image).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:26:04 +01:00
Administrator
bdcd016f33 ci: install protoc 28.3 from GitHub (Ubuntu protoc too old for proto3 optional) 2026-02-25 09:37:16 +00:00
Administrator
39cfb037ea ci: add libfontconfig1-dev to CI builder (plotters dependency) 2026-02-25 09:24:59 +00:00
Administrator
84f935a70e ci: CUDA CI builder image via Scaleway CR
- Push ci-builder to rg.fr-par.scw.cloud/foxhunt-ci (Kapsule nodes can pull natively)
- Add SCR + GitLab imagePullSecrets to runner config
- Dockerfile.ci-builder unchanged (CUDA 12.4 + Rust 1.89 + protoc + sccache)
2026-02-25 08:54:55 +00:00
Administrator
8a73f47624 ci: pre-baked CI builder image, pipeline prepare stage
- Add Dockerfile.ci-builder (CUDA 12.4 + Rust 1.89 + protoc + sccache + git + lld)
- Add prepare stage to build CI builder image via Kaniko (auto on Dockerfile change, manual otherwise)
- Replace before_script apt-get installs with pre-baked image in .rust-base
- Add git to Dockerfile.service for candle git dependency
- Add sccache stats output to check stage
2026-02-25 08:22:30 +00:00
Administrator
08e4d3720b fix(ci): add git to build deps, runner clone_url to internal service 2026-02-25 08:11:36 +00:00
Administrator
fa0e2a3809 infra: HTTPS wildcard cert, subdomain routing, CI/CD pipeline, Grafana
- Wildcard LE cert for *.fxhnt.ai
- Subdomain routing: git/grafana/prometheus.fxhnt.ai
- CI/CD: Kaniko image builds, sccache, deploy stage
- Grafana with Prometheus datasource
- GitLab node upgraded to GP1-XS (16GB)
2026-02-25 01:18:21 +00:00
jgrusewski
5886ae57a6 chore: remove Gitea CI config and old ci-runner module
Gitea replaced by GitLab CE on Kapsule. CI runner replaced by
GitLab Runner with Kubernetes executor on ci-build node pool.

Removed:
- .gitea/workflows/ci.yaml (Gitea Actions config)
- infra/modules/ci-runner/ (Scaleway VM-based runner)
- infra/live/production/ci-runner/ (Terragrunt live config)

Note: The ci-runner VM instance still needs manual `terragrunt destroy`
before the Scaleway resource can be released.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 01:14:59 +01:00
Administrator
8f98230dc3 fix: .gitignore large files, runner internal URL, proxy buffering
- Add .gitignore entries for .dbn, .safetensors, .onnx and other large binaries
- Point GitLab Runner at internal cluster URL (not external Tailscale)
- Disable nginx proxy_buffering for git clone/push through Tailscale proxy
- Increase socat SSH proxy buffer sizes to 1MB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:09:23 +00:00
jgrusewski
840fe15471 Merge branch 'worktree-gitlab-migration'
GitLab CE migration infrastructure: Helm values, Tailscale proxy,
CI/CD pipeline, GitLab Runner, DNS, K8s manifests, S3 storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:56:29 +01:00
jgrusewski
184c07bb82 Merge branch 'worktree-audit-fixes'
# Conflicts:
#	infra/modules/secrets/main.tf
#	infra/modules/secrets/variables.tf
2026-02-25 00:33:10 +01:00
jgrusewski
86f7f1fa76 fix: comprehensive audit — real brokers, deployment fixes, production safety
Codebase audit identified 23 findings across 4 dimensions (production safety,
code health, deployment readiness, test quality). This commit fixes all of them.

Broker execution layer (was entirely stubbed):
- Real IBKR TWS client via ibapi crate (950+ lines, feature-gated)
- ICMarkets ctrader-openapi now always-on (removed feature flag)
- Real broker routing with health monitoring and exponential backoff reconnect
- Validated against live IB Gateway Docker (6/6 connectivity tests pass)

Deployment blockers:
- Fixed 6 broken Dockerfiles (removed COPY foxhunt-deploy)
- Created foxhunt K8s namespace, secret templates, migration job
- Added liveness probes to all 7 K8s services
- IB Gateway manifest (ghcr.io/gnzsnz/ib-gateway:stable)
- IBKR credentials in Scaleway Secret Manager via Terragrunt
- Fixed port collisions and mismatches across services

Production safety (9 critical + 6 high/medium fixes):
- Asset-class-specific VaR volatility (not flat 2%)
- Real parametric VaR with z-score 95th percentile
- Kyle's lambda regression (100-bar rolling window)
- Per-feature running statistics from historical data
- VWAP-based slippage reference, regime duration tracking
- Real Databento JSON parsing for OHLCV/Trade/Quote

Code health:
- Removed #![allow(dead_code)] from ml, data, config
- Fixed log:: → tracing:: in 4 production files
- Removed dead workspace deps (ratatui, crossterm)

Verified: cargo check --workspace (0 errors), trading_engine 330 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:32:10 +01:00
jgrusewski
9c37d0edbb infra(gitlab): enable Prometheus, SSH proxy, explicit always-on type
- Enable Prometheus server (GitLab chart bundled) on gitlab node pool
- Add socat SSH proxy sidecar (port 2222 → gitlab-shell) to Tailscale proxy
- Remove nginx stream module (not available in alpine) in favor of socat
- Set unlimited nginx client_max_body_size for large git pushes
- Add workhorse extraArgs for API limits
- Explicit always_on_type = DEV1-M in kapsule terragrunt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:29:13 +01:00
jgrusewski
26e0caf67c fix(gitlab): Helm values schema, Tailscale proxy, DNS record
- Fix certmanager: use installCertmanager top-level key (not certmanager.install)
- Fix redis: global.redis.auth.enabled (not password.enabled)
- Remove grafana (not in GitLab chart), remove top-level minio (use global.minio.enabled)
- Add toolbox backup objectStorage config (required when minio disabled)
- Set HPA minReplicas/maxReplicas=1 for single-node DEV1-L
- Add Tailscale proxy (nginx + tailscale sidecar) for private access
- DNS: git.fxhnt.ai → 100.90.76.85 (foxhunt-gitlab Tailscale IP)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:06:10 +01:00
jgrusewski
ad4619ae64 fix(gitlab): rewrite postgres init job to avoid psql \gexec 2026-02-24 23:38:29 +01:00
jgrusewski
b507b1bdea infra: rename root terragrunt.hcl → root.hcl (fix anti-pattern warning)
Terragrunt v0.77+ warns about using terragrunt.hcl as root config.
Rename to root.hcl and update all child includes to use
find_in_parent_folders("root.hcl").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:29:11 +01:00
jgrusewski
020e85af2b infra: add DNS module + rename gitlab.fxhnt.ai → git.fxhnt.ai
Keep existing git.fxhnt.ai hostname for GitLab CE (repoint DNS
to new Tailscale IP after deploy). Add Terraform DNS module to
manage the A record via Terragrunt instead of manual scw CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:26:51 +01:00
jgrusewski
4fb398a531 infra(k8s): switch image refs from Scaleway registry to GitLab registry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:25:05 +01:00
jgrusewski
678f526225 infra(gitlab): Runner Helm values with K8s executor for ci-build pool
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:23:16 +01:00
jgrusewski
24c1b94fb0 infra(gitlab): Helm values for GitLab CE deployment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:23:05 +01:00
jgrusewski
1045b190dc infra(gitlab): postgres init job for GitLab database
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:22:54 +01:00
jgrusewski
1c36941ded infra(kapsule): add gitlab and ci-build node pools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:21:14 +01:00
jgrusewski
ce8eeba6fb infra(storage): add GitLab registry and artifacts buckets 2026-02-24 23:21:06 +01:00
jgrusewski
019bc140d7 fix(ci): increase idle timeout to 60min, check act_runner children
The 15min timeout was too short for Rust builds. Also checks
for act_runner child processes (image pull, git clone phases)
not just Docker containers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:52:28 +01:00
jgrusewski
903bf4fbf6 infra(ci): scale-to-zero with idle shutdown and auto-start watcher
CI runner auto-shuts down after 15min idle (no GITEA-ACTIONS containers).
Gitea server polls every 60s for queued runs and starts instance via SCW API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:33:42 +01:00
jgrusewski
a47e0cb947 infra(ci): add Terragrunt-managed CI runner with Tailscale + act_runner
GP1-S instance (8 vCPU, 32GB, 100GB root) provisioned via Terraform
with cloud-init: Docker CE, Tailscale (pre-auth key), Gitea Actions
runner (act_runner v0.3.0). Security group drops all inbound — access
via Tailscale only. Solves: Kapsule pods cannot reach Tailscale IPs.

Also stores Tailscale API key in SCW Secret Manager for future auth
key generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:15:37 +01:00
jgrusewski
c8f2dacc5f feat(infra): H100 smoketest pipeline — S3 output sync, sccache, registry fix
- Fix container registry region nl-ams → fr-par across all CI jobs
- Add sccache build-args to training image build (no-op fallback for local)
- Add rclone to training Docker runtime for S3 output sync
- Update train.sh: S3 sync on Job completion via rclone env-var config,
  --run-id tracking, evaluate preset for walk-forward evaluation
- Add s3-credentials Secret template (.example, apply via kubectl)
- Add design doc and implementation plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:31:22 +01:00
jgrusewski
5fb84a02f0 feat: training pipeline for all 10 ML ensemble models
- Add standalone training binaries: TGGN, KAN, xLSTM, Diffusion (DBN data)
- Update Dockerfile.training: 6 → 16 binaries (all 10 models + hyperopt + baseline)
- Expand train.sh: 4 → 10 models, fix registry URL and GPU pool nodeSelector
- Add GPU overlay manifests for trading-service and ml-training-service
- Create training data PVC and upload pod manifests
- Expand web-gateway model validation: 4 → 10 types (training + tune routes)
- Extend dashboard: 10 model cards grouped by category (RL/Temporal/Graph/Generative)
- Add training image build job to Gitea CI workflow
- Update GPU taint controller to exclude inference pool from tainting
- Fix job-template nodeSelector: gpu → gpu-training

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:47:50 +01:00
jgrusewski
5380bd9186 infra: split GPU pool into training (H100) and inference (L4), add auto-taint
- Split single H100 GPU pool into two purpose-specific pools:
  - gpu-training: H100-1-80G (€2.73/hr) for 10-model ensemble training
  - gpu-inference: L4-1-24G (€0.75/hr) for cost-effective trading inference
- Add GPU taint controller DaemonSet that auto-taints new GPU nodes
  with nvidia.com/gpu=true:NoSchedule to prevent non-GPU workloads
- Fix service log directory permissions with emptyDir volumes
  (observability init fails creating /app/logs as non-root user)
- Increase postgres max_connections from 25 to 100
  (7 services each requesting connection pools exhausted the limit)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:39:56 +01:00
jgrusewski
3db5c950c7 infra: deploy all services to Kapsule cluster
- Fix DB pool connect/acquire timeouts (50ms→5s) for cluster networking
  (pool-level timeouts, not query timeouts — HFT query timeout stays at 800μs)
- Fix secret key references (DATABASE_PASSWORD→db-password) in all manifests
- Fix api-gateway port (50050→50051) to match actual gRPC listen port
- Fix web-gateway health probe path (/api/health→/health)
- Fix S3 endpoint region (nl-ams→fr-par) in ml-training-service
- Add TLS cert volume mount for ml-training-service
- Add BENZINGA_API_KEY placeholder for backtesting-service startup
- Remove always-on nodeSelector from services (let autoscaler handle)
- Add serve subcommand to ml-training-service container

All 10 pods (3 databases + 7 services) now 1/1 Running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:18:47 +01:00
jgrusewski
f6fb67a58f infra: add build-and-push script and fix Dockerfiles for Scaleway registry
Remove deleted foxhunt-deploy COPY from all Dockerfiles, update sccache
default region to fr-par, add build-and-push.sh for building all service
images and pushing to rg.fr-par.scw.cloud/foxhunt/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:29:43 +01:00
jgrusewski
1441f5fbbb infra: security hardening and autoscale-to-zero for CI/GPU pools
- K8s API ACL restricted to Tailscale CGNAT (100.64.0.0/10) + admin IPs
- CI and GPU pools: min_size=0 with lifecycle ignore_changes on size
  so autoscaler manages actual node count without Terraform drift
- Add harden.sh script for post-provisioning security lockdown
  (ACL setup, exposed service check, bucket ACL verify, registry visibility)
- Update smoke-test.sh default region to fr-par

Security audit results:
- All services ClusterIP only (no LoadBalancer/NodePort)
- No Ingress resources
- Container registry: private
- S3 buckets: HTTP 403 on anonymous access
- Gitea: all ports closed on public IP (Tailscale-only via security group)
- Kapsule nodes: no open ports on external IPs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:46:24 +01:00
jgrusewski
f434309f59 infra: provision Kapsule cluster in fr-par with GPU support
Move all infrastructure from nl-ams to fr-par-2 for H100 GPU availability.
Kapsule cluster provisioned with 3 node pools (always-on DEV1-M, CI GP1-XS,
GPU H100-1-80G), databases deployed (Postgres/TimescaleDB, Redis, QuestDB),
and Tailscale subnet router configured.

Key changes:
- Region: nl-ams -> fr-par-2 (GPU types only available in Paris)
- K8s version: 1.30 -> 1.34 with auto-upgrade
- Block storage: migrate b_ssd -> SBS (scaleway_block_volume)
- Kapsule: add VPC private network, delete_additional_resources
- GPU pool: conditional via enable_gpu_pool variable
- K8s manifests: fix registry URLs, secret key names, PG subPath
- Add terragrunt cache/lockfile to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:35:40 +01:00
jgrusewski
4f85d0d475 infra: add training orchestrator and smoke test scripts
- train.sh: submit GPU training K8s Jobs with presets
  (quick-test, single-model, full-ensemble, hyperopt)
- smoke-test.sh: end-to-end verification of cluster,
  networking, databases, services, and node pools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:27:50 +01:00
jgrusewski
2a3c107b31 infra: add K8s manifests for full paper trading stack
- Tailscale subnet router (zero public access)
- PostgreSQL, Redis, QuestDB (PVCs on always-on node)
- 7 service deployments (trading, api-gw, broker-gw, ml, backtest, agent, web-gw)
- GPU training Job template + idle reaper CronJob
- Block Storage PV/PVC for shared training data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:27:32 +01:00
jgrusewski
56c33cc1c1 infra: add Dockerfiles for services, web-gateway, and GPU training
- Dockerfile.service: unified multi-stage with sccache S3 support
- Dockerfile.web-gateway: Node dashboard + Rust gateway combined
- Dockerfile.training: CUDA 12.4 with H100 target (compute cap 9.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:26:54 +01:00