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
4 changes: 4 additions & 0 deletions .console/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,7 @@ Created profile yamls for each with lazygit git pane and standard helpers.
## 2026-05-24 — Fix stale cxrp test fixtures (0.2 → 0.3)

- tests/test_cxrp_capture.py hardcoded schema_version "0.2" but cxrp is at 0.3 (envelope schema const "0.3"). Bumped the 4 envelope schema_version fixtures/assertions to "0.3"; left the separate nested $payload_schema coding_agent_target/v0.2 ref (consistent with code). Full OC suite green.

## 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.
26 changes: 18 additions & 8 deletions src/operator_console/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ def write_bootstrap_file(
return out


# 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.
_CL_ANCHOR_PRELUDE = (
"# ContextLifecycle: anchor at this repo's owning manifest (skips if unhooked).\n"
'eval "$(cl session start 2>/dev/null || true)"\n'
)


def get_claude_command(
profile: dict,
repo_root: Path,
Expand Down Expand Up @@ -138,15 +150,11 @@ def get_claude_command(

sf = str(session_file).replace("'", "'\\''")
pd = str(project_dir).replace("'", "'\\''")
# Anchor the session at its cwd (a manifest for group tabs, the repo for
# single-repo tabs). ContextLifecycle's guard hooks hard-require CL_ANCHOR
# and refuse to fall back to CWD, so it must be exported before launch.
ca = str(cwd.resolve()).replace("'", "'\\''")

script = (
"#!/usr/bin/env bash\n"
f"export CL_ANCHOR='{ca}'\n"
f"SESSION_FILE='{sf}'\n"
+ _CL_ANCHOR_PRELUDE
+ f"SESSION_FILE='{sf}'\n"
f"PROJECT_DIR='{pd}'\n"
"_save_session() {\n"
" newest=$(ls -t \"$PROJECT_DIR\"/*.jsonl 2>/dev/null | head -1)\n"
Expand Down Expand Up @@ -201,7 +209,8 @@ def get_codex_command(

not_found_block = (
"#!/usr/bin/env bash\n"
f"if ! command -v '{safe_bin}' &>/dev/null; then\n"
+ _CL_ANCHOR_PRELUDE
+ f"if ! command -v '{safe_bin}' &>/dev/null; then\n"
" echo 'codex CLI not found.'\n"
" echo 'Install: npm install -g @openai/codex'\n"
" exec bash -l\n"
Expand Down Expand Up @@ -282,7 +291,8 @@ def get_aider_command(

not_found_block = (
"#!/usr/bin/env bash\n"
f"if ! command -v '{safe_bin}' &>/dev/null; then\n"
+ _CL_ANCHOR_PRELUDE
+ f"if ! command -v '{safe_bin}' &>/dev/null; then\n"
" echo 'aider not found.'\n"
" echo 'Install: pip install aider-chat'\n"
" exec bash -l\n"
Expand Down
38 changes: 21 additions & 17 deletions tests/test_anchor_launch.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# SPDX-License-Identifier: Proprietary
# Copyright (C) 2026 ProtocolWarden
"""Launch anchoring: OC-launched Claude sessions must export CL_ANCHOR, and the
cross-repo group tab must anchor at PlatformManifest (not the bare workspace root)."""
"""Launch anchoring: every Console-launched CLI (claude/codex/aider) anchors at
its repo's owning manifest via `cl session start` (no hardcoded CL_ANCHOR=cwd),
and the cross-repo group tab cds into PlatformManifest (not the workspace root)."""

from __future__ import annotations

from pathlib import Path

from operator_console.bootstrap import get_claude_command
from operator_console.bootstrap import (
get_aider_command,
get_claude_command,
get_codex_command,
)
from operator_console.launcher import _multi_pane_block


Expand All @@ -17,20 +22,20 @@ def _console_dir(tmp_path: Path) -> Path:
return cd


def test_claude_wrapper_exports_cl_anchor_equal_to_cwd(tmp_path):
def _script_of(cmd: str) -> str:
# wrapper commands look like: bash '<script-path>'
return Path(cmd.split("'")[1]).read_text(encoding="utf-8")


def test_all_three_cli_wrappers_anchor_via_cl_session_start(tmp_path):
console_dir = _console_dir(tmp_path)
anchor = tmp_path / "PlatformManifest"
cmd = get_claude_command(
{"name": "platform", "repo_root": str(tmp_path / "repo")},
tmp_path / "repo",
console_dir=console_dir,
session_key="platform",
claude_cwd=anchor,
)
# cmd == "bash '<script>'" — read the generated wrapper
script_path = cmd.split("'")[1]
script = Path(script_path).read_text(encoding="utf-8")
assert f"export CL_ANCHOR='{anchor.resolve()}'" in script
profile = {"name": "platform", "repo_root": str(tmp_path / "repo")}
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"
# No hardcoded cwd-as-anchor (the old, wrong behavior).
assert "export CL_ANCHOR='" not in script


def test_group_tab_anchors_at_platform_manifest(tmp_path):
Expand All @@ -40,6 +45,5 @@ def test_group_tab_anchors_at_platform_manifest(tmp_path):
{"name": "b", "repo_root": str(tmp_path / "B")},
]
block = _multi_pane_block(profiles, console_dir=console_dir, tab_name="platform")
# Panes cd into PlatformManifest, not the bare ~/Documents/GitHub root.
assert "PlatformManifest" in block
assert "cd '" in block and "/PlatformManifest'" in block
Loading