Issue #26: one .mam.env parser for the wrapper and the Python tools, decimal integer knobs, tests on a local MQTT broker. Merge-workspace review: round 1 (@b58edac) claude agy PASS, codex NOT PASS (R1-R4), grok no verdict (its late report, archived as report-c9ed5b14.md, found R1 too); round 2 (@0eec795) 4/4 PASS. Reports and round-2 identity sidecars go to .agents/reports/<session>/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
10 KiB
Review: PR #43 (fix/issue-26) @ b58edac
- Role: Reviewer (grok)
- Identity: base
e1eb22bd59ce825e3a22e86433c30f7ffd833810, headb58edace115205500c1197d9ce59cb5c6d724893, tree14b670fbfc82dc9179cab43cf53eef920a3d6ffa - Verified at
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T131705Z-a60092/1(the assigned directory was gone at the start of this pass; the same commit was checked out there again, andHEAD/HEAD^{tree}match the brief) git diff --check e1eb22b...HEADis clean. Main stayed ate1eb22b. No product file was edited.
The two loaders share envfile.py, the integer rule matches on the knobs that reach bash arithmetic, and the in-process broker covers the client paths this repo uses. One failure mode is open: a .mam.env that exists and cannot be read is a warning, and the wrapper keeps going.
1. Parser
mqtt_common._parse_env_file (mqtt_common.py:98-101) and the wrapper both go through envfile.parse / envfile.apply. The same input therefore yields the same pairs.
Checked against the rules in envfile.py:3-9 and a direct parse of temporary files (no workspace .mam.env was read):
| Input | Result |
|---|---|
| CRLF | A=1, B=two after quote removal; no warning |
| One matching quote pair | " spaced " becomes spaced (internal spaces kept) |
| Mismatched quotes | "a' stays "a' |
Extra = |
E=a=b=c keeps value a=b=c; EMPTY= is "" |
$(...), ${...}, backticks |
kept as literal text |
export MQTT_PORT=1883 |
skipped, warning invalid key (the key is export MQTT_PORT) |
| BOM on the first line | that line is skipped (invalid key); the next line loads |
Line with no = / a key that fails ^[A-Za-z_][A-Za-z0-9_]*$ |
skipped with the line number; the value is absent from the warning (envfile.py:60-65) |
Duplicate A=1 then A=2 |
both pairs are returned; apply (envfile.py:80-83) sets the first and leaves the second, because the key is then already in the environment |
export KEY= is the shell prefix the old source accepted. Both loaders now skip it. That matches the shipped files: .mam.env.example assignments are KEY=value, and deploy/install.sh:513 is an unquoted heredoc, so $MAM_PORT, $MAM_CLIENT_PREFIX, and $HOME (install.sh:520-526) are expanded by bash before the parser sees the file. Uncommented example lines contain no $ or backticks. A leading BOM is not in the example; both loaders skip that first key together.
tests/test_issue26_envfile.py (the two real loaders, reserved-variable set, and shlex quoting) passed inside the run below.
2. Wrapper eval
The wrapper runs python -I envfile.py --shell "$TARGET_ENV" and evals stdout (multi-agent-mux-delegate-job:41-45). -I is safe: the module is stdlib only (os, re, shlex, sys, typing), and python -I on the PR 4.3 venv imports that stdlib. Every printed line is export NAME=<shlex.quote(value)> for a key that already passed _KEY.fullmatch and is_reserved (envfile.py:62-67, 97-98). A probe value BASH_VERSINFO[$(touch …)0] came back single-quoted; the marker file was not created, and the warning did not contain touch.
Missing interpreter: rc 1, stderr ERROR: cannot read <path>, usage text absent. That matches test_a1_an_unreadable_parser_fails_loudly (tests/test_issue26_envfile.py:139-146).
Unreadable file, this pass: mode 000 regular file, MAM_ENV_FILE pointed at it, bash wrapper --help.
envfile.parsecatchesOSErrorandUnicodeDecodeErrorand returns a warning (envfile.py:52-53).mainprints that warning and returns 0 (envfile.py:94-99).- The wrapper's
|| exit 1(multi-agent-mux-delegate-job:44) runs only when that process is non-zero. - Observed: rc 0, stderr
WARNING: <file>: cannot read (PermissionError), stdout still containssubmit.
The wrapper is set -euo pipefail (multi-agent-mux-delegate-job:15). The previous body was set -a; source "$TARGET_ENV", so a permission error ended the process. The new path loads nothing from that file and continues. With no broker variables already in the environment, later clients use the product default broker.
test_a1_an_unreadable_parser_fails_loudly covers a missing DELEGATE_JOB_PYTHON only. It does not cover a file open that raises.
Alternative: when parse hits OSError or UnicodeDecodeError, main returns 1 (the warning text can stay). The wrapper already treats a non-zero parser as ERROR: cannot read and exits 1. Per-line skips stay warnings and exit 0. mqtt_common._parse_env_file calls apply and ignores the CLI status, so the Python loader's warning log stays as it is.
3. Precedence
A variable already in the environment is left alone (envfile.py:9, envfile.py:80-83, wrapper comment at multi-agent-mux-delegate-job:37-40). MESSAGING.md:366-368 already stated that for the Python loader. The wrapper used to overwrite it on every source. Callers that need the file's value must leave the variable unset.
- Loop:
LoopRunner.delegate_jobpassesdict(os.environ)(runner.py:548-557). Keys the loop process already loaded stay for the whole process. A process that has not set them still receives the file. - Worktree create adds
MAM_ENV_FILEpointing at the main checkout's file and does not itself exportMQTT_*(worktree_prepare.sh). - The remote runner loads through
mqtt_common, which already used this precedence (mqtt_common.py:44-46). - The test suite depends on the new order: the session broker is injected with
MQTT_BROKER/MQTT_PORT/MQTT_TLS, and a.mam.envon disk must not replace them (tests/conftest.py:1568-1575).
.mam.env.example does not say that an already-exported variable wins. The loader docstring and the wrapper comment do.
4. Integers
mam_int / mam_int_knob (lib.sh:2345-2365) and _positive_int (runner.py:71-81) share one rule: surrounding blanks, digits only, decimal (10# in bash, leading zeros stripped before int in Python), at most 9 significant digits, then a per-knob minimum. Invalid environment values warn and use the default. Invalid wrapper flags exit 1 (multi-agent-mux-delegate-job:194-199).
On this host's bash 3.2, $((0700)) is 448 and $((0900)) errors, while [ 700 -eq 0700 ] is true. The NOT_ARITHMETIC rows that say [ -ge ] (decimal) match that [ builtin. The knobs that reach $(( )) or (( )) in the wrapper and lib.sh go through mam_int: job budget, heartbeat, misses, ack timeout, inject grace, and the SKS_* deadlines including SKS_QUIESCENT_TRIES at the send_keys_safe call. Minimum 0 is the "check once / do not wait" settings (MAM_INJECT_ACK_GRACE_S, delegate_wait_started's ack, SKS_DIALOG_TIMEOUT, SKS_BUSY_TIMEOUT, SKS_DELIVERY_GRACE_S, SKS_QUIESCENT_TRIES). Minimum 1 is the budget and the liveness counts, where 0 would close a job immediately.
MAM_WAIT_GRACE_S (runner.py:627-628), MAM_CHILD_WAIT_S (runner.py:748-749), and MAM_JOB_TREE_MAX (runner.py:752) still use isdigit/int/float. They stay in Python, so they never hit bash octal. This diff does not claim to move them onto _positive_int. MAM_CHILD_WAIT_S=abc still raises ValueError inside wait_for_descendants. That is unchanged behavior, and it is outside the bash-arithmetic fix.
5. Local broker
tests/_local_mqtt.py listens on 127.0.0.1:0 with daemon threads and closes the listen socket and connections in stop. The session fixture (tests/conftest.py:1568-1584) points MQTT_BROKER / MQTT_PORT / MQTT_TLS at it and drops the auth and certificate variables for the session, restoring them afterwards.
What the repo's paho 3.1.1 clients need is implemented and checked by test_c0_make_client_speaks_3_1_1_wildcards_qos1_and_retained: CONNECT levels 3 and 4 with CONNACK, + and # subscriptions, QoS 1 PUBACK only after _route (tests/_local_mqtt.py:196-199), retained replay to a late subscriber, delete on an empty retained payload, and UNSUBSCRIBE. test_c0_holds_when_the_broker_is_slow delays CONNECT, UNSUBSCRIBE, and route so a test that reads broker state before the ack fails. QoS 2 is refused. There is no TLS, no auth check, no persistent session, and no QoS 1 redelivery; the module docstring says so, and the product paths under test do not use those.
The moved tests keep their product assertions. acceptance1 and acceptance5 also require the subscriber log, and acceptance5 requires the runner log plus dispatch_verified / dispatch_fingerprint on the audit row. The "public broker did not deliver" skips on those paths are gone (test_issue18_liveness_contract.py, test_issue19_subscriber_daemon_lifecycle.py:417-424). test_c2_no_test_names_the_public_broker_or_strips_the_local_one scans tests/*.py. Caller-supplied MQTT_PORT still overrides the fixture, so test_t1_4_broker_unavailable_fail_closed can still name an unreachable port.
Ran without exporting .mam.env: the 10 broker tests from issues #6 and #19 that are outside the brief's file list, 10 passed in 100.83s.
6. The "12 baseline purge failures" claim
tests/test_issue10_13_purge.py is not in e1eb22b...HEAD. Run alone on this host, HOME set to a temporary directory, session-id variables unset: 23 passed in 12.69s. The claim that 12 of 13 failures are this file's baseline on this host does not reproduce. The full suite (the brief's ~75 minutes) was left to the orchestrator, so this pass does not confirm or deny a separate total of 13.
7. Design boundary
mam_int reads a decimal for bash arithmetic. It does not choose deadline_timeout versus wall_clock_timeout, extend a budget, or decide a stall. Stall detection stays in stall_watch.py and publishes progress only. Closing stays on the existing clocks. The new bash is the reader plus eval of quoted export lines.
Tests
From the review tree, with AGENT_PYTHON_BIN set to the PR .venv and MQTT_* / MAM_ENV_FILE unset in the parent (the session fixture starts the local broker):
tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_local_mqtt.py tests/test_issue18_liveness_contract.py tests/test_issue33_budget.py tests/test_issue30_stall_watch.py tests/test_tier1_unit.py
228 passed, 1 skipped, 529.36s. Plus the 10 broker tests in section 5.
[VERDICT: NOT PASS]