The two issues are fixed on one branch; the commits are split by issue. #55 comes first so that every suite run after it is already protected. No version bump; the version is set at merge.
#55 — tests no longer write into the user's real agent homes
Cause:lib.sh:77 exports CLAUDE_PROJECT_DIR=$HOME/.claude/projects to every MAM agent pane. DiscoveryContext prefers it over the home_dir it is given. So test_adapter_discover (and anything like it) wrote -tmp-…-test-adapter-discover0-ws into the real ~/.claude/projects whenever an agent ran the suite. 40 such folders had accumulated.
Fix:
An autouse fixture in tests/conftest.py removes inherited agent-home overrides for every test: HOME_DIR, CLAUDE_PROJECT_DIR, CODEX_HOME, LOCAL_BIN. The variable list is checked against what the adapters, base.py and lib.sh actually read.
tests/_agent_home_guard.py is a read-only real-home guard. It fails the session if a new entry appears in the user's agent directories during the run, in the same way as the #20 control-plane guard.
The operational lib.sh export is unchanged.
#50 — onboarding waits for completion; dispatch waits by the registry
A. create --onboard waits for the onboarding job to end (c29c5b2), and so does worktree_prepare.sh --onboard.
After the started handshake, create waits up to MAM_ONBOARD_WAIT_S (default 600 s) for the job to end, then prints onboarding completed: job <id> (<n>s).
New exit code 44: the onboarding ended with error or was cancelled.
New exit code 45: it did not end in time.
Both keep the session and write last_visible_status: onboarding-<status> on its row.
The decision lives in Python. The create SKILL.md exit table and the #42 inventory are updated.
B. Reproduced first (real claude, herdr 0.9.3). Despite the ← 1 agent background indicator, herdr returned to idle about 13 s after completed, and a second submit went through. So herdr 0.9.3 did not cause a session_busy cancel by itself, and the herdr idle wait stays in place, but only as an additional check. The registry decides.
C. The wrapper waits by the registry before it injects (e9efabf and the review fixes).
Before injecting, the wrapper waits up to MAM_DISPATCH_WAIT_S for the target session's earlier open jobs, onboarding included, to end. If they don't, it ends as cancelled (session_busy), exit 5, as before.
An admission is persisted under the registry lock and scoped to the session and the dispatch attempt. Only one job per session is admitted at a time, including before its started (F1).
Dispatch signals are renewed often enough for every accepted ack value (F2).
A waiter is pinned to its own attempt and session (R1).
An unsignalled pending or injected record holds the session for its ack budget, whatever its FIFO place (R2, F-A).
The waiter's own started arriving early counts as admission, not as a takeover (regression fix d50610c).
Same-folder concurrent create (7844ae2; found by the orchestrator's real trial and added to this PR at the issue owner's request).
Before the fix: three create --onboard in one folder on one server could pick the same idle pane. One failed with agent_pane_busy, exit 1. This came from the shim's existing idle-pane reuse (BUG-4).
Fix:
Placement is now serialised per herdr server by lib_py/pane_lock.py, using fcntl.flock on fd 9; macOS has no flock(1).
The lock waits are bounded (MAM_PANE_CLAIM_WAIT_S), and the lock is released when its holder dies.
The lock key is the caller's actual server, also for the ambient or default target (R1).
A create also keeps a row that the monitor adopted for it.
Existing tests changed
tests/test_issue26_ints.py: the two new integer knobs join the #26 table.
tests/test_issue42_skill_blocks.py and tests/data/skill_code_blocks.json: the #42 inventory follows the SKILL.md change.
tests/test_herdr_shim_contract.py: new contract cases for the pane claim (additions).
No existing behaviour test was modified. One regression found during verification (test_issue30_stall_watch_stop::…ignores_term, rc 5 instead of 3) was fixed in the code (d50610c), not in the test.
Verification (orchestrator, independent of the reviews)
All runs: Linux, Python 3.14.7, herdr 0.9.3, detached worktrees.
Full suite at 320aa9f: run with CLAUDE_PROJECT_DIR=$HOME/.claude/projects set, as in an agent pane. Result: 12 failed / 1864 passed / 20 skipped (54 min). The 12 failures are the known tests/test_issue10_13_purge.py baseline (#28). ~/.claude/projects had no new entries.
Real-claude trial: 3 concurrent create --onboard in the same folder on a throwaway herdr session.
Before 7844ae2: 2 succeeded and 1 failed with agent_pane_busy.
At 320aa9f: all 3 exit 0, each waits for its onboarding (18 s, 25 s, 17 s) and gets its own pane. The whole run takes 42 s, about the slowest onboarding.
The trial agents, session and their conversations were purged afterwards.
Static checks: every changed .py file parses with Python 3.9 grammar, no 3.10+ APIs are used, and bash -n passes on the changed shell files.
macOS / Python 3.9.6 / bash 3.2: not run here. Please run the suite on the Mac before merging.
Review
Implementation loop: planner + creator. The creator's implementation job outlived the 3600 s budget (it kept working, and its job record was closed), so the reviews ran as --review-only loops.
Review loop 1: claude, agy and grok passed. codex NOT PASS: F1 (two jobs admitted at once) and F2 (stale live waiter with short ack windows). Earlier rounds had also fixed a token-read order issue (grok). The first two codex rounds produced no verdict: codex took more than 180 s to ack, so later loops used MAM_ACK_TIMEOUT_S=600.
Review loop 2: codex R1 (a superseded waiter adopting a new attempt) and R2 (pending onboarding ignored after 30 s), then unanimous PASS at 30c25f1.
Orchestrator verification after loop 2 found the stall-watch regression and the same-folder pane race. Both were fixed.
Review loop 3:
Round 1: claude F-A (same-second FIFO tie) and codex R1 (pane-lock key).
Round 2: F-B, the flaky test_c10 premise.
Round 3: unanimous PASS at 320aa9f (claude, agy, grok, codex).
Closes #50
Closes #55
The two issues are fixed on one branch; the commits are split by issue. #55 comes first so that every suite run after it is already protected. No version bump; the version is set at merge.
## #55 — tests no longer write into the user's real agent homes
- **Cause:** `lib.sh:77` exports `CLAUDE_PROJECT_DIR=$HOME/.claude/projects` to every MAM agent pane. `DiscoveryContext` prefers it over the `home_dir` it is given. So `test_adapter_discover` (and anything like it) wrote `-tmp-…-test-adapter-discover0-ws` into the real `~/.claude/projects` whenever an agent ran the suite. 40 such folders had accumulated.
- **Fix:**
- An autouse fixture in `tests/conftest.py` removes inherited agent-home overrides for every test: `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CODEX_HOME`, `LOCAL_BIN`. The variable list is checked against what the adapters, `base.py` and `lib.sh` actually read.
- `tests/_agent_home_guard.py` is a read-only real-home guard. It fails the session if a new entry appears in the user's agent directories during the run, in the same way as the #20 control-plane guard.
- The operational `lib.sh` export is unchanged.
## #50 — onboarding waits for completion; dispatch waits by the registry
- **A. `create --onboard` waits for the onboarding job to end** (`c29c5b2`), and so does `worktree_prepare.sh --onboard`.
- After the `started` handshake, create waits up to `MAM_ONBOARD_WAIT_S` (default 600 s) for the job to end, then prints `onboarding completed: job <id> (<n>s)`.
- New exit code 44: the onboarding ended with error or was cancelled.
- New exit code 45: it did not end in time.
- Both keep the session and write `last_visible_status: onboarding-<status>` on its row.
- The decision lives in Python. The create SKILL.md exit table and the #42 inventory are updated.
- **B. Reproduced first** (real claude, herdr 0.9.3). Despite the `← 1 agent` background indicator, herdr returned to idle about 13 s after `completed`, and a second submit went through. So herdr 0.9.3 did not cause a `session_busy` cancel by itself, and the herdr idle wait stays in place, but only as an additional check. The registry decides.
- **C. The wrapper waits by the registry before it injects** (`e9efabf` and the review fixes).
- Before injecting, the wrapper waits up to `MAM_DISPATCH_WAIT_S` for the target session's earlier open jobs, onboarding included, to end. If they don't, it ends as `cancelled` (`session_busy`), exit 5, as before.
- An admission is persisted under the registry lock and scoped to the session and the dispatch attempt. Only one job per session is admitted at a time, including before its `started` (F1).
- Dispatch signals are renewed often enough for every accepted ack value (F2).
- A waiter is pinned to its own attempt and session (R1).
- An unsignalled pending or injected record holds the session for its ack budget, whatever its FIFO place (R2, F-A).
- The waiter's own `started` arriving early counts as admission, not as a takeover (regression fix `d50610c`).
- **Same-folder concurrent create** (`7844ae2`; found by the orchestrator's real trial and added to this PR at the issue owner's request).
- **Before the fix:** three `create --onboard` in one folder on one server could pick the same idle pane. One failed with `agent_pane_busy`, exit 1. This came from the shim's existing idle-pane reuse (BUG-4).
- **Fix:**
- Placement is now serialised per herdr server by `lib_py/pane_lock.py`, using `fcntl.flock` on fd 9; macOS has no `flock(1)`.
- The lock waits are bounded (`MAM_PANE_CLAIM_WAIT_S`), and the lock is released when its holder dies.
- The lock key is the caller's actual server, also for the ambient or default target (R1).
- A create also keeps a row that the monitor adopted for it.
## Existing tests changed
- `tests/test_issue26_ints.py`: the two new integer knobs join the #26 table.
- `tests/test_issue42_skill_blocks.py` and `tests/data/skill_code_blocks.json`: the #42 inventory follows the SKILL.md change.
- `tests/test_herdr_shim_contract.py`: new contract cases for the pane claim (additions).
No existing behaviour test was modified. One regression found during verification (`test_issue30_stall_watch_stop::…ignores_term`, rc 5 instead of 3) was fixed in the code (`d50610c`), not in the test.
## Verification (orchestrator, independent of the reviews)
All runs: Linux, Python 3.14.7, herdr 0.9.3, detached worktrees.
- **Full suite at 320aa9f:** run with `CLAUDE_PROJECT_DIR=$HOME/.claude/projects` set, as in an agent pane. Result: 12 failed / 1864 passed / 20 skipped (54 min). The 12 failures are the known `tests/test_issue10_13_purge.py` baseline (#28). `~/.claude/projects` had no new entries.
- **Real-claude trial:** 3 concurrent `create --onboard` in the **same folder** on a throwaway herdr session.
- Before `7844ae2`: 2 succeeded and 1 failed with `agent_pane_busy`.
- At 320aa9f: all 3 exit 0, each waits for its onboarding (18 s, 25 s, 17 s) and gets its own pane. The whole run takes 42 s, about the slowest onboarding.
- The trial agents, session and their conversations were purged afterwards.
- **Static checks:** every changed `.py` file parses with Python 3.9 grammar, no 3.10+ APIs are used, and `bash -n` passes on the changed shell files.
- **macOS / Python 3.9.6 / bash 3.2: not run here.** Please run the suite on the Mac before merging.
## Review
- **Implementation loop:** planner + creator. The creator's implementation job outlived the 3600 s budget (it kept working, and its job record was closed), so the reviews ran as `--review-only` loops.
- **Review loop 1:** claude, agy and grok passed. codex NOT PASS: F1 (two jobs admitted at once) and F2 (stale live waiter with short ack windows). Earlier rounds had also fixed a token-read order issue (grok). The first two codex rounds produced no verdict: codex took more than 180 s to ack, so later loops used `MAM_ACK_TIMEOUT_S=600`.
- **Review loop 2:** codex R1 (a superseded waiter adopting a new attempt) and R2 (pending onboarding ignored after 30 s), then unanimous PASS at 30c25f1.
- **Orchestrator verification after loop 2** found the stall-watch regression and the same-folder pane race. Both were fixed.
- **Review loop 3:**
- Round 1: claude F-A (same-second FIFO tie) and codex R1 (pane-lock key).
- Round 2: F-B, the flaky `test_c10` premise.
- Round 3: **unanimous PASS at 320aa9f** (claude, agy, grok, codex).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
An agent pane inherits HOME_DIR, CLAUDE_PROJECT_DIR and LOCAL_BIN from
lib.sh's exports (and the adapters read CODEX_HOME). DiscoveryContext
prefers CLAUDE_PROJECT_DIR over the home_dir a test passes and every
adapter path reads HOME_DIR before HOME, so a suite run by an agent
wrote fake conversation folders into the user's real ~/.claude/projects
(test_adapter_discover, 40 folders on 2026-10-10).
- conftest._isolate_agent_homes (autouse): drops the four pane variables
for every test, so HOME - which the suite's tests set for an isolated
home - governs, as in a plain shell. Dropped rather than pointed into
tmp_path: a fixture-set HOME_DIR would shadow the 11 adapter-contract
tests that set HOME alone, the same way the pane's value did.
- conftest.agent_home_leak_guard (session autouse) + tests/_agent_home_guard.py:
snapshots the entry names of the real claude/agy/codex/grok/opencode/
hermes conversation directories (as the ambient environment names them,
recorded in pytest_configure) and fails on new ones; read-only. A claude
folder whose name decodes (against the file system) to a real directory
outside the temp roots is an operating agent's and is not counted.
- tests/test_issue55_agent_homes.py: the dropped-variable list is pinned
against the env reads of lib_py/agents, lib_py/paths.py and lib.sh;
guard unit tests; the issue's reproduction as a subprocess regression
(on base 42e5173 it leaves <dir>/-…-test-adapter-discover0-ws/u-c1.jsonl).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The handshake only said the agent saw the prompt (#23). create_session.sh
now waits, after `started`, for the onboarding job to reach a terminal
status and makes that its own result, so the orchestrator learns it from
create's exit (MULTI_AGENT_RULES §6) instead of guessing from herdr's
agent_status:
- lib_py/delegation/wrapper.py: `wait-terminal` (completed|error|cancelled,
`timeout` after --timeout, `missing` when the record is gone; stops with
its caller via --watch-pid) and `onboard-exit` (completed 0, error or
cancelled 44, timeout or missing 45). bash branches on the printed answer.
- lib.sh: delegate_wait_terminal / delegate_onboard_exit, thin calls.
- create_session.sh: `onboarding completed: job <id> (<n>s)` on stdout, or
`last_visible_status: onboarding-<status>` on the row, the reason on stderr
and `exit 44` / `exit 45` after `trap - EXIT` - the session is kept, as for
a missing handshake. The "no started" path (error, exit 0 before) now ends
with 44. MAM_ONBOARD_WAIT_S (default 600, floor 1) bounds the wait.
- worktree_prepare.sh: 44/45 pass through with "session and worktree kept".
- create SKILL.md exit table (44, 45) and the knob; the #42 inventory follows.
- tests/conftest.py: --onboard is the default, so ~60 sandbox create tests
reach the new wait. The mock herdr now acts as the agent for create's
onboarding prompt - it runs the publish commands the prompt names through
the real publish_event.py - and MAM_ONBOARD_WAIT_S=3 caps a test that turns
that off (MAM_MOCK_ONBOARD_HANDSHAKE=0 -> 44, `started` -> 45). The
delegate-job wrapper's prompt has another shape and is never answered.
- tests/test_issue50_onboard.py: the decision tables, create in the sandbox
(0 / 44 / 45, session kept, row note, trap window), the knob's integer
rule, worktree_prepare's pass-through.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reproduction first (herdr 0.9.3, a real claude, one-off server, job
51ccd800): after a job that left a background subagent running
(`← 1 agent` on screen for 5 min), herdr reported `working` only while
the job ran and `idle` ~10 s after `completed`; a second submit with a
20 s idle wait was injected, not cancelled. The 2026-10-04 observation
(0.9.1, 10+ min of `working`) does not reproduce on 0.9.3, so the herdr
idle wait stays and the registry wait runs before it (plan §4.3 (나)).
- lib_py/delegation/wrapper.py: `open-jobs` (diagnostic) and
`wait-open-jobs`. An open job of the session is live when the #18
contract says so: `running` with an event in events.log within
idle_timeout_sec; `pending` with a dispatch signal (dispatch_wait_at,
else created_at) within ack_timeout_sec. A dead record is reported
(`stale open job <id> ignored (...)`) and never waited for. The waiter
signals every 10 s by merging dispatch_wait_at into its own pending
record through the registry's transition boundary (status untouched, no
event), so later waiters see it alive; a waiter whose wrapper is gone
stops and its signal goes stale (Creator's challenge, job bf8863e1).
Release is FIFO by (created_at, job_id): `clear` when no live running
job remains and this job is the earliest live pending one; `busy <ids>`
at the bound min(MAM_DISPATCH_WAIT_S, remaining wall clock).
- the wrapper: _registry_wait after the preflight in both dispatch paths;
`busy` closes the job as cancelled/session_busy (phase registry_wait),
exit 5 - the existing contract.
- tests/test_issue50_dispatch.py: the open-job table (status x age x
signal x spelling), FIFO and the tie, the signal as a registry merge,
the bound, a dead wrapper, finalize-end's new phase, both call sites.
- tests/test_issue50_dispatch_herdr.py: a real herdr 0.9.3 server per
test (short mkdtemp HOME, fake claude the server reports idle, the real
publish path over the suite's broker): C1 wait-then-run, C2 the bound,
C3 a dead running record, C4 the loop runner's first job behind an
onboarding, C5 a session without a row, C7 two waiters in FIFO order
with no cancellation, C8 a killed waiter skipped once stale; and three
concurrent `create --onboard` / `worktree_prepare.sh --onboard` in one
server (one server process survives the shims' concurrent bootstrap,
so no bootstrap lock is needed).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MAM_ONBOARD_WAIT_S (min 1, 600) and MAM_DISPATCH_WAIT_S (min 0, 600) in
test_issue26_ints.INT_KNOBS, which pins every mam_int_knob call. The
real-herdr box's teardown removes its HOME once more after a second:
herdr writes .local/state/herdr/agent-detection a moment after the
server is gone, which left an empty /tmp/mam50-* behind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The FIFO is (created_at, job_id) at second resolution; B and C registered
within the same second once in the full suite, C sorted first by id and
cleared while B's signal was still fresh, so the stale line never printed.
C7 already keeps the 1.2 s gap; C8 now does too and asserts the order.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
F1: from phase two on, the loop's `_registry_wait` ran before the record was
reset to pending, so the waiter held a terminal record: the engine refused its
dispatch_wait_at merge and other waiters never saw it in the FIFO. The wait
(and the idle wait after it) now follows the `--status pending` update; s1
pins the order and u3c shows why a terminal self record cannot signal.
F2: the real-herdr box teardown survives a hung stop/delete, kills a server
bootstrapped by a late client call, and asserts the server and HOME are gone.
F3: the delegate-job SKILL.md exit 5 row names the registry wait.
Also drops the dead loop in s1.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `--status pending` reset is a redispatch and issues a new dispatch
token, but the loop read `_disp_tok` only after the registry and idle waits.
A busy or idle_wait close from phase two on therefore carried the previous
phase's token and the engine refused it, leaving the record pending while
the caller saw session_busy. The read now follows the reset directly; s1
pins reset < token read < registry wait.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
F1: `clear` is now an admission persisted on the released record - a merge
through the engine whose new `guard` re-reads the session's open jobs under
the registry lock, so two waiters cannot both be admitted. It is scoped to
the attempt by the dispatch token (a redispatch voids it) and reported as
`admitted` to every other waiter, which holds the session as a running job
does, whatever its FIFO place: a lower id in the same second or a
redispatched older loop job now waits.
F2: a pending record's liveness window is the dispatcher's signal cadence
times three misses (10 s x 3 = 30 s), no longer ack_timeout_sec, which the
agent's budget takes down to 1 s. After `clear` a detached hold renews the
admitted record's signal until the record leaves pending, its attempt
changes, the wrapper is gone, or the wall clock ends.
Tests: u1 boundaries, u1d/u2e/u2f/u2g/u3d/u3e for the admission, u4c/u4d for
the hold, s1 pins --hold; real herdr: C3/C7/C8 adjusted, C9 is C8's
live-owner counterpart with short ack windows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
R1: wait-open-jobs takes --dispatch-token (the token the wrapper read for
this attempt) and keeps it, with the session it asked about, for its whole
life; every write (signal, admission, the hold's renewal) is a guarded
engine merge that re-reads the record under the registry lock and refuses
once the record is not that attempt's pending one in that session. A record
redispatched, moved or closed by someone else makes the waiter answer
`superseded`: the bash wrapper exits 5 without a close, the record is its
new owner's. hold-admission is given the waiter's pinned identity instead
of reading a token from disk, so a hold that starts late holds nothing.
R2: a pending record that never carried a waiter's signal (create --onboard
registers and injects directly and waits ack_timeout_sec for `started`) is
live for that budget from created_at, not for the 30 s signal window; the
window applies only once a dispatcher has signalled.
Tests: u1 rows for the two pending lifetimes, u3b asserts the guard, u3f/u3g/u3h
(cross-session redispatch, in-place attempt change, token changed before the
waiter started) with the real waiter subprocess, u4e/u4f (late hold, running
hold released by a redispatch), s1 pins the token argument and the no-close
branch; real herdr C10 (onboarding valid before and after 30 s blocks the
session), C11 (expired onboarding is stale), C12 (redispatched waiter leaves
its record alone, exit 5).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three `create --onboard` at once into one folder (the control root, so no
control-plane prefix reaches the pane and the shim reuses an idle pane):
the shim's new-session chooses a pane - an idle one in a workspace on the
same cwd, a split, a fresh workspace's root pane - and then claims it with
`agent start`; two shims could choose the pane one of them had just made,
and the later start failed with agent_pane_busy (one create in three).
mam_herdr now runs every new-session call holding one lock per server: a
flock on fd 9, opened for the block and taken by lib_py.pane_lock, which
also bounds the wait (MAM_PANE_CLAIM_WAIT_S, default 120). It is released
with the block or with the process, however it ends. The shim heredoc is
unchanged (S11). Other servers' creates are not held.
Serialising the claims exposed a second race: a create's end-of-run
`reconcile.sh --once` adopted the pane of another create that had not yet
written its row, and that create refused its own name as already running
(exit 4). Such a row - registered by the monitor, no job, for a pane
created after this create began - is now replaced, not refused.
Tests: real herdr - shim-level and create-level three-way races into one
folder (the real herdr's pane-creating replies held back so the window is
hit on every run; both fail on the base with the orchestrator's error),
the different-folder test kept; pane_lock unit tests (per-server file,
timeout, kill -9 holder, handover, knob); the adoption rule and its three
refusals; shim contract h26 (held lock: no pane calls). The herdr box gets
a ~/.zshrc: without it every first `agent start` typed into zsh's first-run
menu and timed out at 30 s.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The registry wait judged its record by one rule: not this attempt's
pending record in this session -> superseded (redispatched, moved or
closed by someone else). That also caught the record's own `started`
landing before the prompt went out - the Issue #14 registry ack winning
over an unreachable broker, as test_issue30_stall_watch_stop acknowledges
at registration - and the wrapper left on exit 5 instead of its
broker_unavailable path (rc 3).
A record that is running under this attempt's token in this session is
this attempt's own progress: the waiter answers `clear` at once, writes
no admission and starts no hold. Another token, another session or a
terminal status stay `superseded` (review 55219be1 R1 unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
open_jobs reports a live pending record nobody signals on as `injected`:
its producer registered and injected it without a waiter (create
--onboard, a job sent by hand), so it holds the session like a running or
admitted job, and a waiter of the same second with a lower job id, or one
registered a moment earlier and still in its preflight, no longer passes
it in the FIFO. The waiter signals on its own record before its first
scan, so its record is never mistaken for one.
lib_py.pane_lock resolves the shim's ambient target (no -L,
HERDR_SESSION_NAME empty or `default`) to the server of the pane the
caller runs in - HERDR_SESSION, else HERDR_SOCKET_PATH's layout, as
lib.sh _herdr_caller_server reads them - so an ambient and a named call
into one server claim under one lock; outside a pane it stays the
default server's.
Tests: u2h (the tie, the later-registered onboarding, the signalled
control), u1/u4 labels, c10 forces the same-second registration, p1b,
s2 (real herdr: held lock stops both call forms; mixed concurrent calls
take distinct panes). Two FIFO fixtures now carry a waiter's signal.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The live onboarding test asserted that the first job's wrapper registered in
the same wall-clock second as the onboarding it had just placed at a second
boundary. Process start-up has no sub-second contract, so the setup assertion
failed now and then before any dispatch behaviour was checked. The test now
stamps the onboarding record, which it owns and which nothing else depends on
yet, with the job's created_at once that record exists; the stamp is written
by atomic replace so a scanning wrapper never skips a half-written record.
The lifecycle assertions and the fixed-id tie coverage in test_u2h are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- admission: a `submit --dry-run` record is marked dry_run and is not an
open job of its session (review 1e25cf2f F1)
- create: the monitor-row adoption requires the row's server to be the one
this create spawned on (review 1e25cf2f F2)
- pane lock: retry only on contention (BlockingIOError); the docs say the
flock lasts while any fd-9 holder (the claim shell and the shim) lives
(review 66680d9d)
- tests on macOS: real-herdr box HOMEs under /tmp (sun_path is 104 bytes),
resolved paths, the fake claude reads non-canonical input (MAX_CANON 1024)
and the pane is read unwrapped; lock holders exec their sleep so killing
them frees the lock; the adoption tests name the server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
A `default` scan runs without -L, so the shim answers it from the inherited
HERDR_SESSION_NAME; a create's reconcile --once labelled that server's panes
`default`, and since create adopts only a row of its own server (1e25cf2f
F2), parallel creates in one folder were refused (test_tier4_e2e scenario 4).
The skill-blocks data follows create_session.sh's SystemExit line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Merge review on macOS (--review-only loops, --all-reviewer): round 1 at
320aa9f 4/4 NOT PASS, round 2 codex F1 (a claimed dry-run job holds its
session), round 3 4/4 PASS (discarded: the worktree changed mid-review),
round 4 at 21f0c12 claude/agy/grok PASS with codex interrupted by a stream
disconnect, round 5 codex PASS. Full suite at 21f0c12 on macOS / Python
3.9 / bash 3.2 / herdr 0.9.3: 1860 passed / 0 failed / 38 skipped; no new
entries in ~/.claude/projects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu
merged commit 8b0b4a07dc into main2026-10-12 12:38:55 +00:00
macOS verification before merge (macOS / Python 3.9 / bash 3.2 / herdr 0.9.3): the suite at 320aa9f had macOS-only failures. They were fixed on top of the branch:
a9ffeda: AF_UNIX path 104 bytes (test homes under /tmp), the fake TUI's tty canonical 1024-byte line limit (stty -icanon), bash 3.2 not exec'ing the last command (exec sleep), _adopted_own also requires the row's server, the pane lock retries only contention, and a submit --dry-run record no longer holds its session.
efd27de: once pick-pending claims a dry-run job, it holds the session again (review 211037ab F1).
21f0c12: the monitor writes the server that actually answered a default scan. Before this, the e2e same-folder parallel create was refused with rc 4 after the F2 server check.
Round 3: 4/4 PASS, but discarded because the worktree changed mid-review.
Round 4 at 21f0c12: claude, agy and grok PASS; codex's turn was cut off by a stream disconnect.
Round 5: codex PASS.
The reports are in .agents/reports/<session>/.
Full suite at 21f0c12: 1860 passed / 0 failed / 38 skipped (75 min). ~/.claude/projects had no new entries. After the merge, the touched guard modules on main passed (205 tests).
Merged as 8b0b4a0 (v4.9.0, release commit ffe8665).
**macOS verification before merge** (macOS / Python 3.9 / bash 3.2 / herdr 0.9.3): the suite at 320aa9f had macOS-only failures. They were fixed on top of the branch:
- a9ffeda: AF_UNIX path 104 bytes (test homes under /tmp), the fake TUI's tty canonical 1024-byte line limit (`stty -icanon`), bash 3.2 not exec'ing the last command (`exec sleep`), `_adopted_own` also requires the row's server, the pane lock retries only contention, and a `submit --dry-run` record no longer holds its session.
- efd27de: once pick-pending claims a dry-run job, it holds the session again (review 211037ab F1).
- 21f0c12: the monitor writes the server that actually answered a `default` scan. Before this, the e2e same-folder parallel create was refused with rc 4 after the F2 server check.
**Review** (`--review-only`, all four reviewers):
- Round 1 at 320aa9f: 4/4 NOT PASS.
- Round 2: codex F1.
- Round 3: 4/4 PASS, but discarded because the worktree changed mid-review.
- Round 4 at 21f0c12: claude, agy and grok PASS; codex's turn was cut off by a stream disconnect.
- Round 5: codex PASS.
The reports are in `.agents/reports/<session>/`.
**Full suite at 21f0c12:** 1860 passed / 0 failed / 38 skipped (75 min). `~/.claude/projects` had no new entries. After the merge, the touched guard modules on main passed (205 tests).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #50
Closes #55
The two issues are fixed on one branch; the commits are split by issue. #55 comes first so that every suite run after it is already protected. No version bump; the version is set at merge.
#55 — tests no longer write into the user's real agent homes
lib.sh:77exportsCLAUDE_PROJECT_DIR=$HOME/.claude/projectsto every MAM agent pane.DiscoveryContextprefers it over thehome_dirit is given. Sotest_adapter_discover(and anything like it) wrote-tmp-…-test-adapter-discover0-wsinto the real~/.claude/projectswhenever an agent ran the suite. 40 such folders had accumulated.tests/conftest.pyremoves inherited agent-home overrides for every test:HOME_DIR,CLAUDE_PROJECT_DIR,CODEX_HOME,LOCAL_BIN. The variable list is checked against what the adapters,base.pyandlib.shactually read.tests/_agent_home_guard.pyis a read-only real-home guard. It fails the session if a new entry appears in the user's agent directories during the run, in the same way as the #20 control-plane guard.lib.shexport is unchanged.#50 — onboarding waits for completion; dispatch waits by the registry
create --onboardwaits for the onboarding job to end (c29c5b2), and so doesworktree_prepare.sh --onboard.startedhandshake, create waits up toMAM_ONBOARD_WAIT_S(default 600 s) for the job to end, then printsonboarding completed: job <id> (<n>s).last_visible_status: onboarding-<status>on its row.← 1 agentbackground indicator, herdr returned to idle about 13 s aftercompleted, and a second submit went through. So herdr 0.9.3 did not cause asession_busycancel by itself, and the herdr idle wait stays in place, but only as an additional check. The registry decides.e9efabfand the review fixes).MAM_DISPATCH_WAIT_Sfor the target session's earlier open jobs, onboarding included, to end. If they don't, it ends ascancelled(session_busy), exit 5, as before.started(F1).startedarriving early counts as admission, not as a takeover (regression fixd50610c).7844ae2; found by the orchestrator's real trial and added to this PR at the issue owner's request).create --onboardin one folder on one server could pick the same idle pane. One failed withagent_pane_busy, exit 1. This came from the shim's existing idle-pane reuse (BUG-4).lib_py/pane_lock.py, usingfcntl.flockon fd 9; macOS has noflock(1).MAM_PANE_CLAIM_WAIT_S), and the lock is released when its holder dies.Existing tests changed
tests/test_issue26_ints.py: the two new integer knobs join the #26 table.tests/test_issue42_skill_blocks.pyandtests/data/skill_code_blocks.json: the #42 inventory follows the SKILL.md change.tests/test_herdr_shim_contract.py: new contract cases for the pane claim (additions).No existing behaviour test was modified. One regression found during verification (
test_issue30_stall_watch_stop::…ignores_term, rc 5 instead of 3) was fixed in the code (d50610c), not in the test.Verification (orchestrator, independent of the reviews)
All runs: Linux, Python 3.14.7, herdr 0.9.3, detached worktrees.
320aa9f: run withCLAUDE_PROJECT_DIR=$HOME/.claude/projectsset, as in an agent pane. Result: 12 failed / 1864 passed / 20 skipped (54 min). The 12 failures are the knowntests/test_issue10_13_purge.pybaseline (#28).~/.claude/projectshad no new entries.create --onboardin the same folder on a throwaway herdr session.7844ae2: 2 succeeded and 1 failed withagent_pane_busy.320aa9f: all 3 exit 0, each waits for its onboarding (18 s, 25 s, 17 s) and gets its own pane. The whole run takes 42 s, about the slowest onboarding..pyfile parses with Python 3.9 grammar, no 3.10+ APIs are used, andbash -npasses on the changed shell files.Review
--review-onlyloops.MAM_ACK_TIMEOUT_S=600.30c25f1.test_c10premise.320aa9f(claude, agy, grok, codex).🤖 Generated with Claude Code
Merged as
8b0b4a0(v4.9.0, release commitffe8665).macOS verification before merge (macOS / Python 3.9 / bash 3.2 / herdr 0.9.3): the suite at
320aa9fhad macOS-only failures. They were fixed on top of the branch:a9ffeda: AF_UNIX path 104 bytes (test homes under /tmp), the fake TUI's tty canonical 1024-byte line limit (stty -icanon), bash 3.2 not exec'ing the last command (exec sleep),_adopted_ownalso requires the row's server, the pane lock retries only contention, and asubmit --dry-runrecord no longer holds its session.efd27de: once pick-pending claims a dry-run job, it holds the session again (review 211037ab F1).21f0c12: the monitor writes the server that actually answered adefaultscan. Before this, the e2e same-folder parallel create was refused with rc 4 after the F2 server check.Review (
--review-only, all four reviewers):320aa9f: 4/4 NOT PASS.21f0c12: claude, agy and grok PASS; codex's turn was cut off by a stream disconnect.The reports are in
.agents/reports/<session>/.Full suite at
21f0c12: 1860 passed / 0 failed / 38 skipped (75 min).~/.claude/projectshad no new entries. After the merge, the touched guard modules on main passed (205 tests).