Commit Graph
1478 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 40681927de test(test-e2e-playwright): prove run_parallel overlap without racing stub jobs
The concurrency tests of run_parallel read overlap from wall-clock stamps that include each stub job's python start-up, so under the 80 xdist workers of make test the jobs serialised by accident (quality-high on store-i18n 1f15f528b3: test_onion_jobs_share_tor_with_each_other 1 != 2; on 1c922d39f3: test_a_freed_slot_is_refilled_without_waiting_a_poll 0.6508 not < 0.6). A stub job can now wait until another job has started, so the overlap the tests claim holds by construction and a serialising scheduler times the waiting job out. The freed-slot test polls every 30 s and bounds the refill at 15 s: the green path never waits a poll, and a scheduler that does misses the bound by twice.

🎯 Fix: 94 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 93
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:39:23 +00:00
kevinveenbirkenbachandClaude Opus 5.5 27f7fb90a0 fix(web-app-gitlab): read the logo set on the stack host only
design_assets renders the logo set only where IS_STACK_HOST holds, but the slurp in tasks/utils/design.yml ran on every swarm host, so the workers failed with 'File not found: /opt/compose/gitlab/design/logo.png' (fork run 42465517680 attempt 6, GitLab#0 job 128710892782). The slurp now runs once delegated to STACK_HOST, as the SSH host key slurp in tasks/01_config.yml does, and its result reaches every host for the platform_admin runner.

🎯 Fix: 93 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:51:05 +00:00
kevinveenbirkenbachandClaude Opus 5.5 85d5ad9984 fix(ci): refuse a priority token that leaves no combination to deploy
A priority row whose role is single_mode under a multi network axis (OpenCloud since 6e39be8383) gets no (mode, network, mesh) combination from axes.assign, so the planner silently dropped it and the run reported green for a selection that never ran (fork run 42411117881, OpenCloud#0). check_pins only rejected pinned networks. Extract the priority pick into pins.priority_picks, shared by axes.assign and cli.meta.ci.validate, and make validate (and the trigger's pre-dispatch check) refuse a priority token for which no pinned variant, or for a bare role no variant at all, has a combination left. Whitelist tokens are not held to this rule.

🎯 Fix: 93 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:55:48 +00:00
kevinveenbirkenbachandClaude Opus 5.5 849e6c77b4 test(sys-svc-compose): keep the prune test ruff-clean and its cache-read suppression on one line
subprocess.run now passes check=False (ruff PLW1510), and the stub-log read sits in its own short assignment so the formatter no longer moves its nocheck onto a closing paren.

🎯 Fix: 99 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:05:11 +00:00
kevinveenbirkenbachandClaude Opus 5.5 c1a9015f47 perf(sys-svc-compose): prune storage-constrained hosts only below a free-space floor
GitLab#3 of fork run 42305502796 died at the deploy cap (update pass rc=-15 after 7525 s; both passes may use 19020 s of the 20400 s budget). The round was storage-constrained by a hair (declared 92.1 GiB against 91.6 GiB free), so the prune handler ran after every compose flush: 55 prunes, 2926 s, up to 116 s each. The job log's host sampler (1928 free-disk samples) shows that those 55 prunes reclaimed only 13.2 GiB in total.

prune.sh now reads the free space of the Docker data root and leaves Docker data alone at or above compose_prune_free_floor_mib (30720). A probe that fails falls through to the prune. Replayed on the sampler series, the floor fires 3 of the 55 prunes (302 s, saving 2624 s) and keeps the lowest free space at the observed 22.8 GiB. Across the eight red rows of the run, the largest growth between two flushes is 34.7 GiB, at the first image pulls only while ~90 GiB are free, and at most 15.3 GiB later. The unconstrained NC#7 already bottoms at 7.9 GiB without any prune. The handler reports changed only when it prunes. test_prune.py covers skip, prune, failed probes and a missing floor; against the old script the skip and the missing-floor cases fail.

🎯 Fix: 85 %
💥 Break: 5 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 81
🚦 Gate: ⛔ fix 85 % < 90

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:22:15 +00:00
kevinveenbirkenbach 55697bade3 Merge commit '9fdec3a2a2' into merge/store-i18n-main-9fdec3a2a2
# Conflicts:
#	roles/web-app-jenkins/templates/casc.yaml.j2
#	roles/web-app-moodle/files/build/moodle-entrypoint.sh
#	roles/web-app-moodle/vars/main.yml
#	tests/unit/python/roles/web-app-moodle/files/build/test_moodle_entrypoint.py
2026-11-07 16:04:34 +00:00
kevinveenbirkenbachandClaude Opus 5.5 a4d0c2806a Merge branch 'fix/svc-db-redis/rotation-shared-consumers' into feature/store-i18n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:30:13 +00:00
kevinveenbirkenbachandClaude Opus 5.5 2f3617f149 Merge branch 'fix/ci/host-metrics-sampling' into feature/store-i18n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:30:12 +00:00
kevinveenbirkenbachandClaude Opus 5.5 f6debb01e7 Merge branch 'feature/variant-split' into feature/store-i18n
variant_split and variant_tree tools with their i8 skills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:30:11 +00:00
kevinveenbirkenbachandClaude Opus 5.5 fa39151f1f fix(svc-db-redis): keep the replaced password valid until the server pass redeployed its consumers
Every shared Redis consumer authenticates as the default user with REDIS_PASSWORD (plugins/lookup/engine.py). The rotation gate between PASS 1 and PASS 2 changes that password, the engine restarts with only the new one, and every consumer deployed later in the pass keeps the old secret. In fork run 42305502796, Nextcloud#7 direct then crash-looped Nextcloud and Weblate from 02:50:31 until the deploy cap killed the job (about 2600-2700 WRONGPASS lines per node, host CPU pressure from 25 % to 80 %).

00_core now reads the password hashes a healthy engine accepts before the restart and adds the replaced ones back after it. Once every server role has redeployed, tasks/stages/02_server.yml retires them before the end-to-end tests: only the current password is left, the ACL log is reset and every client is disconnected. After the tests, a verify step in the test runtimes fails the pass on every authentication Redis refused since the retire, so a consumer that never picked up the new secret still fails loudly. The rotation gate is unchanged. The README no longer describes per-consumer ACL users that never existed.

🎯 Fix: 88 %
💥 Break: 6 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:29:52 +00:00
kevinveenbirkenbachandClaude Opus 5.5 8458f8043a ci(deploy): sample host memory, pressure and top RSS into the rescue diagnostics
Deploy jobs killed by the deploy cap left no record of host memory or CPU pressure, so a slow job could not be told apart from a starved one. Every deploy matrix job now starts a background sampler that appends mem, load, PSI and the ten largest processes by RSS (pid, rss, comm only; never argv or env) to host-metrics.log in the rescue-diagnostics folder every 60 s, which the existing always() upload ships. report_failures now downloads each failed artifact into its own folder, because gh refuses to overwrite and the second artifact of a role was lost, and it reports 'no rescue snapshot' only when no artifact holds capture content below <distro>/<app>.

Accepted MEDUSA false positive: B603 on the list-argv gh subprocess call in cli/meta/ci/report_failures.py.

🎯 Fix: 90 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 87
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:20:56 +00:00
kevinveenbirkenbachandClaude Opus 5.5 28ef30a5f6 feat(ci): variant_split and variant_tree tools with their i8 skills
variant_split snapshots the job list of a tree under a fixed discovery seed, compares two snapshots for lost coverage (uncovered children, false credits by token, coverer and reason, lost consumer and per-variant edges, dark pins) and prices a split from per-task wall times of one runner. variant_tree prints the role tree one variant really deploys as text, JSON or YAML and refuses a variant index the role does not declare. i8-variant-split and i8-variant-tree describe how agents use them; a variant-limit escape stays the operator's decision.

Accepted MEDUSA false positives: MEDUSA-INF-SCAN-1703 on the two --out report writes in cli/meta/ci/variant_split/__main__.py, B105 on the infra_per_pass cost key in cost.py, and MEDUSA-INF-SCAN-1596 on an empty edges list in rounds.py.

🎯 Fix: 90 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:19:38 +00:00
kevinveenbirkenbach 586d719d0e Merge pull request #1404 from infinito-nexus/fix/web-app-moodle/nfs-bootstrap-race
fix(web-app-moodle): stand a second bootstrap down and gate the ownership walks on a settled one
2026-11-07 16:08:51 +01:00
kevinveenbirkenbachandClaude Opus 5.5 7b9ceb0bef fix(web-app-discourse): size the web workers by the host's vCPUs and RAM
UNICORN_WORKERS was hard-coded to 8. On a 4 vCPU / 16 GB CI runner that holds about 4 GB of web-worker RSS beside every other application of the round; the Nextcloud#7 rescue snapshot of fork run 42305502796 shows 8 workers of about 500 MB and 6.8 GiB of swap in use. The count is now clamp(min(2 * vCPU, RAM // 4 GiB), 2, 8): upstream's setup-wizard rule of two per vCPU up to 8, capped at one worker per 4 GiB of host RAM because the host is shared. The CI runner gets 3; hosts with at least 4 vCPU and 32 GiB keep 8. Only the env changes, so the launcher recreates the container without a rebuild (03_launcher.yml:50 drops env from the bootstrap digest).

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 15:05:49 +00:00
kevinveenbirkenbachandClaude Opus 5.5 c6b92bb711 test(web-app-jenkins): parse the rendered casc through utils.cache.yaml
tests/lint/repository/yaml/test_no_direct_calls.py rejects direct
yaml.safe_load calls; test_casc_oidc_scopes.py from 68c9aa5b3a had one.
It now uses load_yaml_str. The lint and both scope tests pass.

🎯 Fix: 97 %
💥 Break: 1 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 96
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 08:50:57 +00:00
kevinveenbirkenbachandClaude Opus 5.5 d4388a6a94 Merge branch 'fix/web-app-matrix/mdad-daemon-nofile' into feature/store-i18n
🎯 Fix: 92 %
💥 Break: 6 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ⛔ break 6 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 07:50:30 +00:00
kevinveenbirkenbachandClaude Opus 5.5 e16681ac80 fix(web-app-matrix): bound the open-files limit of every MDAD nested container
containerd inside the matrix-mdad runner runs with LimitNOFILE=infinity, so every nested container without its own --ulimit inherits 2147483584 open files. uwsgi in matrix-synapse-usage-exporter sized its fd table from that and pinned 16 GiB in CI run 42239638356; ccaa075b54 capped that one container. The inner daemon.json now sets default-ulimits nofile 1048576 soft and hard, so any other nested service that sizes a table from RLIMIT_NOFILE stays bounded too. A container's own --ulimit still wins, so the exporter keeps its 65536.

Rendered daemon.json passes dockerd --validate (29.8.2); a string limit is rejected (negative control). The new unit test pins integer Soft/Hard within 65536..1048576 and fails on the base without the key. No live nested-dockerd run: the operator merges sidetrees straight into store-i18n, whose CI proves it; break 6 % accepted by the operator.

🎯 Fix: 92 %
💥 Break: 6 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ⛔ break 6 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 07:49:13 +00:00
kevinveenbirkenbachandClaude Opus 5.5 727ba45626 fix(web-app-moodle): stand a second bootstrap down and gate the ownership walks on a settled one
Main run 42235551402 (5d78f3ae7d), job 128223280156 Moodle#2 (swarm): 03_ownership.yml:24 'find -exec chmod 0644' failed with hundreds of 'No such file or directory' under public/lib/yuilib. moodle_code is one NFS export shared by three replicas and cron, all on release 5.3.0. The bootstrap lock lives on that export and does not serialize across NFS clients (workers log 'flock: 9: Input/output error'). So a second same-release bootstrap swapped the tree in while the deploy walked it; the rescue listing at 02:22:34 shows neither .bootstrap.stage nor .bootstrap.done, the window between the stage rmdir and the sentinel write.

Port of two store-i18n commits by Stage; 5c3749c437 and 7bc5822421 are already on main:
- moodle_bootstrap_code_dir re-reads the sentinel after its copy and stands down when another container finished the same release (cherry picked from commit db1a27b3f2).
- The ownership walks wait until the stage is gone and the sentinel holds the expected release, prune the stage and batch through -exec +; stage and sentinel paths come from vars/main.yml via ARG/ENV (cherry picked from commit 9a5965b87c).

The rationale header the port added to moodle-entrypoint.sh and a stray comment line in 03_ownership.yml are dropped. test_moodle_entrypoint: 8/8 green; without the stand-down test_a_sibling_that_finishes_first_keeps_its_tree fails.

🎯 Fix: 88 %
💥 Break: 6 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: ~100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 06:55:17 +00:00
kevinveenbirkenbachandClaude Opus 5.5 68c9aa5b3a fix(web-app-jenkins): pin the OIDC scopes to openid profile email
Main run 42235551402 (5d78f3ae7d), job 128223280276 Jenkins#0: the OIDC redirect ended at /securityRealm/finishLogin?error=invalid_scope. With a wellKnown server configuration and no scopesOverride, oic-auth requests every entry of the discovery document's scopes_supported (OicServerWellKnownConfiguration). Keycloak rejects the whole request when one scope is not a default or optional scope of the client. The requested list minus the scopes of the infinito.nexus client leaves exactly kc-admin-api, a Keycloak built-in assigned only to admin-cli and security-admin-console.

casc.yaml.j2 now pins scopesOverride to 'openid profile email'; groups keeps arriving as a default client scope. The new unit test renders casc.yaml.j2 and the shared client template default.json.j2 and asserts every requested scope is one the client carries; without the override both tests fail.

🎯 Fix: 91 %
💥 Break: 7 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: ~100 %
🏆 Score: 85
🚦 Gate: ⛔ break 7 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 06:53:47 +00:00
kevinveenbirkenbachandClaude Opus 5.5 86998924d9 fix(ci): name the gh failure when the retrigger finds no inputs
The retrigger guard aborted with 'its job log records no inputs' while _gh(check=False) dropped gh's stderr, so a transient log read failure and a moved Inputs block looked the same. _gh now takes an errors list instead of check: given, a failure appends gh's last stderr line with URL query strings cut (job logs redirect to signed blob URLs) and yields an empty string; absent, it aborts as before. inputs_from_jobs passes such a list through, and dispatched_priority appends how many log reads failed and the last error, or that none failed.

MEDUSA findings accepted as false positives (unchanged lines, no SQL or untrusted argv; independent skeptic review): MEDUSA-FTSCAN-231 runs.py:80 and runs.py:318, B603 gh.py:27 and gh.py:79, B105 runs.py:36.

🎯 Fix: 95 %
💥 Break: 2 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 93
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 06:12:50 +00:00
kevinveenbirkenbachandClaude Opus 5.5 ccaa075b54 fix(web-app-matrix): cap the usage exporter's open-files limit
MDAD's synapse-usage-exporter runs uwsgi with an HTTP router that writes an 8 byte slot per possible file descriptor, sized from RLIMIT_NOFILE. Inside the matrix-mdad runner the nested daemon hands its containers the kernel maximum of 2147483584, so the exporter pinned about 16 GiB of RAM and swap and the swarm watchdog aborted WordPress#0 and Nextcloud#7 in run 42239638356. The addon now declares config.nofile 65536 and the role passes it to MDAD as --ulimit=nofile=N:N for that container; a local compose deploy shows the exporter at nofile 65536 and 38 MiB.

🎯 Fix: 97 %
💥 Break: 2 %
💸 Cost: 1 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 100
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:00:16 +00:00
kevinveenbirkenbachandClaude Opus 5.5 2284d90078 refactor(sys-token-store): include the hand-over so the write block stays at three tasks
4c682fbdc1 grew the when-gated write block of write_many.yml to four children, which test_no_oversized_ansible_block and test_block_size reject, and its integration test read the plays' views uncached. The hand-over and the writer's own merge now live in tasks/utils/hand_over.yml, included from the block with no_log applied once; the order and the lockstep behaviour stay the same (re-proven on ansible-core 2.20.2 and 2.21.5). The integration test reads through utils.cache.files, and a unit test pins the masking of every task that carries the merged store.

🎯 Fix: 95 %
💥 Break: 2 %
💸 Cost: 104 %
⏱️ Time: 107 %
🛡️ Security: 100 %
🏆 Score: 84
🚦 Gate: ⚠️ cost 104 % > 100, time 107 % > 100: ask the operator before the commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:29:30 +00:00
kevinveenbirkenbachandClaude Opus 5.5 4c682fbdc1 fix(sys-token-store): hand a written store to every reader on its stack host
Swarm workers loaded sys_token_store_users before the manager wrote new tokens in pass 1, so they rendered msmtprc with an empty password (CI run 42128590251, WordPress#0). write_many.yml now notes once, before the block, whether the merge changed the store; delegates the merged store to every play host that shares the writer's STACK_HOST; and re-sets the writer's own merge afterwards, so a peer writer's hand-over cannot overwrite it. Verified with lockstep plays on ansible-core 2.20 and 2.21.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 108 %
⏱️ Time: 116 %
🛡️ Security: 100 %
🏆 Score: 71
🚦 Gate: ⚠️ cost 108 % > 100, time 116 % > 100: ask the operator before the commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:26:26 +00:00
kevinveenbirkenbachandClaude Opus 5.5 0aac803507 perf(web-app-discourse): read the addon state back in the apply runner
Chat integration and Mermaid booted Rails twice each, once to apply and once to read back (10.9 s and 11.1 s for the read-backs on echoserver 94767d4). Each verify script now runs after its apply script in the same rails r and starts with SiteSetting.refresh!, so it reads what the database holds, as 2b11a7e9fe does for the site settings. The test stub keeps a dropped write in memory, so the read-back test fails without the refresh.

🎯 Fix: 90 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:10:49 +00:00
kevinveenbirkenbachandClaude Opus 5.5 b5e299da1c test(install): fold the assertRaises into the patch context (SIM117)
🎯 Fix: 99 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:54:52 +00:00
kevinveenbirkenbachandClaude Opus 5.5 7e48e7fe2c fix(install): reinstall node_modules when a declared devDependency is missing
install-lint only checked that @playwright/test existed, so node_modules installed before 1c5e5a4fe1 never received @axe-core/playwright and lint-playwright failed with Cannot find module. The guard now compares package.json's devDependencies with node_modules and runs npm ci when one is absent.

🎯 Fix: 93 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:47:07 +00:00
kevinveenbirkenbachandClaude Opus 5.5 c23f0f0d29 Merge five sidetrees into feature/store-i18n
- fix/web-app-docs/codeql-xss (6e045bced8, 92/3): redirect header splitting, DOM-text navigation
- fix/cli/codeql-file-modes (20fae00bdf, 95-97/1-3): owner-only lock and marker files, explicit legend strings, mirror shim import
- fix/roles/codeql-js-hygiene (787f8d9a6d, 92-97/2-3): crypto wrong password, log sanitizer shape, fillStyle save/restore
- fix/svc-ai-agent-broker/codeql-ssrf (eeb9b22ae4, 92/4): platform and relay path from server-side lists, named error paths
- fix/test-e2e-playwright/concurrency-test-load (eb6940a9ad, 95/1): concurrency proven from the trace (gate pass 2 failure)

CodeQL PR #625 alerts #1079 and #957 stay false positives for the operator to dismiss.

🎯 Fix: 92-99 %
💥 Break: 1-4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 88-98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:24:11 +00:00
kevinveenbirkenbachandClaude Opus 5.5 eb6940a9ad test(test-e2e-playwright): prove scheduler concurrency from the trace, not the wall clock
Gate pass 2 on store-i18n 9154a03fed failed test_jobs_run_concurrently_within_the_cap_and_keep_their_own_results at 2.7046 s against a 2.7 s bound: five interpreter starts under full host load. The peak of 2 read from the job trace already proves the overlap and the cap independent of load, and test_a_freed_slot_is_refilled_without_waiting_a_poll covers prompt refills, so the wall-clock bound goes.

🎯 Fix: 95 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:23:36 +00:00
kevinveenbirkenbachandClaude Opus 5.5 eeb9b22ae4 fix(svc-ai-agent-broker): select platform and relay path from server-side lists
py/partial-ssrf on PR #625 (agents.py:195, :250, server.py:123): the chat body's model now selects its entry from AGENT_PLATFORMS through agents.pick, so the agent host is built from the server-side key; a list or dict model answers 404 instead of a TypeError. The relay forwards only /v1/chat/completions and /v1/models with the broker's key; any other path answers 404 and logs relay_refused (the two paths are the ones the relay logs show in use).

py/empty-except (agents.py:201, :258): the health wait names its last error in the timeout, and the model-list fallback logs agent_model_fallback through the log function the server passes in.

Tests cover pick, six refused relay paths and both error paths; each fails against the old code.

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:18:22 +00:00
kevinveenbirkenbachandClaude Opus 5.5 dcfb220dca test(svc-ai-agent-broker): let the connect fake run Engine.__init__
py/missing-call-to-init on PR #625 (test_engine.py:110): _ConnectEngine never ran the base initializer; it now does, and a new test checks the base state.

🎯 Fix: 99 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:17:39 +00:00
kevinveenbirkenbachandClaude Opus 5.5 20fae00bdf refactor(cli): import ImageRef from its module instead of the mirror shim
py/unused-import on PR #625 (mirror/model.py:10): the module only re-imported ImageRef from utils.docker.image.discovery. providers.py and the four mirror tests import it from there, as wait/__main__.py already did; no other importer exists.

🎯 Fix: 97 %
💥 Break: 2 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 95
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:07:20 +00:00
kevinveenbirkenbachandClaude Opus 5.5 0914910df5 fix(cli): make the architecture legend's split strings explicit
py/implicit-string-concatenation-in-list on PR #625 (architecture.py:147, :151): one table row and one paragraph were split over lines without parentheses; both are now parenthesized, likewise the f-string row ruff ISC004 flags. The new test pins four two-cell rows and one paragraph and fails with a comma inserted.

🎯 Fix: 97 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 96
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:04:48 +00:00
kevinveenbirkenbachandClaude Opus 5.5 ae9907df81 fix(test-e2e-playwright): create the tor reader lock owner-only
py/overly-permissive-file on PR #625 (run_parallel.py:287): tor.lock was created 0o644; the scheduler and wait_onion_reachable.sh both run as root under become. The new test sets umask 0 and fails at 0o644.

🎯 Fix: 95 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 92
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:03:10 +00:00
kevinveenbirkenbachandClaude Opus 5.5 8c199a915d fix(cli): create role markers and the placement lock owner-only
py/overly-permissive-file on PR #625 (marker.py:60, placement.py:182): both files were created 0o644; only the owning user's pull, clean and list read them. The new tests set umask 0 and fail at 0o644.

🎯 Fix: 95 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 92
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:02:47 +00:00
kevinveenbirkenbachandClaude Opus 5.5 6e045bced8 fix(web-app-docs): encode line breaks in redirects, navigate by built URLs
- py/http-response-splitting (server.py:87): _redirect percent-encodes CR
  and LF in the Location header
- js/xss-through-dom (versions.js:82, languages.js:25): both switchers
  navigate to the URL they built instead of select.value

The new cases in test_server.py, versions.test.js and languages.test.js
fail without the fix and pass with it.

🎯 Fix: 92 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:54:27 +00:00
kevinveenbirkenbachandClaude Opus 5.5 2fc48cadbb Merge two operator-approved sidetrees into feature/store-i18n
- fix/sys-dns-cloudflare-records/api-base-url (d301a4d3c3, 95/1): test-external red in run 42056917806, the bare Cloudflare API base answered 404
- fix/web-app-pgadmin/test-cached-reads (0af21e62ad, 97/1): test-lint red in run 42056917806, uncached and direct YAML reads in the pgAdmin test

🎯 Fix: 95-97 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 94-96
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:25:07 +00:00
kevinveenbirkenbachandClaude Opus 5.5 0af21e62ad test(web-app-pgadmin): read project files through the cached readers
test_no_uncached_file_reads and test_no_direct_yaml_calls failed test-lint in run 42056917806. env.j2, manager_ops.yml and the realign script now come from utils.cache; the before/after read of the tempdir SQLite file keeps a direct read with the cache-read opt-out, since a cached read would hide a write.

🎯 Fix: 97 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 96
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:20:42 +00:00
kevinveenbirkenbachandClaude Opus 5.5 d301a4d3c3 fix(sys-dns-cloudflare-records): root the API constant at /zones
The bare https://api.cloudflare.com/client/v4 answers HTTP 404, so test_urls_reachable failed test-external in run 42056917806. Every request of the plugin goes below /zones; the constant now names that path, which answers 403 (alive for the URL test), and the requested URLs stay byte-identical.

🎯 Fix: 95 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:18:01 +00:00
kevinveenbirkenbachandClaude Opus 5.5 735308266e Merge two operator-approved sidetrees into feature/store-i18n
- fix/tests/store-i18n-lint-red (1205c4a2ae, 95/1): clears the raw-walk,
  project-root and env-key lint failures store-i18n would have pushed
- fix/sys-stk-front-proxy/body-filter-fold (4984f506c7, 92/2): the
  body_filter Lua is written in the vhost's single stack_host_contents
  call (about 184 s echoserver and 174 s galaxyserver per deploy,
  projected), and stack_host_contents compares bytes

🎯 Fix: 92-95 %
💥 Break: 1-2 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 90-94
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:37:37 +00:00
kevinveenbirkenbachandClaude Opus 5.5 4984f506c7 perf(sys-stk-front-proxy): write the body_filter Lua in the vhost's single contents call
Every front-proxy vhost wrote its body_filter Lua through a separate
stack_host_template task in sys-front-inj-all: two remote round trips when
unchanged (stat + file) and four to five when changed, 191 s over 54 vhosts
per echoserver deploy and 179 s over 48 per galaxyserver deploy. The Lua
now joins http.conf and the vhost in the Deploy task's stack_host_contents
call, written first as before, rendered through template_shallow with
application_id and domain pinned to the vhost.

- sys-front-inj-all/tasks/main.yml no longer writes the Lua; the new
  standalone.yml includes it and keeps the separate write for matomo's
  re-inject path, which has no vhost Deploy task
- stack_host_contents compares bytes instead of a universal-newline text
  read, so content with a CR stops reporting changed on every run and an
  existing non-UTF-8 file is replaced instead of crashing the module
- a new required diff_paths argument keeps --diff for the Lua only; vhost
  and http.conf content stay out of the diff

A Lua-only change still reports changed and fires reload-openresty once;
the served probe and the fallback redeploy keep their old triggers. Check
mode is unchanged.

Tests: the real stack_host_template action against the fold expression
across 99 roles and TLS/Tor/injection scenarios, byte-identical, with
trim and newline negative controls; owner, group and mode parity with the
copy module; byte compare with CR and non-UTF-8 files; the wiring of both
callers. The saving (about 184 s and 174 s per deploy, net) is projected
until the next deploys.

🎯 Fix: 92 %
💥 Break: 2 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:31:05 +00:00
kevinveenbirkenbachandClaude Opus 5.5 1205c4a2ae fix(tests): clear the four lint failures store-i18n would push
- test_no_raw_project_walk: the Synapse single-writer test and the
  compose single-writer lint walked role task dirs with rglob; both now
  filter the cached iter_project_files walk (the same 1283 role task
  files), and the Synapse test asserts it found any
- test_project_root_import: copy_batch's traversal guard rejected '..'
  through a banned literal; it now compares posixpath.normpath("/" + name)
  with "/" + name, which under the preceding conditions holds exactly when
  no part is '..', and new cases cover an interior a/../b and a
  nested/../../ escape
- test_used_keys_declared: the resolve test embeds the old task text,
  whose 'INFINITO_DESIGN' is a JS marker, not an environment key; marked
  with a same-line nocheck on the operator's approval

🎯 Fix: 95 %
💥 Break: 1 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:24:20 +00:00
kevinveenbirkenbachandClaude Opus 5.5 6d4e5f7c59 Merge feature/ci/wordpress0-playwright-parallel into feature/store-i18n
- feature/ci/wordpress0-playwright-parallel (de40211272, 88/6, merged on
  the operator's word below the gate): roles that opt in through
  playwright.parallel (WordPress) run independent Playwright suites in
  parallel under a CPU-pressure gate, with dependency deferral, an onion
  cap and Tor lock, crash counting and per-job artifacts; roles without
  the opt-in run as before. The next push's WordPress#0 CI run is its
  live proof.

🎯 Fix: 88 %
💥 Break: 6 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5; merged on the operator's word, CI is the live proof

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:11:52 +00:00
kevinveenbirkenbachandClaude Opus 5.5 de40211272 perf(test-e2e-playwright): run independent Playwright suites in parallel under a pressure gate
WordPress#0 runs 18 Playwright suites one after another and exceeds its
CI time budget. A role that opts in through playwright.parallel in its
meta/tests.yml (WordPress: [3, 1, 1] per variant) now hands its suites to
an async scheduler, run_parallel.py, after staging:

- a suite starts only when its dependencies finished here (Keycloak first)
  and the host's CPU pressure, avg10 and avg60, stayed at or below 36 for
  30 s; the first container always starts and running ones are never
  killed
- onion suites share their own cap of 3 and a Tor lock with a
  recreate-pending marker; a probe that times out on the lock re-probes
  the current circuits and, failing that, leaves only that app to the
  serial path
- an app whose dependency was dropped is deferred, and every deferred app
  gets the serial first pass before the existing retry, in round order
- each job gets --oom-score-adj=1000 instead of a memory limit; crashed
  pages, closed browsers and killed workers are counted and named in the
  per-app timing line
- each job's result.json, stdout and stderr land in run-parallel/ and are
  archived with the reports
- the async ceiling is the serial worst case per app and reachable
  network, curl's per-attempt max time included (now
  TEST_E2E_PLAYWRIGHT_ONION_PROBE_MAX_TIME); it stays above the CI step
  timeout and never raises a budget

Roles without the opt-in run exactly as before; the onion probe script
with an empty lock behaves byte for byte like the previous one.

Tests: 126 role unit tests and 30 negative controls cover the gate
window, admission order, onion cap and lock, marker cleanup, the bounded
wait, dependency deferral, crash counting, artifact collection, the
ceiling for the real WordPress#0 app list, and serial equivalence. No CI
run has exercised it yet; the next push is its live proof.

🎯 Fix: 88 %
💥 Break: 6 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5; committed and merged on the operator's word, CI is the live proof

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:10:54 +00:00
kevinveenbirkenbachandClaude Opus 5.5 882e9c5062 perf(web-app-pgadmin): skip the create_app boot when the administrator is in sync
The realign task booted pgAdmin's whole Flask application on every deploy,
about 97 s on galaxyserver at cpus 0.2, even when nothing had drifted. A
stdlib pre-check now reads pgAdmin's SQLite configuration database
read-only and skips the boot only when exactly one active, unlocked
internal account with the Administrator role carries the declared e-mail
and its stored pbkdf2_sha512 hash recomputes from the declared password the
way Flask-Security 5.8.3 hashes it (NFKD, HMAC-SHA512 with the salt from
the keys table, libpass's round and salt limits). Every other state, an
unreadable or locked database, an unknown hash scheme, or a configuration
database moved out of SQLite by environment, falls through to the
unchanged create_app path.

The administrator password no longer reaches the host's /bin/sh argv
through the task environment: it arrives base64-encoded on the first stdin
line and is passed into the container by name only, so any byte sequence
survives and no fragment can reach stderr.

Tests: the pre-check agrees with converge against passlib; hash, role, key,
lock and duplicate-account drift, an over-long salt and a configuration
database override fall through; the rendered task runs through /bin/sh
with a password holding a newline, quotes, $, a backslash and unicode;
negative controls for each guard. The saving is not measured by a timed
deploy yet.

🎯 Fix: 90 %
💥 Break: 4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:28:17 +00:00
kevinveenbirkenbachandClaude Opus 5.5 5556dabbb8 Merge perf/plugins/template-shallow-shared into feature/store-i18n
- perf/plugins/template-shallow-shared (f974ab3832, 90/3): template_shallow
  moves from web-opt-rdr-domains to plugins/lookup, and the nine template
  lookups of the design, dashboard, logout and matomo injectors render
  through it instead of deep-copying every task variable per call

🎯 Fix: 90 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 87
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:32:54 +00:00
kevinveenbirkenbachandClaude Opus 5.5 f974ab3832 perf(plugins): share template_shallow and render the front injectors through it
ansible-core's template lookup deep-copies every task variable on each
call. The role-local template_shallow of web-opt-rdr-domains, which hands
the builtin a top-level copy whose values stay shared, moves to
plugins/lookup so every role can use it. The nine template lookups of the
front injectors switch to it: six in sys-front-inj-design and one each in
sys-front-inj-dashboard, sys-front-inj-logout and sys-front-inj-matomo.
None of the switched templates or the lookups they call writes into the
task variables.

Tests:
- the plugin renders like the builtin, never deep-copies the caller's
  values and leaves the caller's variables unchanged
- for every role with design sources, clearnet and onion vhosts, with and
  without a logo, the design facts are identical to the builtin's, or it
  fails with the identical error
- the dashboard, logout and matomo code renders identically
- the redirect loop of web-opt-rdr-domains still renders through the
  shared plugin

The saving per deploy is not measured by a timed deploy yet.

🎯 Fix: 90 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 87
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:31:23 +00:00
kevinveenbirkenbachandClaude Opus 5.5 75693c42e7 Merge two operator-approved sidetrees into feature/store-i18n
- perf/ci/wordpress0-arm64-pass2 (7b412ffa76, 90/4): the Matrix
  ansible-flavor Synapse vhost has one writer, so it no longer changes
  and restarts OpenResty in every swarm pass (WordPress#0)
- fix/sys-dns-cloudflare-records/none-want-group (d96fe45650, 93/3): a
  record set with an entry the converged check cannot judge stays
  pending instead of flipping

🎯 Fix: 90-93 %
💥 Break: 3-4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86-90
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:55:24 +00:00
kevinveenbirkenbachandClaude Opus 5.5 d96fe45650 fix(sys-dns-cloudflare-records): keep a record set pending when one entry cannot be judged
cloudflare_records_converged skipped an entry its expected() rejects (a
TXT value given as an int, a ttl of null) when building the per-owner
declarations, yet cloudflare_dns still writes for such an entry. In
[TXT t 12345, TXT t "12345" ttl 300] the second entry was judged
converged and skipped while the first was applied with ttl 1, so the
record flipped on every run. A record set declared more than once now
stays pending as a whole when any of its entries cannot be judged, like
the hazardous solo and absent entries already do. No producer in the
repository builds such a list today.

🎯 Fix: 93 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:54:49 +00:00
kevinveenbirkenbachandClaude Opus 5.5 7b412ffa76 perf(web-app-matrix): write the Synapse vhost once instead of twice per deploy
In the ansible flavor the Synapse vhost had two writers: the shared
front-proxy include rendered proxy.conf.j2 for the Synapse domain, and a
second task rendered flavor/ansible/synapse.conf.j2 to the same file
right after it. The two contents differed, so the file changed on every
deploy and notified restart-openresty, which in swarm is a
service update --force of OpenResty in every pass (WordPress#0, fork run
41926377524).

proxy.conf.j2 now includes the ansible-flavor Synapse template for the
Synapse domain itself, and the second task is gone. The rendered vhost is
identical to the former final content across TLS, Element Call,
clearnet and onion, so the 8448 listener and the Matrix RTC locations
stay. A real change now goes through the front-proxy's reload, which
reaches OpenResty in swarm since 7e5da9422c.

🎯 Fix: 90 %
💥 Break: 4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:51:23 +00:00
kevinveenbirkenbachandClaude Opus 5.5 bf1b1f2769 fix(web-app-fider): pass BASE_URL without a trailing slash
Fider v0.39.0 accepts an OAuth redirect only when its origin equals
OAuthBaseURL (isKnownOAuthOrigin compares u.Scheme + "://" + u.Host with
it), and in single-host mode OAuthBaseURL is BASE_URL unchanged. The tls
lookup's url.base ends with "/", so the comparison never matched and
every Keycloak login was rejected. Main run 41925343136 (2df12002c0),
Fider#0 compose clearnet centos/ext4: playwright.spec.js:56 and :88 find
no .c-menu-user after SSO, and the rescue fider.log shows 11x "OAuth
callback state for provider _keycloak points at an unknown origin.
Aborting sign in process." The image is the mirrored getfider/fider
stable, refreshed 35 hours before the run.

The tls lookup now also returns url.origin, the base URL without its
slash (the lookup hunk of 44aaa68924 on feature/store-i18n), and the
Fider env passes it as BASE_URL (d531d35823 there). url.base keeps its
value, so no other consumer changes, and both lines match the
store-i18n side for a conflict-free merge.

| 🎯 Fix | 💥 Break | 🌱 Efficiency | ⚡ Speed | 🛡️ Security | 🏆 Score | Gate |
| --- | --- | --- | --- | --- | --- | --- |
| 88 % | 3 % | – | – | ~100 % | 85 | ⛔ fix 88 % < 90 |

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:42:16 +00:00