Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .console/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
29 changes: 24 additions & 5 deletions src/operator_console/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)


Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/operator_console/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
4 changes: 3 additions & 1 deletion tests/test_anchor_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading