categories.yml moves from roles/ to the repository-root meta/, joining packages.yml. utils/roles/categories.py owns its location plus the tree-walking helpers that were stranded in entity/name.py, so no caller spells the path or duplicates the lookup.
meta/distros.yml becomes the SPOT for every distribution: os_family, Galaxy platform name, dev-environment base image, and the pkgmgr/pkgmgr-virgin/environment reference templates. It replaces seven scattered copies (DISTRO_FAMILY, VALID_DISTROS, _DISTROS, _CANONICAL_PLATFORMS, two shell arrays, a Makefile literal). INFINITO_DISTROS is no longer a static default but generated by utils/env/handlers/infinito/distros.py; the shell consumers read it through the env layer.
Drops dead CLI surface found along the way: --categories-file and --roles-dir on the provision CLI plus three unused parameters of generate_dynamic_inventory, and the positional roles_file of the invokable categories command.
tests/lint/repository/test_distros_spot.py pins the copies that cannot call the SPOT (workflow literals, docs table) and forbids composing a pkgmgr reference outside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sandbox tier carried no check of its own. Three agent replicas once ran on
the shared kernel while every test stayed green, because nothing between the
runtime selection and the running container ever asked what the container
actually got. Each guard below sits where the promise first breaks.
A node is declared a sandbox target by a label that routes every sandboxed
service to it. It now has to prove it can carry them first: the node reads its
own default runtime and refuses the label while that is still runc. Doing it
before the label rather than after is the point, since the label is what makes
the promise.
Compose has no node-wide default to inspect, so the agent roles read the
runtime the container actually received once the stack is up. The check is
skipped in embodied mode, where the runtime is dropped on purpose because a
microVM between the agent and the hardware it drives defeats the point, and it
keys on the same services.kata switch that already drives the placement
constraint and the compose pin.
A runtime is only as good as the template around it. The new lint refuses a
privileged container, a host network, pid, ipc or user namespace, and a host
docker socket bind in any role that declares a kata service. It fails when it
finds no sandboxed role at all, so renaming the service cannot silently retire
the guard.
It caught its own subject on the first run: both agent templates emitted
privileged: true next to the device allowlist, which grants every device and
made the allowlist decorative. The requirement is explicit that no device
outside the allowlist reaches the agent, so the flag is gone and the role
README no longer claims a limit that was not enforced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
categories.yml moves from roles/ to the repository-root meta/, joining packages.yml. utils/roles/categories.py owns its location plus the tree-walking helpers that were stranded in entity/name.py, so no caller spells the path or duplicates the lookup.
meta/distros.yml becomes the SPOT for every distribution: os_family, Galaxy platform name, dev-environment base image, and the pkgmgr/pkgmgr-virgin/environment reference templates. It replaces seven scattered copies (DISTRO_FAMILY, VALID_DISTROS, _DISTROS, _CANONICAL_PLATFORMS, two shell arrays, a Makefile literal). INFINITO_DISTROS is no longer a static default but generated by utils/env/handlers/infinito/distros.py; the shell consumers read it through the env layer.
Drops dead CLI surface found along the way: --categories-file and --roles-dir on the provision CLI plus three unused parameters of generate_dynamic_inventory, and the positional roles_file of the invokable categories command.
tests/lint/repository/test_distros_spot.py pins the copies that cannot call the SPOT (workflow literals, docs table) and forbids composing a pkgmgr reference outside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
package_install resolves ids against the including role, not the role
owning the task file. web-app-mailu and sys-svc-mail-msmtp both include
sys-svc-mail-smtp's disable/enable tasks, so postfix has to live in the
shared root registry; only the owning role had declared it, which aborted
the mailu deploy.
The new lint walks cross-role task includes and reports every id the
including role cannot reach, so this fails in seconds instead of hours
into a deploy. It flags both call sites when the fix is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
package_install resolves ids against the including role, not the role
owning the task file. web-app-mailu and sys-svc-mail-msmtp both include
sys-svc-mail-smtp's disable/enable tasks, so postfix has to live in the
shared root registry; only the owning role had declared it, which aborted
the mailu deploy.
The new lint walks cross-role task includes and reports every id the
including role cannot reach, so this fails in seconds instead of hours
into a deploy. It flags both call sites when the fix is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the `# syntax=docker/dockerfile:1` line from the root Dockerfile and
add a lint test that keeps it out of every Dockerfile in the repo.
The directive makes BuildKit pull an external frontend image from Docker
Hub at build time. That tag floats to the newest 1.x release, Podman and
Buildah have no frontend dispatch and ignore it entirely, and the pull
turns every build into a network dependency that fails whenever Hub is
unreachable or rate-limits the runner.
The root Dockerfile used no frontend-only feature, so the build behaviour
is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the raw package modules at 76 call sites with package_install, moves
each role's distribution mapping into its own meta/packages.yml, and turns the
inventory PACKAGES variable from source-keyed literal names into a list of
declared package ids. The lint that forbids the raw modules is enabled here,
because the migration burns it down from 76 offenders to zero.
Removes sys-aur, sys-aur-install and sys-package. The AUR build user, the
passwordless pacman rule and the inventory-driven install are part of the plan
the action plugin executes, so the roles no longer carry them.
Declaring the mappings surfaced breakage the raw modules had hidden: on Debian
the `which` binary comes from debianutils, jrnl is in no Debian or Ubuntu
suite, chromium-browser is absent from Debian stable, JDK 11 is unobtainable on
RedHat, wireguard had no RedHat branch at all, qbittorrent was built from the
AUR although Arch ships it in extra, and desk-docker installed a Docker variant
that conflicts with the one sys-svc-container installs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A role states which package it needs; the registry owns every distribution
mapping and every acquisition path. Declarations live in the repository-root
meta/packages.yml when more than one consumer installs them, and in a role's
own meta/packages.yml otherwise.
schema.py owns the declaration shape, registry.py finds and indexes the
declarations, plan.py turns a resolved spec into ordered module calls, and the
package_install action plugin only executes them. Keeping the plan pure makes
every acquisition path unit-testable without an Ansible connection:
distribution repositories, the AUR with its unprivileged build user, COPR, PPA,
an EPEL bootstrap package, the pacman multilib section and source builds.
The data is guarded by lints: a package id and a concrete distribution package
name each belong to exactly one declaration, a role installs only its own or
shared ids, every id resolves for all five default distributions with an
empty mapping justified by a nocheck, declarations read in ascending order,
every plan builds and names only whitelisted modules, and an external test
queries the real package indexes of all five distributions.
This commit adds the mechanism and the shared collection; no consumer uses it
yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A person credited with a link in one place and a bare name in another
reads as two different people and drops the reader's path to them.
sys-svc-engine showed it in two adjacent lines: the same name once bare,
once linked.
The new credits-attribution lint requires a name linked in any Credits
section to carry that link everywhere, and rejects one name pointing at
two URLs. It found 13 offenders, including a second contributor whose
attribution had drifted in test-e2e-cli.
The generator produced the defect on every run: its context carried
application_author but never application_author_url, so the template
always took the unlinked branch. Existing READMEs kept their correct
Credits because the generator only fills missing sections, which is why
239 roles were right and 12 wrong.
The name to URL map is derived rather than declared: galaxy_info.company
names the project owner, not the role author (web-app-mattermost is
authored by Alejandro Roman Ibanez but lists Kevin Veen-Birkenbach as
company), so only the Credits lines themselves carry the mapping.
Generator and lint share that derivation and cannot drift apart, and a
new contributor needs one linked credit rather than a code change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A role with a Model Context Protocol surface declares one mcp: entry in
meta/services.yml. The vocabulary module owns every allowed value, a hard
lint validates each declaration, and roles_with_service gains an opt-in
direction= kwarg so a client role resolves its servers at deploy time.
The lookup stays backwards compatible: without direction= the entries
keep their original four keys, so the existing dashboard, prometheus and
matomo callers are untouched. With it, each entry additionally carries
transport, auth, auth_subject and an endpoint dict whose port is
resolved from the referenced service's ports.local or ports.internal.
The lint rejects Jinja on MCP-specific fields (only enabled/shared may
carry it) because the static resolver cannot evaluate a templated block,
and it verifies that endpoint.service_key names a service in the same
file whose ports.* actually contain endpoint.port_key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the merged best-effort '|| true' sites (guide EXIT trap, baseline
fetch fallback) with shell-or-true nocheck reasons, parenthesize the
implicitly concatenated message strings ISC004 enforces since the ruff
0.16 pin, and drop the regex narration comments the comment lint flags
on the now-changed test files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A scheduler (cron, clock, celery beat, a framework scheduler process) enqueues
recurring jobs; running more than one replica fires the same jobs each tick,
causing duplicate work, lock contention, and unique-index collisions (e.g.
GoodJob's (cron_key, cron_at)). On swarm the default replica count is
len(hosts), so a scheduler with no explicit replicas silently fans out one per
node. Worker services (sidekiq, worker) are excluded: they scale horizontally.
Add tests/lint/ansible/services/test_scheduler_single_replica.py to enforce
replicas: 1 on any service whose key ends in cron/scheduler/beat/clock, with a
nocheck opt-out. Fix the two offenders it found, moodle cron and zammad
scheduler, and justify openproject cron with the required single-replica nocheck.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
user_token.yml moved to tasks/utils/manage/ during the task-file renumbering,
but its include of create_user_token.yml kept a single '../', resolving to the
non-existent tasks/utils/05_manage_user/ and collapsing to /opt/src/05_manage_user/
at deploy time. Every app that pulls in mailu (fusiondirectory, kix, odoo,
openproject, penpot, pgadmin, peertube, xwiki, logout) failed on it. Use '../../'
to reach tasks/05_manage_user/create_user_token.yml.
Add tests/lint/ansible/tasks/test_relative_includes_resolve.py to guard the
class: it parses every role tasks file and asserts each relative
include_tasks/import_tasks/include_vars path resolves through ansible's search
bases, so a moved task or vars file can no longer leave a dangling include that
only surfaces at deploy time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ruff toolchain was unpinned, so host venvs, the lint container and CI
installed different releases over time. That drift is why lint-python only
failed on CI: locally ruff 0.15.x preview-gated ISC004/FURB192 and (falsely)
fired S310 on urllib.request.Request, while CI's 0.16.0 has ISC004/FURB192
stable and drops the S310-on-Request false positive.
Pin ruff==0.16.0 in pyproject.toml's dev extras (Dependabot keeps it current)
and have utils.install.lint.ruff read that pin and reinstall whenever the
found version drifts, so every environment resolves the same ruff. Then fix
what 0.16.0 flags: parenthesise the implicit string concatenations (ISC004),
use min() over sorted()[0] (FURB192), drop the now-unused S310 noqa on the
Request() builders (RUF100), and delete the narration comments that the
comment lint surfaces in the touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.
Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pacman's XferCommand curl gains --retry-all-errors --retry-delay 2 so
connection resets during package downloads retry instead of failing the
update; plain --retry does not cover curl exit 35. No --max-time here:
package downloads are size-unbounded and a ceiling would truncate large
transfers. dnf gets retries=5 via its native config, matching the apt
Acquire::Retries hardening already in place for Debian.
Settle the egress lint follow-ups: collapse the nested retry checks per
ruff SIM102 and reformat the curl filter tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add lookup('asset', app, pkg, path) and lookup('asset_host'): both resolve
the asset origin through one shared resolve_host, returning the internal
CDN domain when web-svc-cdn is deployed with flavor internal and
cdn.jsdelivr.net otherwise, so template URLs and CSP origins can never
diverge. Versions come from the role's committed files/package-lock.json.
Roles declare frontend dependencies in files/package.json; web-svc-cdn
aggregates every role lockfile, runs one npm ci per role in a node
container and mirrors the packages under /npm/<pkg>@<version>/ following
jsdelivr's path scheme, so internal and external URLs differ only by host.
Convert web-app-dashboard (keycloak-js 24.0.5 UMD) and web-app-littlejs
(bootstrap, fontawesome) to the asset lookup, drop the vendored keycloak
blob, and add a repository lint forbidding hardcoded external CDN hosts
in templates and CSP metas; pre-existing external CDN entries are checked
out with nocheck markers and a migration TODO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transient CDN/mirror TLS resets are a recurring hard-fail class (docker.com, PyPI). Harden all three egress paths at their SPOT: the curl filter now emits --retry-all-errors (retries=0 opts a call out), so every ansible curl retries; a new lint requires --retry-all-errors on file-downloading curl and --tries on wget in verbatim files/*.sh (which cannot use the filter), fixing the moodle/alias downloads it flagged; and the update role configures native retries for every package manager - apt Acquire::Retries, dnf retries=, pacman XferCommand over retrying curl - covering all five CI distros (ubuntu shares apt, centos shares dnf).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the runner Dockerfile to the lint-standard Acquire::Retries=3
and keeps the collabora TODO wording; hardens the merged tree for the
stricter lints (apt retries in the tor Dockerfile, shell-or-true
markers, comment cleanup).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read INFINITO_LIFECYCLES from default.env via a new tested_lifecycles() helper; derive TESTED_LIFECYCLES, the README overview envelope, the guide-select role filter, and the invokable-lifecycle lint from it. Add "maintained" to the envelope and drop the redundant hardcoded "alpha beta rc stable" strings from workflows, CLI help, and docs.
Guide selection now skips pre-alpha/eol roles (153 -> 117 testable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cached YAML loader crashed on the !vault tag with a ConstructorError,
so any lint test that walks the tree and parses every .yml (yaml-syntax,
role/group-vars usage, ...) failed as soon as a vault-encrypted inventory
was present. Register a !vault constructor on SafeLoader that loads the
ciphertext as a plain scalar, mirroring the existing !override/!reset
handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the operator cheatsheet's copy-paste prompts into per-workflow i8- skills
that each route to their authoritative procedure doc, and reduce the cheatsheet
to a table index plus a usage paragraph, enforced by a new lint. Add skills for
develop, test, quality, refactor, the debug and requirement flows, and
commit / pull-request / push-trigger-pull; rename the iteration skills to
i8-iterate-* and rename the index to i8-help. Wire the active-listening,
dialectic, and robot skills into the flows for clarification, uncertain root
causes, and autonomous execution. Drop the Infinito-specific agent aliases
(alias.md, INFINITO_ALIAS_MD, the make alias agent section, and the two alias
tests); terminal aliases stay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A transient Debian/Ubuntu mirror fetch aborts a docker build layer with exit 100 (it killed the web-app-nextcloud swarm openldap build while the same image built fine elsewhere). New lint under tests/lint/docker/dockerfile/apt/ flags any RUN with apt-get update/install that omits Acquire::Retries; retrofit all 15 apt Dockerfiles with -o Acquire::Retries=3. Both apt lints move into the apt/ subpackage. dnf retries by default and apk has no equivalent flag, so both stay out of scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The matrix and plan aliases called the query CLI without the .env
loaded, so the strict INFINITO_* lookups died with a KeyError. A new
i8source alias sources scripts/meta/env/load.sh (venv python + .env)
and the six inspection aliases chain it. Relax the alias lint from no
adjacent consonants to at most two so the name passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The six iteration skill wrappers move from infinito-iteration-* to i8-iteration-* (frontmatter, consistency lint prefix, alias and cheatsheet references follow). The new i8-cheatsheet skill routes to the operator cheatsheet and prints its prompt blocks verbatim, or just the block matching a named situation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite all 254 role descriptions to a uniform noun-phrase schema
(function-first, no leading verb, no trailing period, <=120 chars) and
enforce it via the extended description lint. Rename 12 role README H1
titles for clarity (e.g. Backup Container to Local -> Backup Docker
Volumes, CSS Service -> Corporate Design, NGINX Legal -> Imprint) and
regenerate their Cosmos and Quick Setup sections. Overview table
regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add cli.build.readme.overview (make readme-index / make docs) rendering a
roles overview in the root README: Name, Lifecycle, Homepage, Video,
Description, Integrated with, Install. New info-media lint requires
homepage + video in every invokable role's meta/info.yml (opt out via
'# nocheck: info-media' in meta/main.yml); backfilled 147 roles with
researched, adversarially-verified homepages and official videos.
Retire the 'unsupported' lifecycle value: fold it into 'eol' as a valid
live end-of-life status (shipped but untested/unmaintained) across the
stage lists, symbol glossary, allow-list lint and docs. Retag confluence,
jira, minio, phpldapadmin to eol and magento to pre-alpha.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
categories.yml now declares modes defaults per category (deepest match wins, parents apply as fallback): update, drv and svc-net disable compose and swarm (host-hardware and host-networking categories), svc-registry requires both enabled (CI bootstrap infrastructure). role_modes_defaults resolves a role's policy; the new category-modes lint requires every deploy-bearing service entry to declare exactly those modes.<mode>.enabled values explicitly, exempt via '# nocheck: category-modes' on the service key. update, the four drv roles and the three wireguard roles declare their values explicitly; the dict-valued modes meta key is excluded from the category-tree walkers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add cli/build/readme: derives each role README's managed sections from
templates/roles/README.md.j2.tmpl (the Cosmos mermaid diagram from
meta/services.yml, an invokable-gated Quick Setup, and Credits).
The Cosmos diagram shows dependencies, every declared service, and
dependents (roles that consume this one, matched by role name and by
provides provision-name in both directions). Role nodes carry deploy-mode
emoji markers (compose/swarm/host); disabled and shared services get a
stop mark; dependents are capped at 12.
test_readme enforces the Quick Setup body is byte-exact with the template
render. `make readme-generate` and `make cosmos` regenerate; `make cosmos`
also runs inside `make quality`.
Refactor: utils/symbol_glossary.py is the single source of truth for the
word/emoji mapping (unique project-wide), consumed by the complexity
matrix and the Cosmos builder. utils/roles/deploy.py holds role_has_stack
and role_deploy_modes, shared by both so the logic never drifts.
Add `make lint-mermaid` and its CI workflow, rendering every diagram via
mermaid-cli; the render step provisions chrome-headless-shell and unzip
on demand so it works in the minimal container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One module per concern, mirroring the inventory/ subpackage layout:
run.py (single deploy pass + env forwarding SPOT), drill.py (recover
drill, host mode exempt, paths from CONTAINER_REPO_ROOT instead of a
hardcoded /opt/src/infinito), purge.py (inter-round cleanup) and
cli.py (arg parsing + round/pass loop, PASS 1/2 deduplicated).
Also extend the comment linter to flag bare string-expression
statements outside docstring position in Python: a triple-quoted
literal floating in a body is a comment in disguise and now runs
through the same validity rules; docstrings and PEP 224 attribute
docstrings stay allowed. Test narration comments moved into method
docstrings accordingly, and the design docs now link deploy/cli.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitHub hard-kills hosted-runner jobs at 360 minutes, so a 360m budget
leaves the post-failure diagnostics and artifact-upload steps zero
runtime and the failure logs are lost. Cap every deploy workflow at
345m and add a lint that fails any .github/workflows/*.yml
timeout-minutes above 345.
nextcloud swarm shards 2 and 5 in run 29448092737 died at the previous
240m step budget with zero task failures, 10-15 minutes short of a
green update pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Host lane for invokable non-stack roles (drivers, backup, wireguard, swapfile, ...). modes.host.enabled declared on 56 such roles; new lint test_role_host_mode enforces it. The complexity matrix gains a host column (not stack and modes.host.enabled), --deploy-mode host, and host bundles (one per variant); apps.sh accepts INFINITO_DEPLOY_MODE=host and filters host == true. New test-deploy-host.yml workflow (INFINITO_MAX_JOBS=10) deploys discovered host roles via the mode-agnostic ci/all.sh path, wired into ci-orchestrator (modes default 'swarm compose host').
Also: complexity -s/--symbol compact view (emoji-only headers, true/false as check/cross, lifecycle stage symbols, grouped legend with per-entry explanations, display-width aligned); stack column moved after host. scripts/meta/env/load.sh made zsh-compatible by delegating to bash and importing INFINITO_/PYTHON/VENV/PATH back.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-role deploy-mode opt-out moves from meta/tests.yml 'skip:' to meta/services.yml <entity>.modes.{compose,swarm}.enabled on the primary entity. get_role_skip now derives from it; get_role_mode_enabled added. A new lint requires every role shipping templates/*compose*.yml.j2 to declare both mode flags. Migrates 107 services.yml and drops 11 skip-only tests.yml. Also removes the variant_status deployability gate so swarm runs one job per variant unconditionally (variant_status.py and deployable_variant_indices deleted, complexity model simplified). Adds the i8matix alias.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New roles/sys-svc-container/templates/env.j2 sets TZ={{ HOST_TIMEZONE }} and LANG=C.UTF-8. Every role env.j2 includes it at the top; it renders via stack_host_template, the same engine that resolves the includes in compose.yml.j2, so it works in both compose and swarm. TZ/LANG are removed from the individual role env.j2 so each value stays single-source.
New lint tests/lint/ansible/test_env_base_include.py verifies every role env.j2 includes the base and never re-sets a base-owned key (keys derived from the base file).
mattermost and gitea meta/volumes.yml drop the /etc/timezone host-path bind: swarm hard-rejects a task whose bind source is missing on the node (Debian-trixie nodes have no /etc/timezone). TZ now comes from the base env. /etc/localtime bind also dropped; the container timezone follows TZ.
Non-directive comments were removed from the touched env.j2 (banners, restatements, reference URLs, narration) so the changed files pass the comment-lint; only tool directives remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move files that share a delimiter-bounded name prefix within a folder (threshold 2) into a subfolder named after the prefix, dropping the prefix from the filename. Covers plugins/filter (docker, generate, has, merge, get/all), env handler subpackages, utils/roles/entity, utils/tests/swarm/write, role SQL/scripts/templates, keycloak/mailu task-includes, shared tasks/utils/load, and CI scripts. Update every import, query_file/script/include_tasks/src/lookup target, compose bind mount, and python -m path to the new locations.
Extend the prefix-grouped-files lint to threshold 2 with dir-level .nocheck and file-head nocheck opt-outs, and skip groups whose shortest member's stem equals the shared prefix. Keep flat only where a loader forbids nesting: WordPress mu-plugins and the docs/requirements namespace. Ansible filter discovery is recursive, so nested filters stay Jinja-resolvable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve every 3+-file same-prefix group flagged by the new prefix lint
by moving the files into a subfolder named after the shared prefix and
dropping the prefix from the file names, updating all references:
- utils/env/handlers/infinito_*.py -> infinito/ (dir/, package_cache/
nested a second level); registry imports and the four handler-dir
globs switched to iter_project_files
- utils/install/lint/ansible_*.py -> ansible/ (aliased imports)
- roles/sys-svc-compose/files/swarm_*.{py,sh} -> files/swarm/
- web-app-erpnext/web-app-zammad files/scripts/apply_*.{py,rb} -> apply/
- web-app-odoo files/sql/{select,update}-* and sql/oidc/{grant,insert,
select,update}-* -> matching subfolders
- web-app-keycloak tasks/update/05_ldap*.yml -> 05_ldap/ (internal
includes made role_path-absolute)
- plugins/filter/get_*.py -> plugins/filter/get/ (filter names are the
filters() dict keys, so Jinja usage is unaffected; ansible discovers
the one-level-deep subdir)
Ansible lookup dirs, addon dirs and .github/workflows keep their flat
layout (the filename is the public id / GitHub only discovers flat).
Unit tests mirror the new source layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scan git-tracked files per directory and fail when three or more
siblings share a delimiter-bounded name prefix (the longest common
token run); a shared prefix is a folder in disguise, so the group
belongs in a subfolder named after the prefix with the prefix dropped
from the file names. Test-prefixed files, dotfiles/dunders and purely
numeric ordering prefixes are exempt, and directories whose flat layout
an external contract enforces (.github/workflows, ansible lookup dirs,
addon dirs) are skipped structurally rather than via a grandfather list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.
Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).
Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>