render_replicated_templates notifies 'compose up' for every file it
writes. With the config render placed before sys-stk-full and that
include flushing handlers itself, compose build ran against a directory
sys-svc-compose had not written compose.yml into yet and aborted. Load
the stack first, render, then flush, the way web-app-openclaw does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compose and host deploy jobs tee the whole deploy stdout into the file
ANSIBLE_LOG_PATH names, and both downstream steps parse that stream. Only
stdout carries the host-side "matrix-deploy: round N/M ... PASS n" markers,
so it is the sole source the per-variant runtime tables can come from.
dedicated.sh's cleanup then copied the container-side ansible log onto that
same path. docker cp unlinks and recreates the destination, so tee kept
writing into an orphaned inode and the visible file became ansible's own
log_path output — whose "<ts> p= u= n= LEVEL| " line prefix defeats the
anchored recap matchers in logparse.py while the unanchored has_sections
substring test still fires. in_roles_recap never flips, every role-time line
is skipped, and the step dies with "no profile_roles entries found" although
the deploy succeeded. Run 30578244959: 14 of 14 compose+host jobs.
Why it looked like a flake: docker cp cannot read a /tmp that is a tmpfs
mounted inside the container, and the Debian image enables systemd's
tmp.mount while CentOS Stream 10 does not. The copy silently failed on
debian (host file survived, jobs green) and landed on centos (host file
destroyed). feature/swarm-nfs never had the block; it returned here through
merge drift from main, where the copy is still load-bearing because those
workflows do not tee.
The copy is dropped: stdout is a strict superset of what it provided, and
the in-container verifier reads the log inside the container. Alongside it:
the swarm workflow's file-exists guard is adopted so an aborted deploy does
not paint the summary step red on top; the log_path prefix moves to
utils/ansible_log.py and is applied in logparse.py, so an ansible run log
parses instead of yielding an empty summary; a lint pins that nothing writes
onto ANSIBLE_LOG_PATH again; and the parser regression test now covers the
recap headers, not just the role rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous probe counted effective directives in /etc/dnsmasq.conf and read
any of them as a foreign owner. Every distro ships a stock file with some, so
on centos and fedora the drop-in rendered without listen-address, dnsmasq bound
every interface via bind-dynamic and refused to start — both workspace jobs died
at router.yml:80 five minutes in, with byte-identical 244-byte drop-ins.
Deploy mode separates the two real cases: the swarm lab writes its own full
config that already serves every interface, so the drop-in must not narrow it;
everywhere else the role owns the listener as before. Same variable that already
gates TOR_CONTAINER_DNS_HOST.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mock.patch.dict left the surrounding environment in place, so on a GitHub runner the ambient GITHUB_REPOSITORY_OWNER decided the result and the assertion measured the runner instead of the case under test. It passed locally, where that variable is unset, and failed in CI.
Clear the environment so the test states what its name claims, and cover the precedence that hid the mistake: GITHUB_REPOSITORY_OWNER over the owner in the repository slug, which no test exercised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make quality rejected the new reclaim script for missing the executable
bit, and the S310 suppression in the Home Assistant token probe for
carrying no reason. A security suppression must state why it is safe, so
the loopback justification goes back on the urlopen call; ruff dropped
the second marker on Request(), where S310 never fires.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hermes registers MCP servers in config.yaml under HERMES_HOME, which the
image pins to the data volume. Render the discovered servers there and
reference each bearer as ${env:<ROLE>_MCP_TOKEN} so the config carries no
secret into the swarm config store. A per-server 'hermes mcp test' then
refuses a deploy whose agent cannot reach a server it was configured
against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lab subnet is pinned per worktree slot while lab network names carry
the cluster id, so a cluster kept from one app blocked the next run of
another with 'invalid pool request: Pool overlaps'. The pre-run teardown
only cleared the same cluster id, which never matched. Matching on the
IPAM subnet instead clears exactly the blocker and leaves a sibling
worktree's cluster alone; swarm-clean stays the host-wide tool.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Home Assistant exposes no per-tool filter: enabling the Assist API
publishes its actuating intents regardless of the services.mcp tool
flags. The enforced control is entity exposure, and the role exposes
none, so the actuating tools have nothing to reach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role body runs per host, so the mint ran on every node in the play.
mint_long_lived_token deletes the existing token under the same client
name before creating a new one, so each node invalidated its
predecessor while the store persisted the first one. Clients then got a
401 on every call. A deploy-time probe now asks the hub whether the
stored token still authenticates, re-mints only when it does not, and
refuses the deploy when even the fresh token is rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make quality runs test-external, test-integration, test-lint and test-unit
concurrently, each with its own xdist pool, so a wall-clock assertion in one
of them measures machine contention as much as code. The cold domains render
takes 7.34s isolated on this host and took 91.50s inside that gate - a factor
of 12.5 with no code between the two measurements.
Profiling the cold render puts copy.deepcopy on top by a wide margin
(4.35M calls, 3.4s tottime), but those copies are contractual rather than
accidental: utils/cache/README.md:36 states that cache getters hand out shared
instances and that every mutating caller must deep-copy first. Removing them
means replacing that invariant with copy-on-write or immutable structures,
which is a redesign of the cache contract and not a fix for a flaky budget.
Only the cold-start ceiling moves. The assertions that catch a real cache
regression are untouched: warm calls still have to stay under
WARM_CALLS_BUDGET_SECONDS = 0.5, and the cache still has to be at least 50x
faster than the cold render.
The sibling applications budget is left at 60s deliberately; it measures
7.61s isolated and is exposed the same way, so it is the next candidate if the
gate flakes there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docker info reports DockerRootDir as the DAEMON sees it, and
host_storage_constrained fed that string straight into shutil.disk_usage,
which statvfs()es it in the CALLING process's mount namespace. The two
coincide only when the caller is the daemon's host. In the nested act
container of the workspace jobs they do not, so os.statvfs('/var/lib/docker')
raised FileNotFoundError, matrix.py died, and because 00_one.sh invokes it
bare under set -euo pipefail the whole swarm drill went down with it. All
five workspace jobs of run 30560683608 failed this way, byte-identically,
across five base images and five nested distros.
docker_root_free_bytes() now takes the vantage point as a required keyword:
it measures the daemon's data root when that path resolves here, and the
caller's stated local path when it does not. matrix.py passes "/", which is
the same vantage it already trusts for its ENOSPC abort 270 lines earlier;
overlayfs passes statfs through to the upper filesystem, so a container of a
daemon measures that daemon's data-root filesystem through its own rootfs.
Nothing is caught and nothing is guessed. An unreachable daemon still raises
RuntimeError out of docker_data_root(), which is the guard this module exists
for, and local_vantage carries no default because only the caller knows where
it is running.
The compose-side twin loses its || true and its hardcoded /var/lib/docker
substitution, so an unresolvable root no longer measures a guessed filesystem.
Its compose.exec locality was already correct and is unchanged.
Verified against the pre-fix module under the CI condition: with the data root
stubbed to a path absent from this namespace, the old code raises
FileNotFoundError and the new code measures the local vantage; with the daemon
stubbed unreachable, the new code still raises RuntimeError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems, same two files, neither ever gated: the earlier run died in
autoformat before test-unit started.
ruff wanted Path.stat/Path.chmod over their os counterparts and an explicit
check on subprocess.run — worth stating here, since down() is called precisely
to observe a non-zero exit while its sibling git() sets check=True.
The write-bit pinning the four remaining tests rely on does not restrain root,
and the suite runs as root in the container: the script deleted the directory
anyway, the assertion inverted, and the finally block then chmod'd a path that
was already gone. Those four now skip when euid is 0, because the scenario
cannot exist there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git worktree remove deletes the checkout first and the metadata dir
second. Under the agent sandbox, which holds config.worktree and
commondir as read-only bind mounts, it therefore dies with EBUSY after
the checkout is already gone, aborting make worktree-down with rc 255
even though the branch is in fact free. down.sh now falls back to the
unregistration prune.sh already used (drop gitdir + HEAD, best-effort
rm -rf) and reports any dir the sandbox still pins. That routine moves
into lib.sh so both callers cannot drift apart, and gains a third
outcome for the case where gitdir/HEAD themselves survive: only then is
the branch still claimed, and only then must the teardown fail.
The discriminator for the fallback is whether the checkout survived,
not a match on "Device or resource busy" — the identical half-removed
state arises from EACCES, EPERM and EROFS.
A failing git status probe no longer reads as clean: on a broken .git
link it exits 128 with empty output, which used to satisfy the guard,
tear down the compose stack, and only then die at git worktree remove
with an unexplained 128.
Measured against the unfixed scripts with real fixtures: pinned
metadata dir 255 -> 0, broken .git link 128 -> 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems, same two files, neither ever gated: the earlier run died in
autoformat before test-unit started.
ruff wanted Path.stat/Path.chmod over their os counterparts and an explicit
check on subprocess.run — worth stating here, since down() is called precisely
to observe a non-zero exit while its sibling git() sets check=True.
The write-bit pinning the four remaining tests rely on does not restrain root,
and the suite runs as root in the container: the script deleted the directory
anyway, the assertion inverted, and the finally block then chmod'd a path that
was already gone. Those four now skip when euid is 0, because the scenario
cannot exist there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git worktree remove deletes the checkout first and the metadata dir
second. Under the agent sandbox, which holds config.worktree and
commondir as read-only bind mounts, it therefore dies with EBUSY after
the checkout is already gone, aborting make worktree-down with rc 255
even though the branch is in fact free. down.sh now falls back to the
unregistration prune.sh already used (drop gitdir + HEAD, best-effort
rm -rf) and reports any dir the sandbox still pins. That routine moves
into lib.sh so both callers cannot drift apart, and gains a third
outcome for the case where gitdir/HEAD themselves survive: only then is
the branch still claimed, and only then must the teardown fail.
The discriminator for the fallback is whether the checkout survived,
not a match on "Device or resource busy" — the identical half-removed
state arises from EACCES, EPERM and EROFS.
A failing git status probe no longer reads as clean: on a broken .git
link it exits 128 with empty output, which used to satisfy the guard,
tear down the compose stack, and only then die at git worktree remove
with an unexplained 128.
Measured against the unfixed scripts with real fixtures: pinned
metadata dir 255 -> 0, broken .git link 128 -> 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mock.patch.dict left the surrounding environment in place, so on a GitHub runner the ambient GITHUB_REPOSITORY_OWNER decided the result and the assertion measured the runner instead of the case under test. It passed locally, where that variable is unset, and failed in CI.
Clear the environment so the test states what its name claims, and cover the precedence that hid the mistake: GITHUB_REPOSITORY_OWNER over the owner in the repository slug, which no test exercised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guest spec requested /mcp_server/sse, a path Home Assistant does not
route. The hub answers 401 on any unknown path, so the assertion passed
without ever reaching the MCP surface. Upstream serves it at /api/mcp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docker info reports the daemon's data root. Under act only the socket is
mounted, so that path is not in the runner's mount namespace and every
swarm run died at the STORAGE_CONSTRAINED lookup with FileNotFoundError.
The container's own writable layer lives under that data root, so its
root filesystem carries the same free space.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five requests in one session ran the same chain by hand: fetch, merge, resolve,
gate, wait for the commit decision. The conflict doctrine that emerged from
those merges is now written down instead of re-derived each time — union
additive lists, prefer the SPOT variable over an inlined duplicate, keep the
conventions this branch's lint enforces, and take the side that fails loudly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mock.patch.dict left the surrounding environment in place, so on a GitHub runner the ambient GITHUB_REPOSITORY_OWNER decided the result and the assertion measured the runner instead of the case under test. It passed locally, where that variable is unset, and failed in CI.
Clear the environment so the test states what its name claims, and cover the precedence that hid the mistake: GITHUB_REPOSITORY_OWNER over the owner in the repository slug, which no test exercised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whitelist dispatch no longer signs and pushes. It runs against whatever the remote already holds for the current branch, which the trailing comment now states so the caller is not surprised by a run that exercises an older tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whitelist dispatch no longer signs and pushes. It runs against whatever the remote already holds for the current branch, which the trailing comment now states so the caller is not surprised by a run that exercises an older tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
i8puma drives the priority line, which still runs every diff-derived role after the priority jobs pass. Validating a fixed set of roles wants the whitelist input instead, and the workspace and instructions tests carry no signal for that, so i8puwl pins both to false.
The push stays part of it, as everywhere in the i8pu family: the dispatch passes the local branch name as --ref, so without pushing first the run would exercise whatever the remote last saw rather than the tree under test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
i8puma drives the priority line, which still runs every diff-derived role after the priority jobs pass. Validating a fixed set of roles wants the whitelist input instead, and the workspace and instructions tests carry no signal for that, so i8puwl pins both to false.
The push stays part of it, as everywhere in the i8pu family: the dispatch passes the local branch name as --ref, so without pushing first the run would exercise whatever the remote last saw rather than the tree under test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mock.patch.dict left the surrounding environment in place, so on a GitHub runner the ambient GITHUB_REPOSITORY_OWNER decided the result and the assertion measured the runner instead of the case under test. It passed locally, where that variable is unset, and failed in CI.
Clear the environment so the test states what its name claims, and cover the precedence that hid the mistake: GITHUB_REPOSITORY_OWNER over the owner in the repository slug, which no test exercised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The min_storage gate measures every declared pin on each run, and every measurement was an anonymous manifest request to Docker Hub, which meters those per IP. A full run exhausted the pull budget, so the following local deploy could no longer pull at all and the gate itself stopped measuring: 96 of 165 images answered indeterminately and only warned.
Read the GHCR mirror first and fall back to the upstream registry only for images it does not carry. The copy is byte-identical, so the measured size is unchanged. Persist measured sizes under the temporary directory; store positive results only, because a cached indeterminate answer would be remembered as fact and the gate would stop measuring for good.
Lift the mirror path formula out of the provider into utils so the writer and the reader share one definition, resolving the namespace from GITHUB_REPOSITORY, then remote.pushDefault, then origin. pushDefault has to win: origin may point at an upstream that hosts no mirror. Parse .git/config by hand because configparser rejects the duplicate keys git writes and the test container ships no git binary.
Measured over all 165 declared pins: no requests to Docker Hub, 277 to the mirror, 9 to registries it does not carry, and 164 of 165 images measured.
Leave manifest_exists on the upstream registry. It answers whether the declared pin is still pullable from where the deploy pulls it, which a mirrored copy cannot attest; it takes the cache only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hadolint 2.15.0 drops the SHELL binding across an intervening ARG/ENV block and lints the RUN instructions as POSIX sh, so the pinned bash shell went unnoticed and lint-dockerfile failed on SC3040/SC3001 without the Dockerfile having changed. Declaring SHELL below that block restores the binding; nothing runs before it, so the build is unaffected.
The last RUN sits behind ENV container=docker and STOPSIGNAL, where the same reset applies. Moving those would change the environment the entry.sh compile step runs in, so that one instruction carries an ignore instead. Give HEALTHCHECK its command in JSON notation, which DL3025 asks for and which drops the needless shell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
entry-manual.yml already exposes workspace and instructions as auto/true/false, but the alias had no way to reach them, so triggering either meant leaving the shell for the GitHub UI. Accept them as an optional third argument and map it straight onto both inputs.
Pass the argument through rather than collapsing it to a flag: auto is not the same state as false, and the workflow distinguishes them. Reject anything outside true/false instead of treating it as unset, so a typo fails at the prompt rather than silently producing a run without the tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI deploy bootstrap hand-listed two packages and let the ubuntu-latest runner image supply the rest, so a new host-side import surfaced as a red deploy job instead of a failed install. Size [project.dependencies] to what runs outside the project image and move the deployment stack into a deploy extra, then have every install site read that instead of naming packages itself: the image build and make install-python take the deploy extra, the CI host and the mirror and instructions workflows take the base.
Declare cryptography, which utils/manager/value_generator.py imports directly and which only reached the environment transitively through ansible. Drop numpy, which nothing in the repository imports. Guard the boundary with a lint test that blocks every deploy package and asserts the runner-host CLI still builds its parser.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The storage-constrained work put utils.storage.constrained into the host-side import chain of cli.administration.deploy.development, which pulls humanfriendly. The CI dependency line installed only ruamel.yaml and PyYAML, so every compose and host deploy job aborted at parser build time with ModuleNotFoundError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make worktree-up places checkouts under ~/.local/share/worktrees, which the
sandbox did not grant write access to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.cache/ holds generated worktree checkouts, so it was neither ignored nor
excluded from the markdownlint globs. That put 809 foreign markdown files in
scope; the read-only pass tolerated them, but autoformat's --fix could not
write there and took the whole quality gate down.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three conflicts, none resolved by picking a side wholesale:
- Makefile: took the incoming ACT_INPUTS, which routes through SWARM_DISTROS
instead of inlining the same expression a second time.
- test-e2e-playwright/02_run_one.yml: took the incoming rc handling. An absent
rc now counts as failure; the previous default(0) let an aborted Playwright
run pass silently, contradicting the debug message right above it.
- web-app-peertube spec: combined both. Kept gotoOnion and resolveTimeout,
which this branch's lint enforces, and adopted the incoming retry-inside-poll
for the OIDC button plus its networkidle wait.
compose_networks renders a foreign provider network as external: true, leaving open who creates it. The swarm handler has answered that since it was written, looping compose_external_networks through the network create task so a shared provider overlay exists even when the provider role did not run in the same play. Compose had no counterpart: it only worked while consumer and provider deployed together, because then the provider role created the network itself.
Deploying roles one at a time, as the roundtrip does, breaks that assumption and compose up aborts with "network <provider> declared as external, but could not be found". The compose handler now mirrors the swarm block: same lookup, same create task, triggered before the stack comes up. No cluster gate, since that condition is swarm-only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The foreign-config probe looked in /etc/dnsmasq.d/ only, so a host whose
resolver is configured in /etc/dnsmasq.conf — the swarm lab — read as
unowned. The role then wrote listen-address=127.0.0.1 and narrowed a
bind-dynamic resolver that had been answering on every interface, leaving
nested image builds without a nameserver. Count the main config's effective
directives too, ignoring comments and the conf-dir line the role adds itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Formatter-only change; make autoformat rewrote the three is_constrained calls that exceeded the line limit when the file landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The declared values were guesses that ran up to 60x the size of the image they belong to: seaweedfs asked for 30GB against a 104MB image, nextcloud for 20GB against 353MB. Because min_storage feeds the storage guard, the runner packer and the sufficient-storage filter, those numbers made hosts look too small and pruned caches that deploys did not need to lose.
Every service that declares min_storage together with image and version now carries the measured compressed transfer size of that image plus 200MB of headroom. Sizes were read from the registries through utils.docker.registry.manifest_transfer_size, the same path the headroom test uses, so the declarations and the gate agree by construction. Values are written in MB throughout, matching the existing 500MB / 100MB / 50MB style.
119 declarations across 89 roles. Range: 206MB (memcached) to 3473MB (ollama); four exceed 1GB. The 84 min_storage entries that name no image are untouched, since there is nothing to measure them against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An over-declared min_storage makes a host look too small: it feeds the storage guard, the runner packer and the sufficient-storage filter, so a deploy that would have fit prunes a cache it did not need to lose. Nothing checked the declared number against reality.
The new external test measures what a pull of the service's own image:version actually transfers - config blob plus every layer blob, resolved for linux/amd64 - and fails when min_storage exceeds it by more than 1GB. A service whose data volume genuinely dominates keeps its value by carrying '# nocheck: min-storage-headroom' together with a 'Reason:', the same two-marker shape meta/volumes.yml already uses for nfs: false.
Sizes come from the repo's own registry client rather than 'docker buildx imagetools': make test-external runs inside the compose container, which has no docker socket. manifest_transfer_size reuses the existing bearer-challenge flow, keeps the tri-state contract of manifest_exists (an indeterminate registry warns instead of failing), and resolves a multi-platform index to its amd64 manifest - the case a raw jq over '.layers' would miss entirely. Distinct image refs are measured concurrently through resolve_max_fetch_workers().
A min_storage that parses out of the YAML but cannot be located in the file text fails the test, so a declaration can never silently lose its marker resolution.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The flag was decided by a fixed free-space threshold (100 GiB by default), which fired on roomy hosts and stayed silent on a 145 GiB CI runner that a single deploy round could fill. It now compares free space on Docker's data root against what the deploy declares it needs: the summed min_storage of the selected applications and their transitive dependencies.
The need is variant-aware. utils/storage/constrained.py overlays each app's meta/variants.yml entry before walking the service registry, so a round that trims services down is measured at its own size instead of the base configuration's.
Both entry paths pass their per-round variant map: cli/administration/deploy/development/init.py decides inside the round loop (compose and host share this via ci/one.sh -> dedicated.sh), and utils/tests/swarm/matrix.py feeds the decision into the swarm host_vars payload. --threshold-gib is gone; --force-storage-constrained still overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It shipped with the persistent-base change; the constraint it described - git forbids a branch feature beside feature/x, so the nested directories cannot collide - belongs in that commit's message, where it already is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The topology SPOT read default.env only, so every lab took the same subnet no matter what the generated .env said. Two labs on one host therefore collided with "invalid pool request: Pool overlaps", which is what stopped a worktree lab from running beside the primary checkout's.
Collect the generated .env first, then default.env, and emit each assignment as := so precedence becomes exported environment, then .env, then default.env. default.env stays the fallback because the workflow sources this file before make dotenv has run, which was the original reason for reading it directly.
A line that does not parse as a balanced assignment now aborts naming the line. Sourced as-is it would have become an unconditional assignment and silently defeated the precedence this change exists to establish.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>