fix: DPLAN-0241 round 4 — R6 extra_args threaded through ALL launch paths (user flags survive resume/takeover/continue/dead-window/headless), R7 auto-namer stamps --name branch-shortid on every launch (flag live-verified 2.1.209, user -n/--name wins), new-over-all aborts on failed daemon stop, honest close-all hint, exit/q/quit in all menus. op:kill per-job stop found in daemon socket protocol — documented, not shipped (undocumented internal). 1048 hooks tests green (102 session_boot, 11 CLI contract).

This commit is contained in:
AIOSAI
2026-07-14 02:10:44 -07:00
parent 0b739ac525
commit 116c4e9d69
4 changed files with 346 additions and 13 deletions
+22
View File
@@ -9,6 +9,28 @@ PyPI version — not the changelog header.
---
## [2026-07-14]
### Fixed
- **DPLAN-0241 round 4 (night shift): user flags survive every launch path, and
every session is born with an honest name.** R6 — the bug behind Patrick's
approve-everything chat: the boot menu suppressed its bypass defaults when the
user passed `--permission-mode` himself, but only the fresh-launch path threaded
the user's flags into the exec — resume, takeover, continue, and dead-window
paths all launched flagless. `extra_args` now threads through ALL launch paths
(headless `-p` included). R7 — auto-namer: every launch is stamped
`--name <branch>-<short-session-id>` (flag live-verified on claude 2.1.209; a
user-passed `-n/--name` wins), so made-up auto-names can no longer hide which
chat is which. Plus four drill nits: new-over-all ABORTS if the daemon stop
fails (one brain even in failure paths), close-all's failure hint no longer
recommends the mechanism that just failed, `exit`/`q`/`quit` quietly leave every
menu, session rows stay rich (PID, kind, name, age). Surgical-stop probe:
`op:kill` exists in the daemon's Unix-socket control protocol (per-job bg stop,
8-char sessionId prefix, no auth) — documented in DPLAN-0241, deliberately NOT
shipped: undocumented internal protocol. 1048 hooks tests green (102
session_boot, 11 real-binary CLI contract).
## [2026-07-13]
### Fixed
@@ -225,7 +225,9 @@ def _stop_session(session: dict, claude_bin: str) -> str:
return f"PID {pid}: no action"
def _resume_session(session: dict, branch: str, claude_bin: str, defaults: list[str]) -> dict:
def _resume_session(
session: dict, branch: str, claude_bin: str, defaults: list[str], extra_args: list[str] | None = None
) -> dict:
"""Resume a session — right mechanism per kind.
bg: takeover (daemon stop + --resume in tmux). Never opens agents view.
@@ -234,9 +236,10 @@ def _resume_session(session: dict, branch: str, claude_bin: str, defaults: list[
"""
pid = session.get("pid")
kind = session.get("kind", "unknown")
ea = list(extra_args or [])
if kind in ("bg", "background"):
return _takeover_bg(session, branch, claude_bin, defaults)
return _takeover_bg(session, branch, claude_bin, defaults, extra_args)
tmux_session = _find_tmux_session_for_pid(pid) if pid else None
if tmux_session:
@@ -245,7 +248,9 @@ def _resume_session(session: dict, branch: str, claude_bin: str, defaults: list[
return {"exit_code": 0, "action": "attached", "tmux_session": tmux_session}
logger.info("[SESSION_BOOT] Continuing dead-window session via --continue")
return _exec_in_tmux(branch, "", claude_bin, [claude_bin] + defaults + ["--continue"])
sid = session.get("sessionId", "")
nf = _name_flag(branch, sid, extra_args)
return _exec_in_tmux(branch, "", claude_bin, [claude_bin] + defaults + ["--continue"] + ea + nf)
def _make_session_name(branch: str, session_id: str = "") -> str:
@@ -256,6 +261,13 @@ def _make_session_name(branch: str, session_id: str = "") -> str:
return branch
def _name_flag(branch: str, session_id: str = "", extra_args: list[str] | None = None) -> list[str]:
"""Build --name args for session stamping, unless user already provided one."""
if extra_args and ("-n" in extra_args or "--name" in extra_args):
return []
return ["--name", _make_session_name(branch, session_id)]
def _exec_in_tmux(branch: str, session_id: str, claude_bin: str, claude_cmd: list[str]) -> dict:
"""Exec a claude command inside a new tmux session."""
session_name = _make_session_name(branch, session_id)
@@ -291,7 +303,7 @@ def boot(cwd: str | None = None, extra_args: list[str] | None = None) -> dict:
if os.environ.get("TMUX"):
logger.info("[SESSION_BOOT] Already inside tmux — running claude directly")
claude_cmd = [claude_bin] + defaults
claude_cmd = [claude_bin] + defaults + _name_flag(branch, extra_args=extra_args)
if extra_args:
claude_cmd.extend(extra_args)
os.execvp(claude_bin, claude_cmd)
@@ -375,7 +387,9 @@ def _daemon_stop(claude_bin: str, branch: str, pid: int | None) -> dict:
return {"ok": True}
def _takeover_bg(session: dict, branch: str, claude_bin: str, defaults: list[str]) -> dict:
def _takeover_bg(
session: dict, branch: str, claude_bin: str, defaults: list[str], extra_args: list[str] | None = None
) -> dict:
"""Take over a bg session: daemon stop --any, poll, then --resume in tmux.
Checks blast radius first (other branches' bg sessions). On daemon stop
@@ -384,6 +398,8 @@ def _takeover_bg(session: dict, branch: str, claude_bin: str, defaults: list[str
"""
session_id = session.get("sessionId", "")
pid = session.get("pid")
ea = list(extra_args or [])
nf = _name_flag(branch, session_id, extra_args)
stop_result = _daemon_stop(claude_bin, branch, pid)
if not stop_result["ok"]:
@@ -391,10 +407,12 @@ def _takeover_bg(session: dict, branch: str, claude_bin: str, defaults: list[str
if session_id:
logger.info("[SESSION_BOOT] Resuming session %s after takeover", session_id[:8])
return _exec_in_tmux(branch, session_id, claude_bin, [claude_bin] + defaults + ["--resume", session_id])
return _exec_in_tmux(
branch, session_id, claude_bin, [claude_bin] + defaults + ["--resume", session_id] + ea + nf
)
logger.info("[SESSION_BOOT] No sessionId for takeover — continuing last")
return _exec_in_tmux(branch, "", claude_bin, [claude_bin] + defaults + ["--continue"])
return _exec_in_tmux(branch, "", claude_bin, [claude_bin] + defaults + ["--continue"] + ea + nf)
def _is_session_file_present(pid: int | None) -> bool:
@@ -430,7 +448,7 @@ def _menu_live(
choice = _read_choice()
if choice in ("", "r"):
return _resume_session(session, branch, claude_bin, defaults)
return _resume_session(session, branch, claude_bin, defaults, extra_args)
elif choice == "n":
if is_bg:
stop = _daemon_stop(claude_bin, branch, session.get("pid"))
@@ -449,6 +467,8 @@ def _menu_live(
result = _stop_session(session, claude_bin)
sys.stderr.write(f" {result}\n")
return {"exit_code": 0, "action": "closed"}
elif choice in ("exit", "q", "quit"):
return {"exit_code": 0, "action": "quit"}
else:
sys.stderr.write(" Unknown choice. Exiting.\n")
return {"exit_code": 1, "error": "unknown choice"}
@@ -468,10 +488,13 @@ def _menu_live(
if choice == "n":
return _new_over_all(live, branch, claude_bin, defaults, extra_args)
if choice in ("exit", "q", "quit"):
return {"exit_code": 0, "action": "quit"}
try:
idx = int(choice) - 1
if 0 <= idx < len(live):
return _resume_session(live[idx], branch, claude_bin, defaults)
return _resume_session(live[idx], branch, claude_bin, defaults, extra_args)
except (ValueError, IndexError):
logger.info("[SESSION_BOOT] Invalid menu choice: %r", choice)
@@ -492,7 +515,7 @@ def _close_all(live: list[dict], branch: str, claude_bin: str) -> dict:
sys.stderr.write(f" Stopped {len(bg)} bg session(s) via daemon stop.\n")
else:
for s in bg:
sys.stderr.write(f" PID {s.get('pid')}: bg session remains (use Enter to take over)\n")
sys.stderr.write(f" PID {s.get('pid')}: bg session remains — daemon stop failed\n")
return {"exit_code": 0, "action": "closed_all"}
@@ -512,7 +535,8 @@ def _new_over_all(
if bg:
stop = _daemon_stop(claude_bin, branch, bg[0].get("pid"))
if not stop["ok"]:
sys.stderr.write(f" {len(bg)} bg session(s) remain — starting new chat anyway\n")
sys.stderr.write(" Cannot start new — bg session(s) still running.\n")
return {"exit_code": 1, "error": "daemon stop failed, aborting to preserve one-brain"}
return _start_fresh(branch, claude_bin, defaults, extra_args)
@@ -531,9 +555,13 @@ def _menu_no_live(
if choice in ("", "r"):
logger.info("[SESSION_BOOT] Continuing last chat via --continue")
return _exec_in_tmux(branch, "", claude_bin, [claude_bin] + defaults + ["--continue"])
nf = _name_flag(branch, extra_args=extra_args)
cmd = [claude_bin] + defaults + ["--continue"] + list(extra_args or []) + nf
return _exec_in_tmux(branch, "", claude_bin, cmd)
elif choice == "n":
return _start_fresh(branch, claude_bin, defaults, extra_args)
elif choice in ("exit", "q", "quit"):
return {"exit_code": 0, "action": "quit"}
else:
sys.stderr.write(" Unknown choice. Exiting.\n")
return {"exit_code": 1, "error": "unknown choice"}
@@ -552,7 +580,7 @@ def _start_fresh(
logger.info("[SESSION_BOOT] Killing stale tmux session '%s'", session_name)
subprocess.run(["tmux", "kill-session", "-t", session_name], check=False)
claude_cmd = [claude_bin] + defaults
claude_cmd = [claude_bin] + defaults + _name_flag(branch, extra_args=extra_args)
if extra_args:
claude_cmd.extend(extra_args)
@@ -54,6 +54,10 @@ class TestClaudeMainFlags:
h = _get_main_help()
assert "-p" in h or "--print" in h
def test_name_flag(self):
h = _get_main_help()
assert "--name" in h or "-n" in h
@_SKIP
class TestClaudeAgentsFlags:
+279
View File
@@ -118,6 +118,28 @@ class TestMakeSessionName:
assert session_boot._make_session_name("hooks", "") == "hooks"
class TestNameFlag:
def test_branch_only(self):
result = session_boot._name_flag("hooks")
assert result == ["--name", "hooks"]
def test_branch_with_session_id(self):
result = session_boot._name_flag("hooks", "abcdef1234")
assert result == ["--name", "hooks-abcdef12"]
def test_skips_when_user_provides_name(self):
result = session_boot._name_flag("hooks", "", ["--name", "myname"])
assert result == []
def test_skips_when_user_provides_n(self):
result = session_boot._name_flag("hooks", "", ["-n", "myname"])
assert result == []
def test_adds_when_extra_args_no_name(self):
result = session_boot._name_flag("hooks", "", ["--verbose"])
assert result == ["--name", "hooks"]
class TestSessionLabel:
def test_formats_label(self):
session = {"pid": 1234, "sessionId": "abcdef1234", "kind": "interactive"}
@@ -820,3 +842,260 @@ class TestIsSessionFilePresent:
def test_none_pid(self):
assert session_boot._is_session_file_present(None) is False
class TestExtraArgsThreading:
"""R6: extra_args must reach every launch path, not just _start_fresh."""
def test_resume_dead_window_threads_extra_args(self, tmp_path):
session = {"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "interactive"}
with (
patch.object(session_boot, "_find_tmux_session_for_pid", return_value=None),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot._resume_session(session, "hooks", "/usr/local/bin/claude", [], ["--permission-mode", "plan"])
cmd = mock_exec.call_args[0][1]
assert "--continue" in cmd
assert "--permission-mode" in cmd
assert "plan" in cmd
def test_takeover_bg_threads_extra_args(self, tmp_path):
session = {"pid": 1234, "sessionId": "abc-uuid", "cwd": str(tmp_path), "kind": "bg"}
with (
patch.object(session_boot, "_daemon_stop", return_value={"ok": True}),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot._takeover_bg(session, "hooks", "/usr/local/bin/claude", [], ["--permission-mode", "plan"])
cmd = mock_exec.call_args[0][1]
assert "--resume" in cmd
assert "--permission-mode" in cmd
assert "plan" in cmd
def test_takeover_bg_continue_threads_extra_args(self, tmp_path):
session = {"pid": 1234, "sessionId": "", "cwd": str(tmp_path), "kind": "bg"}
with (
patch.object(session_boot, "_daemon_stop", return_value={"ok": True}),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot._takeover_bg(session, "hooks", "/usr/local/bin/claude", [], ["--permission-mode", "plan"])
cmd = mock_exec.call_args[0][1]
assert "--continue" in cmd
assert "--permission-mode" in cmd
def test_no_live_continue_threads_extra_args(self, tmp_path):
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=[]),
patch.object(session_boot, "_read_choice", return_value=""),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path), extra_args=["--permission-mode", "plan"])
cmd = mock_exec.call_args[0][1]
assert "--continue" in cmd
assert "--permission-mode" in cmd
assert "plan" in cmd
def test_live_bg_enter_threads_extra_args(self, tmp_path):
live = [{"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "bg"}]
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=live),
patch.object(session_boot, "_read_choice", return_value=""),
patch.object(session_boot, "_daemon_stop", return_value={"ok": True}),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path), extra_args=["--permission-mode", "plan"])
cmd = mock_exec.call_args[0][1]
assert "--resume" in cmd
assert "--permission-mode" in cmd
assert "plan" in cmd
def test_multi_session_pick_threads_extra_args(self, tmp_path):
live = [
{"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "interactive"},
{"pid": 5678, "sessionId": "def", "cwd": str(tmp_path), "kind": "interactive"},
]
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=live),
patch.object(session_boot, "_read_choice", return_value="1"),
patch.object(session_boot, "_find_tmux_session_for_pid", return_value=None),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path), extra_args=["--permission-mode", "plan"])
cmd = mock_exec.call_args[0][1]
assert "--continue" in cmd
assert "--permission-mode" in cmd
assert "plan" in cmd
class TestNewOverAllAbort:
def test_aborts_on_daemon_stop_failure(self, tmp_path):
live = [
{"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "interactive"},
{"pid": 5678, "sessionId": "def", "cwd": str(tmp_path), "kind": "bg"},
]
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=live),
patch.object(session_boot, "_read_choice", return_value="n"),
patch.object(session_boot, "_stop_session", return_value="stopped"),
patch.object(session_boot, "_daemon_stop", return_value={"ok": False, "error": "failed"}),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
result = session_boot.boot(cwd=str(tmp_path))
assert result["exit_code"] == 1
assert "one-brain" in result["error"]
mock_exec.assert_not_called()
class TestMenuQuit:
def test_single_session_q_quits(self, tmp_path):
live = [{"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "interactive"}]
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=live),
patch.object(session_boot, "_read_choice", return_value="q"),
):
result = session_boot.boot(cwd=str(tmp_path))
assert result["exit_code"] == 0
assert result["action"] == "quit"
def test_single_session_exit_quits(self, tmp_path):
live = [{"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "interactive"}]
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=live),
patch.object(session_boot, "_read_choice", return_value="exit"),
):
result = session_boot.boot(cwd=str(tmp_path))
assert result["action"] == "quit"
def test_multi_session_quit_quits(self, tmp_path):
live = [
{"pid": 1234, "sessionId": "abc", "cwd": str(tmp_path), "kind": "interactive"},
{"pid": 5678, "sessionId": "def", "cwd": str(tmp_path), "kind": "interactive"},
]
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=live),
patch.object(session_boot, "_read_choice", return_value="quit"),
):
result = session_boot.boot(cwd=str(tmp_path))
assert result["action"] == "quit"
def test_no_live_q_quits(self, tmp_path):
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=[]),
patch.object(session_boot, "_read_choice", return_value="q"),
):
result = session_boot.boot(cwd=str(tmp_path))
assert result["action"] == "quit"
class TestAutoNamer:
"""R7: --name flag stamped on every launch for self-identifying sessions."""
def test_fresh_start_gets_name(self, tmp_path):
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=[]),
patch.object(session_boot, "_read_choice", return_value="n"),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path))
cmd = mock_exec.call_args[0][1]
assert "--name" in cmd
branch = tmp_path.name
idx = cmd.index("--name")
assert cmd[idx + 1] == branch
def test_takeover_gets_name_with_session_id(self, tmp_path):
session = {"pid": 1234, "sessionId": "abc12345-full-uuid", "cwd": str(tmp_path), "kind": "bg"}
with (
patch.object(session_boot, "_daemon_stop", return_value={"ok": True}),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot._takeover_bg(session, "hooks", "/usr/local/bin/claude", [])
cmd = mock_exec.call_args[0][1]
assert "--name" in cmd
idx = cmd.index("--name")
assert cmd[idx + 1] == "hooks-abc12345"
def test_in_tmux_gets_name(self, tmp_path):
with (
patch.dict("os.environ", {"TMUX": "/tmp/tmux-1000/default,123,0"}),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path))
cmd = mock_exec.call_args[0][1]
assert "--name" in cmd
def test_no_live_continue_gets_name(self, tmp_path):
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=[]),
patch.object(session_boot, "_read_choice", return_value=""),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path))
cmd = mock_exec.call_args[0][1]
assert "--name" in cmd
def test_user_name_not_overridden(self, tmp_path):
with (
patch.dict("os.environ", {}, clear=True),
patch.object(session_boot, "_resolve_claude_binary", return_value="/usr/local/bin/claude"),
patch.object(session_boot, "_find_tmux", return_value="/usr/bin/tmux"),
patch.object(session_boot, "_find_live_sessions", return_value=[]),
patch.object(session_boot, "_read_choice", return_value="n"),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot.boot(cwd=str(tmp_path), extra_args=["--name", "myname"])
cmd = mock_exec.call_args[0][1]
assert cmd.count("--name") == 1
idx = cmd.index("--name")
assert cmd[idx + 1] == "myname"
def test_dead_window_resume_gets_name_with_session_id(self, tmp_path):
session = {"pid": 1234, "sessionId": "abc12345-uuid", "cwd": str(tmp_path), "kind": "interactive"}
with (
patch.object(session_boot, "_find_tmux_session_for_pid", return_value=None),
patch.object(session_boot, "_tmux_session_exists", return_value=False),
patch(f"{_MOD}.os.execvp") as mock_exec,
):
session_boot._resume_session(session, "hooks", "/usr/local/bin/claude", [])
cmd = mock_exec.call_args[0][1]
assert "--name" in cmd
idx = cmd.index("--name")
assert cmd[idx + 1] == "hooks-abc12345"