Commit Graph
323 Commits
Author SHA1 Message Date
GodopuandClaude Fable 5.1 63d816468e test(issue-57): the derivable audit-log root is recorded, not forwarded
New: prefix()/CLI forward three keys for a sibling binding and four for an
explicit root; create records the sibling on the row, omits it from the
pane's --env and still pins it in the injected publish commands; the
issue's scene (a pane with WORKSPACE_ROOT and MAM_JOBS_DIR only) moves its
audit log with --registry-dir and leaves the control root alone;
worktree_prepare's pane --env carries no DELEGATE_JOB_LOGS_DIR.

The three existing four-key forwarding assertions (user-approved) compare
FORWARDED_ENV = CP_ENV[:3]; CP_ENV and the negative assertions are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 13:11:29 +00:00
GodopuandClaude Fable 5.1 c982616188 fix(issue-57): review nits - a damaged audit sequence or an unresolvable log root never aborts the check
_check_disk_fallback converted transition_seq outside its try, so a
status.json whose sequence is not an integer would raise out of the
subscriber's main loop; such a status is now ignored with a warning, like a
stale one. delegate_started_recorded returned 1 before looking at the
registry's events.log when the audit-log root could not be resolved; it now
skips only the audit check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 12:46:50 +00:00
GodopuandClaude Fable 5.1 f299034d8d fix(subscriber): the disk fallback takes an audit status only when it is at least as new as the record
The audit mirror is written after the registry commits, so a redispatched
record (attempt 2, pending) can sit next to attempt 1's completed
status.json, and a failed mirror leaves it there. The fallback validated the
token and attempt against the record, which say nothing about the audit
status's provenance, and closed attempt 2 with attempt 1's completion
(review P1, Issue #57). A versioned record (transition_seq) now takes the
audit status only when its transition_seq is at least as new; an audit
status without a sequence cannot prove which attempt it describes and is
ignored by a versioned record. An unversioned record (never transitioned,
or MAM_ENGINE_CAS=0) and a missing record keep the old rule.

Regressions: the redispatch/flush interval, the failed-mirror leftover, the
unversioned audit status, a newer audit-only terminal, and conflict
refusals and legacy claims next to a moved registry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 11:47:17 +00:00
GodopuandClaude Fable 5.1 813cfd38d1 test(issue-57): the audit log follows a moved registry
New file tests/test_issue57_logs_follow_registry.py: the resolver (L1),
registry.py (L2), the wrapper's submit/wait/logs (L3), publish_event.py
(L4), control_plane.from_env, worktree_prepare, status.sh, the comment-5
disk fallback, and the invariant that an explicit DELEGATE_JOB_LOGS_DIR
still wins. Every test ends by asserting the workspace's own
.mam/delegate_job_logs was never created.

Two existing tests gain monkeypatch.delenv("MAM_JOBS_DIR"): conftest's
autouse _isolate_mam_state_dirs gives every test a MAM_JOBS_DIR, and under
the sibling rule the WORKSPACE_ROOT/cwd fallbacks they assert would
otherwise resolve next to that registry. The order they assert (explicit
root, workspace, cwd) is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 10:02:08 +00:00
GodopuandClaude Fable 5.1 a159a790df fix(subscriber): disk fallback reads status.json's status, not the dict
read_logged_status() returns the status.json dict; _check_disk_fallback
compared that dict with the terminal status strings, so a terminal status
recorded only in the audit log was never recognised (Issue #57, comment 5).
The dispatch-token and attempt checks are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 10:00:25 +00:00
GodopuandClaude Opus 5.5 3a30d7da89 fix(tests): u1e gives the wrapper its own WORKSPACE_ROOT - it wrote the checkout's .mam/shim/herdr
test_u1e_a_dry_run_record_is_not_an_open_job (PR #56) ran the delegate-job
wrapper without WORKSPACE_ROOT; the wrapper sources lib.sh, which then
defaults it to the checkout and rewrites <checkout>/.mam/shim/herdr
(found by review 38a64423). The node joins W1's list, which runs its
nodes in a copy of the checkout and fails on any .mam written there.
A scan of the #50/#55 and leak modules from a clean worktree found no
other writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-13 11:57:46 +09:00
GodopuandClaude Opus 5.5 db7f4cab9c test(sandbox): mam_sandbox also drops HERDR_SESSION (reviews 63919c5d / 191f0ec3, non-blocking)
The caller-server helpers read it, so tests branched on whether they ran in
an agent pane. L2 covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-13 10:01:55 +09:00
GodopuandClaude Opus 5.5 325b056057 test(issue-23): the generated-shim pin moves to the real-server leak fix (525df2b)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-13 09:17:57 +09:00
GodopuandClaude Opus 5.5 525df2bb58 fix(tests): tests no longer reach a real herdr server - a *-shim ancestor hid the mock, the sandbox kept the pane's socket
A suite run from an agent pane with --basetemp .../pytest-pr56-shim left
seven workspaces on that pane's real server (multi-agent-mux):
- the real-herdr resolvers (lib.sh _is_shim_path and the generated shim)
  skipped every PATH directory with "-shim" anywhere in its path, so the
  basetemp hid the mock's bin/ and the real herdr ran; only a directory
  that is itself *-shim is a wrapper now;
- mam_sandbox kept HERDR_SOCKET_PATH (and the pane/tab ids), which the real
  herdr follows over the sandbox HOME; it drops them now.

tests/test_herdr_real_server_leak.py: L1 (ancestor *-shim, both resolvers)
and L2 (pane routing dropped) fail before the fix; L1b keeps a *-shim
wrapper directory excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-13 09:15:22 +09:00
GodopuandClaude Opus 5.5 21f0c12b9e fix(issue-50): the monitor labels a row with the server that answered
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
2026-10-12 10:37:50 +09:00
GodopuandClaude Opus 5.5 efd27def47 fix(issue-50): a claimed dry-run job holds its session (review 211037ab F1)
open_jobs skips a dry-run record only while it is pending: a pick-pending
claim makes it running work, which the admission must wait for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-12 10:12:05 +09:00
GodopuandClaude Opus 5.5 a9ffedac36 fix(issue-50): review findings of PR #56 on macOS
- 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
2026-10-12 09:03:08 +09:00
GodopuandClaude Fable 5.1 320aa9f7a0 test(issue-50): c10 makes the same-second tie instead of timing it (reviews 98b31bb2 / 352066ec / 244b8072 F-B)
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>
2026-10-11 12:05:19 +00:00
GodopuandClaude Fable 5.1 6c99fedf40 fix(issue-50): an injected record holds its session whatever its FIFO place; the ambient pane claim locks the caller's server (reviews 5a09a13f F-A, e8e471ff R1)
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>
2026-10-11 08:50:46 +00:00
GodopuandClaude Fable 5.1 d50610c1ab fix(issue-50): a waiter whose own attempt is already running answers clear, not superseded
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>
2026-10-11 06:28:01 +00:00
GodopuandClaude Fable 5.1 7844ae2c5f fix(create): one pane claim at a time per herdr server; a create keeps the pane the monitor registered for it
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>
2026-10-11 06:05:52 +00:00
GodopuandClaude Fable 5.1 30c25f193c fix(issue-50): the waiter and its hold are pinned to one attempt and session; an unsignalled pending record lives by its ack budget (review 55219be1 R1, R2)
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>
2026-10-11 02:58:16 +00:00
GodopuandClaude Fable 5.1 9cbe08e8de fix(issue-50): the registry wait admits one job per session and holds it (review aa1bff92 F1, F2)
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>
2026-10-11 01:29:57 +00:00
GodopuandClaude Fable 5.1 9e78ff3ba3 fix(issue-50): the loop path reads this attempt's token before it waits (review reviewer-grok-01)
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>
2026-10-10 18:14:48 +00:00
GodopuandClaude Fable 5.1 3705bc72a8 fix(issue-50): the loop path waits by the registry on its pending record (review F1-F3)
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>
2026-10-10 17:28:09 +00:00
GodopuandClaude Fable 5.1 05bffc6475 test(issue-50): C8 registers its two jobs in different seconds
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>
2026-10-10 17:14:14 +00:00
GodopuandClaude Fable 5.1 630f04bfdc test(issue-50): the two new knobs join the #26 table; the herdr box sweeps a late state write
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>
2026-10-10 16:05:59 +00:00
GodopuandClaude Fable 5.1 e9efabf74b feat(issue-50): the delegate-job wrapper waits by the registry before it injects (C)
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>
2026-10-10 15:39:21 +00:00
GodopuandClaude Fable 5.1 c29c5b2142 feat(issue-50): create --onboard waits for the onboarding job to end (A)
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>
2026-10-10 15:39:21 +00:00
GodopuandClaude Fable 5.1 40293841e7 test(issue-55): tests no longer write into the user's real agent homes
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>
2026-10-10 14:29:36 +00:00
GodopuandClaude Opus 5.5 a93c9bf292 test(loop-lock): the b13 runs of the checkout's run_loop.sh take a loop lock of their own
test_b13_reexec_preserves_original_argv and test_b13_no_freeze_switch_
disables_reexec ran the checkout's run_loop.sh with --creator, which gets
past argument parsing to mam_acquire_loop_lock on the default marker,
$MAM_REAL_ROOT/.mam/loop-guard-active with MAM_REAL_ROOT = the script's own
checkout. In the main checkout that is the live loop's lock (taken and
released for a moment: a real loop refused, or the test refused); in a review
tree it left an empty .mam. Both now set MAM_LOOP_MARKER under their tmp_path.

A sweep of the 16 modules that use run_loop.sh (a per-test watcher for a new
.mam in the checkout, an empty .mam removed before each test) found only
these two; the others pin MAM_LOOP_MARKER already, end before the lock
(--help, argument errors), or run a copy of the tree.

Tests: test_loop_marker_isolation.py L1 runs every test that reaches the lock
through the checkout's run_loop.sh in a copy of the checkout and requires no
.mam there; W1 of the Issue #17 isolation test now also requires that no .mam
exists (it only looked for files, so the empty lock directory passed it). At
97c2946 both fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-09 09:01:34 +09:00
GodopuandClaude Opus 5.5 ac48cb7861 test(issue-17): test_o1_rebuttal and test_o2_race_free_lock run run_loop.sh under their own WORKSPACE_ROOT (review claude, grok)
Two modules still ran the checkout's run_loop.sh - which sources lib.sh
before it parses an option - with the process environment and no root of
their own: test_o2_race_free_lock's test_o2_12 (review claude; it was in the
sweep's list and missed in 8b47cc7) and test_o1_rebuttal's
test_v3_rebut_option_parsing_and_budget, whose calls pass no env (review
grok; outside the env-copy grep). Both now use own_workspace_root, and W1
runs one node of each: at 8b47cc7 W1 fails with the copy's .mam/shim/herdr.

A last pass over the test files that run lib.sh, run_loop.sh, the wrapper or a
skill script: every one is marked, uses mam_sandbox or pins WORKSPACE_ROOT,
or does not source lib.sh (b7 diff_collect, deploy, issue42, issue45 config,
issue5_phase0, local_mqtt).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-09 02:24:22 +09:00
GodopuandClaude Opus 5.5 8b47cc76ee test(issue-17): every test that sources lib.sh does it under its own WORKSPACE_ROOT (review grok, codex)
d198dd5 fixed one helper; the same defect was in eleven more modules
(reviews grok and codex R1/R2): their shells inherited the process
environment, so each `source lib.sh` rewrote <WORKSPACE_ROOT>/.mam/shim/herdr
- the checkout's own when the variable is unset or names this checkout (which
pytest_configure keeps), another workspace's under --noconftest.

- conftest: own_workspace_root, an opt-in fixture setting WORKSPACE_ROOT to
  <tmp_path>/.mam-test-state/workspace. Not autouse: runner.py and
  mqtt_common read WORKSPACE_ROOT in-process, and some tests unset it on
  purpose. mam_sandbox and explicit envs run later and still win.
- pytestmark usefixtures("own_workspace_root") in test_version_consistency,
  test_b19_headless_reconcile_fixes, test_b8_send_keys_verification,
  test_issue6_fail_closed, test_loop_cli, test_issue5_phase3_loop,
  test_issue5_phase4_runtime, test_o3_scoped_guard, test_issue25_plan_reuse,
  test_orc_onboard (their shells inherit os.environ, many with no env at all).
- test_orc_onboard (R2): SCRUBBED_ENV is taken at import and no longer
  carries WORKSPACE_ROOT; every call builds _scrubbed_env(root) with the root
  of the call (ws / cwd / mam_sandbox, else this test's).
- test_workspace_scope (unittest) and test_issue21_worktree_prereqs._mutate
  pin the root they already create.

Tests (test_issue17_workspace_root_isolation.py): W1 copies the checkout and
runs one test of each of those modules there with WORKSPACE_ROOT naming the
copy - the copy gets no .mam; W2 imports test_orc_onboard under a sentinel
root, then calls it under another - the sentinel stays empty. At d198dd5 both
fail (the copy gets .mam/shim/herdr; the sentinel gets it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-09 00:22:54 +09:00
GodopuandClaude Opus 5.5 d198dd50aa test(issue-17): item-4 helpers run lib.sh in the test's own WORKSPACE_ROOT
_env kept an inherited WORKSPACE_ROOT, so every helper that sources the
wrapper (lib.sh's _init_herdr_isolation) rewrote <that workspace>/.mam/shim/
herdr - another workspace's during the PR #51 review (v4.8.7 review, claude).
Only dropping it is not enough: lib.sh then derives the checkout itself, and
conftest keeps a WORKSPACE_ROOT that names this checkout, so the run would
rewrite the checkout's own shim (the real one when this is the main
checkout). _env now sets WORKSPACE_ROOT to the test's tmp dir.

Test: with a sentinel WORKSPACE_ROOT in the environment, _run_both and a
direct _env run leave the sentinel empty and the checkout's shim untouched,
and the shell sees the tmp dir. With the old _env the sentinel gets
.mam/shim/herdr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 23:10:35 +09:00
GodopuandClaude Opus 5.5 65b78b6ec4 fix(issue-17): a failed yes/no decision is not a "no" (review codex R1)
subscriber-up and stall-watch answered only by exit code, so a call that
could not run (module error rc 1, missing interpreter 127) read as "no":
with a subscriber that had ended 0, the error path mapped 0 to completed
and the submit ended with 0; a failed stall-watch silently skipped the
watcher.

Both now take --print: the answer is printed as yes/no and the exit code
is 0 for either. The wrapper asks with --print through _decided_yes, so a
nonzero exit, no answer or anything else is _decision_failed (ERROR line,
exit 1). Without --print the rc contract is unchanged (yes 0, no 1).

Tests: subscriber-up (direct and loop/discuss call sites) and stall-watch,
each failing by traceback, rc 127 or no answer with a subscriber that
ended 0, end the submit with 1 and the ERROR line - at 8241bd5 eight of the
nine ended with 0 (the loop/no-answer one ended with 1 through the loop's
own result and failed only on the ERROR line); yes and no keep their
branches in both paths; --print unit table; the static guard forbids an
rc-read yes/no decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 09:26:41 +09:00
GodopuandClaude Opus 5.5 541c3febe8 test(issue-17): the frozen wrapper literals follow _decided/_exit_for (review agy)
a670ef5 routed the wrapper's sub-status and exit-code decisions through
_decided / _exit_for, but test_tier1_unit G-10 and test_issue5's
test_wrapper_exit_code_mapping_literals_are_frozen still looked for the raw
`_decide sub-status "$sub_rc"` and `_decide exit-code "$job_status"`. They now
pin `_decided job_status sub-status "$sub_rc"` and `_exit_for "$job_status"`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 09:26:41 +09:00
GodopuandClaude Opus 5.5 8241bd53b1 test(issue-17): the failing wait-rc test sources the wrapper in its own WORKSPACE_ROOT
Sourcing the wrapper sources lib.sh, which writes $WORKSPACE_ROOT/.mam/shim/
herdr; without WORKSPACE_ROOT that is the checkout. The new N2 wait-rc test
now runs with its tmp dir as WORKSPACE_ROOT, so the N2 tests leave the
checkout untouched. (The existing C tests in this file still write the
checkout's .mam/shim/herdr, as at 56d0bfd - not changed here.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 08:14:05 +09:00
GodopuandClaude Opus 5.5 a670ef53ee fix(issue-17): a decision the wrapper cannot make ends it with 1 (N2)
`exit "$(_decide exit-code ...)"` became `exit ""` when wrapper.py could
not answer (its module or $PY broken): rc 255 on bash 3.2. The other printed
decisions failed open in their own ways: a failed wait-rc returned "" (255),
a failed finalize-begin left finalize-end with an empty --final (argparse
rc 2), and a failed sub-status or finalize-end ended under set -e with the
interpreter's own code (1 for a traceback, 127 for one that cannot run).

Printed decisions now go through _decided <var> <subcommand>, and the
exits through _exit_for <status>: a failed call, an empty answer or a
non-numeric exit code / wait rc prints one ERROR line and exits 1. finalize-
end may legitimately answer nothing (its wrapper gone), so only its rc is
checked. The normal path is unchanged: same answers, exit codes and output.

Left as they are: subscriber-up and stall-watch are answered by rc, so a
failure reads as "no" - the subscriber is then treated as dead (the error
path, whose sub-status/exit-code are now fail-closed) and the notify-only
stall watcher is not started.

Tests (F): the real submit (human job, unreachable broker, 3 s budget) with
a $PY that fails one decision - exit-code, sub-status, finalize-begin,
finalize-end; traceback, rc 127 or no answer - ends with 1 and the ERROR
line; the same run without a failure still ends with 3. cmd_wait with a
failing wait-rc ends with 1. A static guard keeps printed decisions off
raw $(...). At 56d0bfd: exit-code 255/255, finalize-begin 2/2, wait-rc
255/255, sub-status and finalize-end 127 for "gone"; the traceback and
no-answer cases there already ended with 1 (by set -e or by the empty
status mapping to 1) and fail only on the missing ERROR line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 08:12:16 +09:00
GodopuandClaude Opus 5.5 f19f94099f fix(issue-17): resume closes herdr 0.9.3's placeholder for a removed directory
With the cwd premise fixed, test_b still failed on herdr 0.9.3: a pane whose
saved directory is gone is restored as a placeholder - `restore_error`
"Saved directory is unavailable...", no process - and `pane process-info`
answers pane_not_found. close_restored_shells raised on it and exited 1
before looking at the other panes, so the resumed session's removed-worktree
pane (the case #45 closes on herdr 0.9.1, where it is a shell in $HOME) was
left behind.

Such a pane has nothing running, so it now meets condition 4. Only that
exact answer counts - restore_error set AND pane_not_found; any other
process-info failure still stops the run with exit 1 as before.

Tests: test_issue45_restore.py passes on herdr 0.9.3 (5/5). Unit tests with a
stub herdr: the placeholder is closed (fails at 56d0bfd); pane_not_found
without restore_error, or restore_error with another error, still raises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 07:53:58 +09:00
GodopuandClaude Opus 5.5 25c99704f2 test(issue-17): herdr 0.9.3 keeps a removed directory as the restored pane's cwd
test_b_only_the_resumed_sessions_restored_shell_is_closed asserted that a
pane whose directory was removed comes back in $HOME. That is herdr 0.9.1;
herdr 0.9.3 keeps the old path. The cwd check now takes either, with a note
on which version does what; closing never looked at the cwd.
herdr_restore.py's docstring said the same "$HOME when that directory is
gone" and now names both versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-08 07:52:40 +09:00
GodopuandClaude Opus 5.5 4c86107846 fix(issue-17): review R1 - a finalize wait is measured on the monotonic clock, like bash's sleep
codex, PR #51 round 1. wrapper._sleep computed the span and the remainder
with time.time(). The broker re-observation (up to 3 s) and the disk-heartbeat
grace (1 s) use it, so a wall clock stepped back during the wait stretched it
by the step: on a mocked clock stepped back 10 s, an already completed job was
seen after 11 s / 13 s instead of 1 s / 3 s. The old bash ran `sleep 1` /
`sleep "$left"`, which do not follow the wall clock.

_sleep now measures on time.monotonic(). The deadline (START_TIME + TIMEOUT),
the evidence window and event timestamps stay on the wall clock (_now), as in
the bash.

test_u_a_wait_is_not_stretched_by_a_wall_clock_step_back (1 s, 3 s) swaps the
module's time for a clock stepped back 10 s during the first sleep; on the
previous code it measures 11.0 / 13.0 and fails. The comparison test and the
finalize-related behaviour tests (issue33, issue18, issue14) on macOS /
Python 3.9: 321 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-08 00:53:09 +09:00
GodopuandClaude Opus 5.5 518bedac5d fix(issue-17): review R1 round 2 - an exited, unreaped wrapper is not a live owner
kill(pid, 0) succeeds for a zombie. A caller that captures the wrapper's
output and calls communicate() after TERM leaves the wrapper unreaped while
the finalizer holds its pipes, so the owner check passed until the deadline
and the job was still closed (codex, round 2; reproduced with the real
registry). _alive now asks `ps -o stat=` (POSIX, no /proc) once kill(0)
succeeds and treats state Z as gone, as tests/conftest.py::_pid_alive does;
no ps answer keeps kill(0)'s verdict. The owner is looked at every 0.5 s.

Tests: TERM + communicate() with no wait() for both graces (stand-ins) and
for the timeout grace with the real registry, old vs new; a zombie is not
alive. All four fail on c5cebe6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:09:13 +00:00
GodopuandClaude Opus 5.5 c5cebe6c32 fix(issue-17): review R1 - the finalizer ends with its wrapper
finalize-end ran the disk grace and the broker re-observation as a child
of the wrapper; TERM to the wrapper left it running, and at the deadline it
still closed the job and published the error (codex R1, reproduced with the
real registry). The bash loops it replaced ended with the wrapper.

The wrapper now passes its own pid (--watch-pid "$$", as for the stall
watcher). finalize-end checks it every 0.2 s through both waits and before
the close and the event, and leaves without either once it is gone.
A comparison test sends TERM to the old and the new wrapper inside each
grace: both exit at once, leave no process behind and make no call.

Also: no blank line at the end of the old-decisions fixture, and the #42
inventory follows main() one line down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 07:02:26 +00:00
GodopuandClaude Opus 5.5 66d1490230 test(issue-17): item 4 - l15's stand-in control root carries lib_py/ints.py next to lib_int.sh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:37:02 +00:00
GodopuandClaude Opus 5.5 6947d3c785 test(issue-17): item 4 - the #42 inventory follows runner.py's shorter header
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:36:48 +00:00
GodopuandClaude Opus 5.5 3b3ce307df test(issue-17): item 4 - the #42 inventory points at main() where it now is
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:36:23 +00:00
GodopuandClaude Opus 5.5 6bf9f92462 test(issue-17): item 4 - the frozen-mapping test imports the wrapper module directly (it has no scripts/ shim)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:08:35 +00:00
GodopuandClaude Opus 5.5 63d9c205ff refactor(issue-17): item 4 - the delegate-job wrapper's decisions move to Python
The wrapper keeps spawn / wait / kill, key injection and the herdr
diagnostic probe; what it decided in bash is now lib_py/delegation/wrapper.py,
reached through one `_decide` helper:

- finalize_unfinished: the clock choice, the disk grace and broker
  re-observation, the close request / label, the close and the event
  (finalize-begin / finalize-end; bash runs only the busy probe between them)
- start_stall_watch: whether to watch (MAM_STALL_WATCH, remote, human)
- _map_sub_rc, _rc_for (+ MAM_DELEGATE_FAIL_CLOSED), the subscriber
  early-exit check and cmd_wait's rc normalisation
- mam_int / mam_int_knob run lib_py/ints.py, which is also the runner's
  _positive_int: one implementation of the Issue #26 rule

No behaviour change: tests/test_issue17_item4_decisions.py runs the old
bash functions (copied verbatim from 7209750) and the new wrapper on the
same inputs and compares output, FINAL_JOB_STATUS and the registry /
publish calls. Three static tests that pinned the old bash literals now
pin the Python entry points instead.

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 04:59:58 +00:00
GodopuandClaude Opus 5.5 ab93776d6d fix(issue-45): review round 2 - an unrunnable config check is not a pass, odd pids never close a pane, nan does not drop settings
codex round 2 (PR #48).

- R1: when `herdr config check` could not run (timeout, OSError), the
  merged config was handed to the server as checked. A merge with a schema
  error makes herdr drop the whole file, restore back on - the #45 failure.
  The merged body is no longer used unchecked: _write_checked falls to the
  fixed MINIMAL config with a warning. When the check cannot run on MINIMAL
  either, MINIMAL is still used (with its own warning) rather than no
  config: returning none would leave restore on, and MINIMAL is a fixed
  two-line file the real-herdr test accepts. Unknown non-zero exits stay as
  they were - herdr exits non-zero for harmless warnings (an unknown
  section) that do not drop the file.
- R2: _shell_in_foreground compared shell_pid and the foreground group with
  ==, so 0/0, False/False, ""/"" or -1/-1 closed the pane. Both must now be
  positive ints (bool excluded) before they are compared.
- R3: the re-parse check used ==, and nan != nan, so a valid TOML nan threw
  away every other user setting even on Python 3.11. _same_doc compares
  documents recursively with nan equal to nan.

Tests: R1 (timeout, OSError) and R3 (top-level, array, nested nan) in
test_issue45_herdr_config.py; R2 in the new tests/test_issue45_restore_unit.py
(a herdr stub: zero, false, empty, negative, null, string, true, float;
missing info; a real shell closed; a busy pane kept). On the previous code
12 of them fail. Python 3.11: 67 passed; Python 3.9: 33 passed / 34 skipped
(tomllib-only). Real-herdr restore tests and related files on 3.9: 197
passed / 1 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-05 17:50:15 +09:00
GodopuandClaude Opus 5.5 e8d9151226 test(issue-45): review - the real-server tests run on macOS, and the bootstrap config check follows the bootstrap's python3
claude round 1, B2 and B3 (B1 was the previous commit).

- B2, tests/test_issue45_restore.py: the throwaway root is resolved, because
  on macOS /tmp links to /private/tmp and herdr reports real paths (two B
  tests compared /tmp/... with /private/tmp/...). The server's environment
  is read from /proc on Linux and from `ps -E` elsewhere (macOS has no /proc;
  two A tests failed with FileNotFoundError).
- B3, tests/test_issue9_server_env.py: the bootstrap builds the config with
  the python3 on PATH, which here is 3.9 without tomllib, so the test's
  merged-settings expectation could not hold and its own `import tomllib`
  failed. The test now parses the result with that same python3; when it
  has no tomllib, it expects the key-only config and the bootstrap's
  "no tomllib" warning instead.

macOS, Python 3.9 venv: test_issue45_restore, test_issue9_server_env and
test_issue45_herdr_config 42 passed / 29 skipped (the tomllib-only checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-05 15:59:26 +09:00
GodopuandClaude Opus 5.5 36f0b8dfd6 test(issue-45): the herdr-config tests collect on Python < 3.11; the docs say what that Python loses
tests/test_issue45_herdr_config.py imported tomllib at module level. On
Python 3.9 - macOS's /usr/bin/python3, which the venvs and the bootstrap's
python3 use here - the import failed during collection and pytest ran no
test of the whole suite.

The import is optional now, and the 29 tests that parse a result with
tomllib or expect the user's settings kept are marked needs_tomllib: on
Python < 3.11 herdr_config deliberately writes the key-only config (restore
still off, the other settings dropped, a warning on stderr), which
test_without_tomllib_restore_is_still_off covers on every Python.
Python 3.9: 22 passed / 29 skipped; Python 3.11: 51 passed.

create SKILL.md (same line, so the #42 inventory is unchanged) and the
4.8.5 entry state that building the server config needs tomllib (Python
3.11+) in the python3 on PATH, and what an older python3 loses. Checked
with Python 3.9 in a throwaway HOME: the warning reaches stderr (the
bootstrap captures only stdout) and the user's file is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-05 15:56:27 +09:00
GodopuandClaude Opus 5.5 35935f9e8a fix(issue-45): review round 2 R3 - the .mam fallback obeys the same no-overwrite rules
The fallback wrote <.mam>/herdr-server-config.toml unchecked, so it could
replace a foreign file there, the source itself, or a link to it. It now goes
through output_path like the normal path (source identity incl. aliases,
generated-for-this-source ownership, then a per-source name) and writes
nothing when no name is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:49:08 +00:00
GodopuandClaude Opus 5.5 7c544c06ab test(issue-45): S11 base follows the review-R1 heredoc change; VERSIONS notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:18:09 +00:00
GodopuandClaude Opus 5.5 811b080966 fix(issue-45): review R1-R3 - herdr checks the config, any TOML spelling kept, source never overwritten
R1: tomllib accepting a file is not herdr accepting it (a schema error makes
herdr drop the whole file, restore included). The shim passes REAL_HERDR and
the written file must pass `herdr config check` ("parse error"/"using defaults"
reject; unknown keys and sound-file notices do not), else the key-only file is
written and checked. A non-table `session` and a source that is not UTF-8 reach
that fallback instead of raising.

R2: the line edit is replaced by parse -> set -> serialize; the result must
parse back to the source plus the key, so dotted keys and inline tables keep
the user's other settings.

R3: the output is never the source (aliases included) nor a file not generated
for the same source; then a per-source name beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:17:59 +00:00
GodopuandClaude Opus 5.5 eedcebcc12 fix(issue-45): restore stays off when python3 has no tomllib
The shim runs a bare python3. Before 3.11 the module failed to import and the
server started with restore on; now it gets the key alone, with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:34:27 +00:00