From 3fd2416a303015f2dc1525c9f34096b86f1a0152 Mon Sep 17 00:00:00 2001 From: Godopu Date: Wed, 28 Oct 2026 15:01:50 +0000 Subject: [PATCH] feat(create): warn on --no-onboard for post-discovered-id agents (Issue #68) --- .../skills/multi-agent-mux-create/SKILL.md | 2 +- .../scripts/create_session.sh | 4 + tests/data/skill_code_blocks.json | 2 +- tests/test_issue68_onboard_default.py | 130 ++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index bf26670..d24b9d7 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -170,7 +170,7 @@ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace | `--herdr-session` (`--herdr-server`) | Isolated herdr server name | | `--herdr-workspace` | herdr workspace label recorded in the registry (a label only, never a socket) | | `--submit-job PROMPT` | Register a delegated job with this prompt and send it to the new agent | -| `--onboard` / `--no-onboard` | Submit the onboarding job (default: on) | +| `--onboard` / `--no-onboard` | Submit the onboarding job (default: on); with `--no-onboard` an agent whose id appears only after its first turn (adapter `id_lifecycle`) records none until then and blocks a same-folder create of its type (#63) - create warns | | `--auto-trust` / `--no-auto-trust` | Answer a start-up directory-trust dialog or leave it to a human (`MAM_AUTO_TRUST`; default: answer) | | `--isolate` / `--no-isolate` | Legacy, no-op | diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh index daac476..625bcfe 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh @@ -205,6 +205,10 @@ fi # Retrieve agent facts once eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$AGENT" 2>/dev/null || true)" +# Issue #68: warn when --no-onboard is used with a post-discovered-id agent (adapter id_lifecycle via facts). +if [ "$ONBOARD" = 0 ] && [ "${MAM_ID_POST_DISCOVERED:-0}" = 1 ]; then + echo "WARNING: --no-onboard: $AGENT records its conversation id only after its first turn; until then it is not in the registry, and another $AGENT session created in this folder waits for it (Issue #63, MAM_ID_ORDER_WAIT_S)." >&2 +fi CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "$SESSION_UUID" "${USE_WRAPPER:-0}" 2>/dev/null || true)" if [ -z "$CMD_FULL" ]; then diff --git a/tests/data/skill_code_blocks.json b/tests/data/skill_code_blocks.json index 0d4a81d..7a8acd1 100644 --- a/tests/data/skill_code_blocks.json +++ b/tests/data/skill_code_blocks.json @@ -633,7 +633,7 @@ "other" ], "passthrough": { - "4": "multi-agent-mux-create/scripts/create_session.sh:550" + "4": "multi-agent-mux-create/scripts/create_session.sh:554" } } }, diff --git a/tests/test_issue68_onboard_default.py b/tests/test_issue68_onboard_default.py index 35d1c3d..1d53319 100644 --- a/tests/test_issue68_onboard_default.py +++ b/tests/test_issue68_onboard_default.py @@ -23,6 +23,7 @@ if str(SKILLS) not in sys.path: from lib_py.agents.base import POST_DISCOVERED # noqa: E402 from lib_py.agents.registry import get_adapter # noqa: E402 +from test_issue63_same_cwd_ids import _mock_herdr_agent, _read_rows, _row, _write_rows # noqa: E402 ALL_AGENTS = ("claude", "agy", "hermes", "grok", "opencode", "codex") EXPECTED_POST_DISCOVERED = { @@ -33,6 +34,15 @@ EXPECTED_POST_DISCOVERED = { "claude": 0, "grok": 0, } +UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.I) + + +def _norm_uuid(text: str) -> str: + return UUID_RE.sub("", text) + + +def _warn_lines(stderr: str) -> list[str]: + return [l for l in stderr.splitlines() if l.startswith("WARNING: --no-onboard:")] @pytest.mark.parametrize("agent", ALL_AGENTS) @@ -50,3 +60,123 @@ def test_facts_expose_post_discovered(agent): adapter = get_adapter(agent) assert val == (1 if adapter.id_lifecycle == POST_DISCOVERED else 0) assert val == EXPECTED_POST_DISCOVERED[agent] + + +@pytest.mark.parametrize("agent", ALL_AGENTS) +def test_no_onboard_warning_dry_run(mam_sandbox, mock_herdr, mock_agents, monkeypatch, agent): + monkeypatch.setenv("XAI_API_KEY", "test-key") + ws = mam_sandbox / f"ws-dry-{agent}" + ws.mkdir(parents=True, exist_ok=True) + create_sh = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh" + + res = subprocess.run( + ["bash", str(create_sh), "--dry-run", "--no-onboard", "--workspace", str(ws), + "--agent", agent, "--role", "creator", "--session", f"t68-{agent}"], + capture_output=True, text=True, cwd=str(mam_sandbox), timeout=60, + ) + assert res.returncode == 0, res.stdout + res.stderr + warns = _warn_lines(res.stderr) + assert len(warns) == EXPECTED_POST_DISCOVERED[agent], res.stderr + if warns: + assert "Issue #63" in warns[0] and agent in warns[0] + + # Control: --dry-run with default onboarding emits 0 warning lines for all agents + ctl_res = subprocess.run( + ["bash", str(create_sh), "--dry-run", "--workspace", str(ws), + "--agent", agent, "--role", "creator", "--session", f"t68-ctl-{agent}"], + capture_output=True, text=True, cwd=str(mam_sandbox), timeout=60, + ) + assert ctl_res.returncode == 0, ctl_res.stdout + ctl_res.stderr + assert _warn_lines(ctl_res.stderr) == [], ctl_res.stderr + + +@pytest.mark.parametrize("agent", ALL_AGENTS) +def test_no_onboard_warning_keeps_exit_and_row(mam_sandbox, mock_herdr, mock_agents, monkeypatch, agent): + monkeypatch.setenv("XAI_API_KEY", "test-key") + ws = mam_sandbox / f"ws-real-{agent}" + ws.mkdir(parents=True, exist_ok=True) + session_name = f"t68-{agent}" + create_sh = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh" + lib_sh = mam_sandbox / ".agents" / "skills" / "lib.sh" + ws_slug = subprocess.run( + ["bash", "-c", f'source "{lib_sh}" && derive_workspace_slug "{ws}"'], + capture_output=True, text=True, check=True, cwd=str(mam_sandbox), + ).stdout.strip() + + res = subprocess.run( + ["bash", str(create_sh), "--no-onboard", "--workspace", str(ws), + "--agent", agent, "--role", "creator", "--session", session_name], + capture_output=True, text=True, cwd=str(mam_sandbox), timeout=120, + ) + # 1. Exit code + assert res.returncode == 0, res.stdout + res.stderr + + # 2. Stable row fields (checked BEFORE warning assertion so base b9c828f fails only at step 3) + rows = _read_rows(mam_sandbox) + assert session_name in rows + row = rows[session_name] + pre_assigned = agent in ("claude", "grok") + expected_cmd_full = _norm_uuid( + get_adapter(agent).spawn_spec( + str(mock_agents / agent), + "00000000-0000-0000-0000-000000000000" if pre_assigned else "", + ) + ) + expected_source = "assigned" if pre_assigned else ("pending-discovery" if agent in ("opencode", "codex") else None) + expected_verified = False if agent in ("claude", "grok", "opencode", "codex") else None + expected_vis = "assigned (awaiting first message)" if pre_assigned else "unverified" + expected_cp = { + "workspace_root": os.path.realpath(mam_sandbox), + "agent_sessions_yaml": os.path.realpath(mam_sandbox / ".mam" / "agent-sessions.yaml"), + "mam_jobs_dir": os.path.realpath(mam_sandbox / ".mam" / "jobs"), + "delegate_job_logs_dir": os.path.realpath(mam_sandbox / ".mam" / "delegate_job_logs"), + } + + assert row["name"] == session_name + assert row["status"] == "running" + assert row["role"] == "creator" + assert row["delegate_job_id"] is None + assert row["herdr_server"] == ws_slug + assert row["herdr_workspace"] == ws_slug.removeprefix("mam-") + assert row["pane"]["cmd"] == agent + assert _norm_uuid(row["pane"]["cmd_full"]) == expected_cmd_full + assert os.path.realpath(row["pane"]["cwd"]) == os.path.realpath(ws) + assert row.get("session_id_source") == expected_source + assert row.get("session_id_verified") == expected_verified + assert row["last_visible_status"] == expected_vis + assert row["control_plane"] == expected_cp + + # 3. Warning line count + warns = _warn_lines(res.stderr) + assert len(warns) == EXPECTED_POST_DISCOVERED[agent], res.stderr + if warns: + assert "Issue #63" in warns[0] and agent in warns[0] + + +def test_no_onboard_warning_precedes_a_later_refusal(mam_sandbox, mock_herdr, mock_agents): + ws = mam_sandbox / "c68-ws" + ws.mkdir(parents=True, exist_ok=True) + epoch_a = int(time.time()) - 100 + name_a, name_b = "c68-ws-creator-agy", "c68-ws-reviewer-agy" + create_sh = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh" + + holder = subprocess.Popen(["sleep", "120"]) + try: + _write_rows(mam_sandbox, [_row(name_a, "agy", ws, epoch_a, pid=holder.pid)]) + _mock_herdr_agent(mock_herdr, name_a, ws, holder.pid, epoch_a, pane_id="w1:p_a") + + env0 = dict(os.environ, MAM_ID_ORDER_WAIT_S="0") + res = subprocess.run( + ["bash", str(create_sh), "--workspace", str(ws), "--agent", "agy", + "--role", "reviewer", "--session", name_b, "--no-onboard"], + capture_output=True, text=True, cwd=str(mam_sandbox), env=env0, timeout=120, + ) + assert res.returncode == 1, res.stdout + res.stderr + warns = _warn_lines(res.stderr) + assert len(warns) == 1, res.stderr + assert "Issue #63" in warns[0] + assert name_a in res.stderr + assert res.stderr.index("WARNING: --no-onboard:") < res.stderr.index(name_a), res.stderr + finally: + holder.kill() + holder.wait()