# Review: PR #73 / Issue #71 (macOS, snapshot 2) Job fd90992c. Reviewer only. Checks ran on the review snapshot. The source worktree and the control-plane checkout were not edited. ## Identity - Snapshot: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261026T133440Z-c8961b/2` - Loop: `loop-20261026T133440Z-c8961b` - Base: `29cc4fd90478936f15387a7665e2c95d13a3f14c` (main, v4.10.1) - Head: `5f6065344f800c2c5ad6f802e95e9e8ff5cc7b26` - Tree: `98d57bdc60920238c115ece90c90cc694f2744bc` - 14 commits, base is an ancestor, porcelain 0, no `.mam` in the snapshot, `git diff --check` clean - Diff: 28 files, +953/−176 - Host: `/usr/bin/python3` 3.9.6, bash 3.2.57(1)-release, sqlite 3.51.0, `LANG=en_US.UTF-8` `tests/test_issue66_stall_watch_sanitized_name.py` has an empty diff against `29cc4fd`. Commit `0fc162e` is an ancestor. The diff file list has no `multi-agent-mux-update` skill. `MAM_VERSION` stays `4.10.1`. Issue #71, the "범위 정리" comment (items 1–10), and PR #73 were read. The registry entry point is documented for #72 and is not wired to an update skill. ## What was checked ### attach_command `lib_py/agents/sanitize.py:34` builds `herdr --session agent attach `. An empty server becomes `default`, and both the server and the name go through `shlex.quote`, so `default` is written as `--session default` and a server that needs quotes is quoted. The three writers call it: - create, `create_session.sh:566` - resume, `update_yaml_resumed.sh:151` (new row) and `:161` (existing row whose server is set) - reconcile, `reconcile.sh:676`, on the row whose note is `auto-registered by monitor` The shim `agent attach` (`lib.sh:605-613`) resolves the target with `_resolve_herdr_target` and runs `_real_herdr agent attach "$tgt" "$@"`. Options after the name stay in `"$@"`. ### Removed fields and the stop hints `kill_command` and `start_command` remain only as fields the migration pops and as assertions that they are absent. `status.sh` no longer emits `start_command`. The shim still has the fake `new-session` and `kill-session` verbs, which create, stop, and reconcile use. `kill-session` sanitizes the name and exits 1 when that fails. Create exit 43 (`create_session.sh:364`) and `id_order.py:330` and `:344` tell the user to run `stop_session.sh --session --herdr-session `. The row-less tests below run that script. ### Row-less stop `stop_rowless` (`stop_session.sh:123`) runs only when the row lookup exits 7 (`:189`). A strict read failure exits 1 before that (`:170`). `--purge-conversation` with no row exits 1 (`:124`). A cwd outside this workspace exits 5. A session that survives `kill-session` exits 6. Those codes match `multi-agent-mux-stop/SKILL.md:58-66`. Server order: `--herdr-session`, else a non-default `HERDR_SESSION_NAME` captured before resolve (`:109`, `:128`), else the workspace server. `lib.sh` stamps an unset name as `default`, and the row-less path treats that stamp as unset. The cwd check realpaths both sides and accepts the workspace itself or a child (`root + os.sep`). That rejects a sibling prefix and a symlink whose target is outside the workspace, and it treats a trailing slash as the same directory. `/tmp` and `/private/tmp` are the same path on this Mac. The row-less tests compare registry bytes before and after and passed. `load_state_json strict` (`lib.sh:1594`) is the bash reader. A missing `state` or `sessions` table raises in strict mode and the function exits 1. The argument-free call still prints `{}`. ### One name rule `lib.sh` has one `_sanitize_herdr_agent_name` (`:44`). `00000000` and `lower()[:32]` are absent. The shim receives the function through `declare -f` (`:181`). The function was extracted with `sed` and run under `/bin/bash` 3.2 without sourcing `lib.sh`. Against `sanitize_herdr_agent_name` there were 0 mismatches under `LC_ALL=C` and `LC_ALL=en_US.UTF-8` for empty, `Upper-Case`, `-n`, `-e`, a 33-character name, the long branch name, `İstanbul-creator-claude`, a newline, `a\nb`, café, the kelvin sign, Japanese, Korean `한글`, and `A B`. Korean is two characters; the non-ASCII path uses `python3` `str.lower()` and the same regex, so the replacement follows characters, and the result matched Python. With `PATH` limited to `tr`, `sed`, `awk`, and `cut`, the long name exits 1 with `no shasum, sha1sum, openssl or python3 to hash it` and prints no name. `한글` exits 1 with `not ASCII`. `/usr/bin/python3` is 3.9.6. The diff adds no `mapfile`, `${var,,}`, `declare -A`, `match`, or `tomllib`. A throwaway SQLite database opened with `PRAGMA journal_mode=WAL`, written, and closed still had a `-wal` (0 bytes) and a `-shm` (32768 bytes). ### Registry format 2 `atomic_yaml.py:118-137` migrates under the lock, after `BEGIN IMMEDIATE` and before the caller mutation, when `stored_version < 2`. Migration sets `force_yaml`, so YAML is rewritten even if the mutation changes no session. If the DB is already current and the YAML copy is still older, the next write rewrites the YAML. `migrate` (`registry_format.py:50-66`) updates every named row, including stopped and terminated, and sets `registry_format` to 2. A version already greater than 2 is not less than `CURRENT`, so the writer does not call `migrate`. The entry point `python -m lib_py.registry_format --yaml ` prints `registry-format: -> , rows changed: ` and exits 0, or `registry-format: error:` and a nonzero status when neither file exists. The only `yaml.safe_dump` under `.agents/skills` is the writer. Readers (`lib_py/state.py` and the argument-free bash loader) do not migrate. ### stall_watch and the edited tests `stall_watch.py:229` computes `self.herdr_name` once and passes it into `HerdrSource`. The issue #66 test file is unchanged and is in the passing run. Edited assertions keep the old intent: pane-lock uses `session_id_source` (create sets it at `create_session.sh:579`) to tell a create row from a monitor row; the worktree test looks for a leaked `WORKSPACE_ROOT=` in `attach_command`; tier 2 checks the new attach string and the absence of the two dropped keys; tier 1 and tier 3 plant a row with no agent so "cannot infer agent" still exits 2. `skill_code_blocks.json` moved with the docs, and `test_issue42_skill_blocks.py` expects stop exits `0` through `6`. That file passed aside from the skills-link skip. S11 (`test_issue23_24_startup_path.py:306-310`) compares the shim with `0fc162e`. That commit is in this checkout, so the comparison ran as part of the passing file. If the object is missing, the test calls `pytest.skip` with a message that names the commit. That skip is intentional: the PR says a squash would drop `0fc162e`, and it asks for a merge commit. The reconcile tests plant a mock pane (`test_issue71_herdr_names.py:127-140`) and require one new row. They passed, so registration ran. ## Tests Pytest was `PYTHONDONTWRITEBYTECODE=1` with the control-plane venv, `-p no:cacheprovider`, and `--basetemp=/tmp/mamfd9-fd90992c-pytest`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `PYTHONPATH`, the four `XDG_*` homes, and `AGENT_SESSIONS_YAML` were unset. `AGENT_PYTHON_BIN` pointed at the control-plane venv. The snapshot was the working directory and was not checked out. Head, the files named in the brief plus `test_b19_headless_reconcile_fixes.py`, `test_b4_session_created.py`, and `test_issue30_stall_watch.py`: **546 passed, 2 skipped** in 1458.68s, exit 0. `tests/test_issue71_herdr_names.py` collects 70 tests and they are in that pass. The two skips, confirmed with `-rs` on those two tests, are `test_claude_skills_link_resolves_to_agents_skills` (`.claude/skills` absent, `test_issue42_skill_blocks.py:266`) and `test_s6_the_heartbeat_timer_is_not_a_background_child` (Linux `/proc`, `test_issue30_stall_watch.py:340`). Base: a `--shared` clone at `/tmp/mamfd9-fd90992c-base`, detached at `29cc4fd`, with only this test file copied in. Same pytest flags, `--basetemp=/tmp/mamfd9-fd90992c-base-pytest`. **68 failed, 2 passed** in 98.42s, exit 1. The two passes are `test_the_names_need_converting_and_the_plain_one_does_not` and the plain-name `test_wrapper_attach_calls_herdr_with_the_herdr_name`. The long and uppercase wrapper cases failed. ## Residuals (non-blocking) - Readers stay read-only. An old registry keeps a format-1 `attach_command` until the next writer, or until `python -m lib_py.registry_format` runs. The module docstring describes that window. The explicit entry point is left for #72. - `VERSIONS.md` is unchanged. The release sentence from the scope comment is in the PR body. - `migrate` skips a row with no `name` (`registry_format.py:57-58`), so that row can keep `kill_command` after the format key becomes 2. - `lib_py/state.py` `load_state_json` still returns `{}` on a read error. Stop uses the bash strict reader. - The SKILL.md exit-0 cell describes a stop or an already-stopped no-op. The pre-flight paragraph and the script header also describe the row-less close as exit 0. The pre-flight sentence names `--herdr-session` and `HERDR_SESSION_NAME`; the code also uses the workspace server when both are absent. - A direct call to `migrate` on a format already greater than 2 would write the key back to 2. The writer does not call it in that case. - S11 skips only when `0fc162e` cannot be resolved. A squash merge would take that path. The PR forbids squash for that reason. ## Host Checked before the suite at 2026-10-27 01:43:34 and again after it at 02:08:54. No process whose first command token was `opencode`. `lsof` of the user opencode database returned exit 1. Database mtime 1792641027, size 36712448. `opencode.log` absent. Config `service.json` mtime 1792246838, size 64. State `service.json` absent. Herdr config mtime 1784456394, size 19. `~/.local/state/mam` directory mtime 1792846843, size 128. `id-order` is a directory, mtime 1792846886, size 128; its entries were not listed. The same two herdr servers were present before and after: pid 11655 (`/opt/homebrew/bin/herdr server`) and pid 69309 (`/opt/homebrew/bin/herdr --session multi-agent-mux server`). No server argv contained the scratch prefix, the snapshot path, or the loop id. The database was not opened. After the suite the snapshot was still head `5f60653`, tree `98d57bdc`, porcelain 0, no `.mam`. The control-plane checkout was still `29cc4fd`, porcelain 0. Rechecked after this report was written, at 2026-10-27 02:09:37. The snapshot was still head `5f60653`, tree `98d57bdc`, porcelain 0, with no `.mam`. The control plane was still `29cc4fd`, porcelain 0. Opencode first-token count 0, database `lsof` exit 1, and the database, log, service files, herdr config, `~/.local/state/mam`, and `id-order` mtimes and sizes were the same as the pre-suite reading. Herdr servers were still pid 11655 and pid 69309. No server argv contained the scratch prefix, the snapshot path, or the loop id. [VERDICT: PASS]