feat(issue-21): resume only at the original cwd; persist the control-plane binding on the already-running path; fail loudly when a worktree binding cannot be computed

- A11: resume refuses (exit 2) a --workspace other than the row's pane.cwd, and
  a pane.cwd that no longer exists (checked first: mam_abs_workspace returns a
  missing path unchanged). Before, claude/grok failed with a misleading "No
  saved session" and class A agents started a new conversation in the wrong
  directory. Rows without pane.cwd are not checked.
- A12 (recommendation 1): create and resume no longer swallow a failed binding
  computation. A worktree (--workspace != control root) is refused with exit 2
  and nothing spawned or recorded; the control root itself warns and records
  the row without control_plane.
- A13 (recommendation 2): resume computes the binding before the herdr-alive
  check, so the already-running path records it too (update_yaml_resumed.sh
  fills a missing field only). Pane forwarding and the whitespace refusal stay
  on the spawn path.

Tests W7-W9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-14 15:24:10 +00:00
1 parent fadd38cf56
commit 2679ddfffd
3 files changed
+138 -15

No files matched your search

+70
View File
@@ -176,3 +176,73 @@ def test_w11_reinstall_replaces_the_old_rule_and_keeps_user_lines(tmp_path):
assert "user-one" in gi.splitlines() and "user-two" in gi.splitlines()
for ledger in list(tgt.rglob("asset_hashes.txt")) + list(tgt.rglob("install_manifest.txt")):
assert ".gitignore" not in ledger.read_text().split(), ledger
# ---------------------------------------------------------------------------
# resume: original cwd only (A11), binding failures (A12), already running (A13)
# ---------------------------------------------------------------------------
def _resume(root, ws, name, agent="claude", env=None):
return _run(root, ("multi-agent-mux-resume", "scripts", "resume_session.sh"),
"--workspace", str(ws), "--agent", agent, "--session", name, env=env)
def test_w7_resume_only_at_the_original_cwd(mam_sandbox, mock_herdr, mock_agents):
root = mam_sandbox
here, there, gone = root / "here", root / "there", root / "gone"
here.mkdir()
there.mkdir()
_mutate(root, f"""d['herdr_sessions'] = [
{{'name': 'x-creator-claude', 'status': 'stopped', 'pane': {{'cwd': {str(here)!r}, 'cmd': 'claude'}},
'claude_session_id_own': '11111111-2222-4333-8444-555555555555', 'session_id_source': 'assigned'}},
{{'name': 'x-creator-agy', 'status': 'stopped', 'pane': {{'cwd': {str(here)!r}, 'cmd': 'agy'}}}},
{{'name': 'y-creator-claude', 'status': 'stopped', 'pane': {{'cwd': {str(gone)!r}, 'cmd': 'claude'}}}}]""")
since = len(_calls(mock_herdr))
res = _resume(root, there, "x-creator-claude")
assert res.returncode == 2 and f"--workspace '{here}'" in res.stderr
res = _resume(root, there, "x-creator-agy", agent="agy")
assert res.returncode == 2
res = _resume(root, gone, "y-creator-claude")
assert res.returncode == 2 and "no longer exists" in res.stderr
assert _spawn_calls(mock_herdr, since) == [], "nothing was spawned in the wrong directory"
def test_w8_binding_failure_refused_for_a_worktree_warned_for_the_control_root(mam_sandbox, mock_herdr, mock_agents):
root = mam_sandbox
Path(_skill(root, "lib_py", "control_plane.py")).write_text("raise SystemExit(1)\n")
wt = root.parent / f"{root.name}-wt8"
wt.mkdir()
since = len(_calls(mock_herdr))
create = ("multi-agent-mux-create", "scripts", "create_session.sh")
res = _run(root, create, "--workspace", str(wt), "--agent", "claude", "--role", "creator", "--no-onboard")
assert res.returncode == 2 and "cannot compute the control-plane binding" in res.stderr
assert _rows(root) == [] and _spawn_calls(mock_herdr, since) == []
res = _run(root, create, "--workspace", str(root), "--agent", "claude", "--role", "creator", "--no-onboard")
assert res.returncode == 0 and "WARNING: control-plane binding unavailable" in res.stderr, res.stderr
(row,) = _rows(root)
assert "control_plane" not in row
_mutate(root, f"""d['herdr_sessions'].append({{'name': 'w8-creator-claude', 'status': 'stopped',
'pane': {{'cwd': {str(wt)!r}, 'cmd': 'claude'}},
'claude_session_id_own': '11111111-2222-4333-8444-555555555555', 'session_id_source': 'assigned'}})""")
res = _resume(root, wt, "w8-creator-claude")
assert res.returncode == 2 and "cannot compute the control-plane binding" in res.stderr
def test_w9_already_running_resume_persists_the_binding(mam_sandbox, mock_herdr, mock_agents):
root = mam_sandbox
create = ("multi-agent-mux-create", "scripts", "create_session.sh")
res = _run(root, create, "--workspace", str(root), "--agent", "claude", "--role", "creator", "--no-onboard")
assert res.returncode == 0, res.stdout + res.stderr
name = _rows(root)[0]["name"]
_mutate(root, "d['herdr_sessions'][0].pop('control_plane', None)")
res = _resume(root, root, name)
assert res.returncode == 0 and "already running" in res.stdout, res.stdout + res.stderr
row = _rows(root)[0]
assert set(row["control_plane"]) == set(CP_KEYS)
assert row["control_plane"]["workspace_root"] == os.path.realpath(root)
custom = {**row["control_plane"], "mam_jobs_dir": str(root / "custom-jobs")}
_mutate(root, f"d['herdr_sessions'][0]['control_plane'] = {custom!r}")
assert _resume(root, root, name).returncode == 0
assert _rows(root)[0]["control_plane"] == custom, "an existing row binding is kept"