diff --git a/.console/log.md b/.console/log.md index df050bc..5314fd4 100644 --- a/.console/log.md +++ b/.console/log.md @@ -384,3 +384,9 @@ Created profile yamls for each with lazygit git pane and standard helpers. ## 2026-05-24 — OC panes anchor all 3 CLIs via cl session start (Phase 3) - bootstrap.get_{claude,codex,aider}_command now prepend a shared _CL_ANCHOR_PRELUDE (`eval "$(cl session start 2>/dev/null || true)"`) so every Console-launched CLI anchors at its repo OWNING MANIFEST (RepoGraph-resolved), not the bare cwd. Corrects the earlier hardcoded CL_ANCHOR=cwd. Repos not hooked to a manifest resolve to nothing → skipped. Updated tests/test_anchor_launch.py (asserts prelude across all 3 CLIs). 135 tests pass. + +## 2026-05-27 — Fix: re-anchor claude in pane shells after session exit + +`bootstrap.py`: `_CL_ANCHOR_PRELUDE` now resolves `cl` via `CL_HOME` (works in non-login shells); `get_claude_command` writes a shared `console-rc-{key}.sh` that defines `claude()` with auto-anchor, used by the post-claude shell and the shell pane. +`launcher.py`: shell pane (`while true`) uses `bash --rcfile /tmp/console-rc-{key}.sh` so typing `claude` from that pane re-anchors automatically. +`tests/test_anchor_launch.py`: updated assertion to match new prelude shape (`session start` + `_CL_BIN`). diff --git a/src/operator_console/bootstrap.py b/src/operator_console/bootstrap.py index f8f3503..f16c9da 100644 --- a/src/operator_console/bootstrap.py +++ b/src/operator_console/bootstrap.py @@ -111,12 +111,14 @@ def write_bootstrap_file( # Anchor every Console-launched CLI session at its repo's *owning manifest* via # ContextLifecycle. `cl session start` (no arg) resolves cwd→manifest through # RepoGraph and emits eval-able CL_ANCHOR/CL_SESSION_ID exports. Repos not hooked -# to a manifest resolve to nothing and are skipped (no CL) — `|| true` keeps the -# CLI launching regardless. Claude's guard hooks then read CL_ANCHOR; codex/aider -# use it for session-boundary cognition. +# to a manifest resolve to nothing and are skipped (no CL) — the CLI launches +# unanchored, cl_wrap stays a no-op. Uses CL_HOME-relative path so the prelude +# works even in non-login shells where ~/.bashrc hasn't been sourced. _CL_ANCHOR_PRELUDE = ( "# ContextLifecycle: anchor at this repo's owning manifest (skips if unhooked).\n" - 'eval "$(cl session start 2>/dev/null || true)"\n' + '_CL_BIN="${CL_HOME:+$CL_HOME/bin/cl}"\n' + '_CL_BIN="${_CL_BIN:-$(command -v cl 2>/dev/null || true)}"\n' + '[ -n "$_CL_BIN" ] && [ -x "$_CL_BIN" ] && eval "$($_CL_BIN session start 2>/dev/null || true)"\n' ) @@ -151,6 +153,22 @@ def get_claude_command( sf = str(session_file).replace("'", "'\\''") pd = str(project_dir).replace("'", "'\\''") + # RC file: sourced by the post-claude shell and by the shell pane so that + # typing `claude` in either context re-anchors automatically. + rc_path = Path(tempfile.gettempdir()) / f"console-rc-{key}.sh" + rc_path.write_text( + "[ -f ~/.bashrc ] && source ~/.bashrc\n" + "claude() {\n" + " local _cl=\"${CL_HOME:+$CL_HOME/bin/cl}\"\n" + " _cl=\"${_cl:-$(command -v cl 2>/dev/null)}\"\n" + " [ -n \"$_cl\" ] && [ -x \"$_cl\" ] && eval \"$($_cl session start 2>/dev/null || true)\"\n" + " command claude \"$@\"\n" + "}\n", + encoding="utf-8", + ) + rc_path.chmod(0o755) + src = str(rc_path).replace("'", "'\\''") + script = ( "#!/usr/bin/env bash\n" + _CL_ANCHOR_PRELUDE @@ -167,7 +185,8 @@ def get_claude_command( " claude\n" "fi\n" "_save_session\n" - "exec bash -l\n" + # Drop to an interactive shell where `claude` re-anchors automatically. + f"exec bash --rcfile '{src}' -i\n" ) script_path = Path(tempfile.gettempdir()) / f"console-claude-{key}.sh" diff --git a/src/operator_console/launcher.py b/src/operator_console/launcher.py index f693fb9..2d71802 100644 --- a/src/operator_console/launcher.py +++ b/src/operator_console/launcher.py @@ -87,7 +87,7 @@ def _single_pane_block( f'{i} pane size="28%" {{\n' f'{i} pane stacked=true {{\n' f'{i} pane name="shell" command="bash" {{\n' - f'{i} args "-c" "export CONSOLE_PROFILE=\'{profile_name}\' && cd \'{safe_repo}\' && while true; do bash -l; sleep 1; done"\n' + f'{i} args "-c" "export CONSOLE_PROFILE=\'{profile_name}\' && cd \'{safe_repo}\' && while true; do bash --rcfile \'/tmp/console-rc-{profile_name.lower()}.sh\' -i 2>/dev/null || bash -l; sleep 1; done"\n' f'{i} }}\n' f'{i} pane name="status" command="bash" {{\n' f'{i} args "-c" "{status_cmd}"\n' @@ -171,7 +171,7 @@ def _multi_pane_block( f'{i} pane size="28%" {{\n' f'{i} pane stacked=true {{\n' f'{i} pane name="shell" command="bash" {{\n' - f'{i} args "-c" "export CONSOLE_PROFILE=\'{session_key}\' && cd \'{safe_cwd}\' && while true; do bash -l; sleep 1; done"\n' + f'{i} args "-c" "export CONSOLE_PROFILE=\'{session_key}\' && cd \'{safe_cwd}\' && while true; do bash --rcfile \'/tmp/console-rc-{session_key.lower()}.sh\' -i 2>/dev/null || bash -l; sleep 1; done"\n' f'{i} }}\n' f'{i} pane name="status" command="bash" {{\n' f'{i} args "-c" "{status_cmd}"\n' diff --git a/tests/test_anchor_launch.py b/tests/test_anchor_launch.py index da93b82..6677c1b 100644 --- a/tests/test_anchor_launch.py +++ b/tests/test_anchor_launch.py @@ -33,7 +33,9 @@ def test_all_three_cli_wrappers_anchor_via_cl_session_start(tmp_path): for builder in (get_claude_command, get_codex_command, get_aider_command): cmd = builder(profile, tmp_path / "repo", console_dir=console_dir, session_key="platform") script = _script_of(cmd) - assert "cl session start" in script, f"{builder.__name__} missing anchor prelude" + # Prelude resolves cl via CL_HOME, then calls `cl session start`. + assert "session start" in script, f"{builder.__name__} missing anchor prelude" + assert "_CL_BIN" in script # No hardcoded cwd-as-anchor (the old, wrong behavior). assert "export CL_ANCHOR='" not in script