Skip to content

Commit 201aaa1

Browse files
feat: platform tab anchors at PlatformManifest + export CL_ANCHOR
Group/cross-repo tabs now cd into PlatformManifest instead of the bare ~/Documents/GitHub root, and the claude wrapper exports CL_ANCHOR=<cwd> so OC-launched sessions satisfy ContextLifecycle's guard hooks (which now hard-require CL_ANCHOR with no CWD fallback). Adds anchor-launch tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ad84fad commit 201aaa1

4 files changed

Lines changed: 65 additions & 4 deletions

File tree

.console/log.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,9 @@ Created profile yamls for each with lazygit git pane and standard helpers.
339339
## 2026-05-23 — Genericize fleet-repo ref + standardize hook
340340

341341
- Genericized SyncingSolution ref in .custodian/config.yaml comment (public repo; private fleet layer must not be named). Standardized .hooks/pre-push.
342+
343+
## 2026-05-24 — Platform tab anchors at PlatformManifest + export CL_ANCHOR
344+
345+
- launcher._multi_pane_block: cross-repo group tab cwd → PlatformManifest (was bare ~/Documents/GitHub). git-watcher still spans all group repos.
346+
- bootstrap.get_claude_command: wrapper now exports CL_ANCHOR=<cwd> so OC-launched sessions satisfy the CL guard hooks (which now hard-require CL_ANCHOR, no CWD fallback). Single-repo tabs anchor at their repo; group tab at PlatformManifest.
347+
- Added tests/test_anchor_launch.py (2 tests). NOTE: pre-existing test_watcher_pane.py failures are unrelated (confirmed on clean main).

src/operator_console/bootstrap.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,14 @@ def get_claude_command(
138138

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

142146
script = (
143147
"#!/usr/bin/env bash\n"
148+
f"export CL_ANCHOR='{ca}'\n"
144149
f"SESSION_FILE='{sf}'\n"
145150
f"PROJECT_DIR='{pd}'\n"
146151
"_save_session() {\n"

src/operator_console/launcher.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,25 @@ def _multi_pane_block(
110110
indent: str = " ",
111111
tab_name: str | None = None,
112112
) -> str:
113-
safe_cwd = str(_GITHUB_DIR).replace("'", "'\\''")
113+
# Cross-repo group tabs anchor at PlatformManifest (the ecosystem manifest /
114+
# cognition host) rather than the bare workspace root — so the session has a
115+
# valid CL_ANCHOR (see bootstrap.get_claude_command) instead of an
116+
# un-anchored ~/Documents/GitHub. The git-watcher still spans all group repos.
117+
group_cwd = _GITHUB_DIR / "PlatformManifest"
118+
safe_cwd = str(group_cwd).replace("'", "'\\''")
114119
session_key = tab_name or _multi_tab_name(profiles)
115120
i = indent
116121

117122
claude_cmd = get_claude_command(
118123
profiles[0], Path(profiles[0]["repo_root"]),
119-
console_dir=console_dir, session_key=session_key, claude_cwd=_GITHUB_DIR,
124+
console_dir=console_dir, session_key=session_key, claude_cwd=group_cwd,
120125
)
121126
codex_cmd = get_codex_command(
122-
profiles[0], _GITHUB_DIR,
127+
profiles[0], group_cwd,
123128
console_dir=console_dir, session_key=session_key,
124129
)
125130
aider_cmd = get_aider_command(
126-
profiles[0], _GITHUB_DIR,
131+
profiles[0], group_cwd,
127132
console_dir=console_dir, session_key=session_key,
128133
)
129134

tests/test_anchor_launch.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# SPDX-License-Identifier: Proprietary
2+
# Copyright (C) 2026 ProtocolWarden
3+
"""Launch anchoring: OC-launched Claude sessions must export CL_ANCHOR, and the
4+
cross-repo group tab must anchor at PlatformManifest (not the bare workspace root)."""
5+
6+
from __future__ import annotations
7+
8+
from pathlib import Path
9+
10+
from operator_console.bootstrap import get_claude_command
11+
from operator_console.launcher import _multi_pane_block
12+
13+
14+
def _console_dir(tmp_path: Path) -> Path:
15+
cd = tmp_path / "console"
16+
(cd / "config" / "profiles").mkdir(parents=True)
17+
return cd
18+
19+
20+
def test_claude_wrapper_exports_cl_anchor_equal_to_cwd(tmp_path):
21+
console_dir = _console_dir(tmp_path)
22+
anchor = tmp_path / "PlatformManifest"
23+
cmd = get_claude_command(
24+
{"name": "platform", "repo_root": str(tmp_path / "repo")},
25+
tmp_path / "repo",
26+
console_dir=console_dir,
27+
session_key="platform",
28+
claude_cwd=anchor,
29+
)
30+
# cmd == "bash '<script>'" — read the generated wrapper
31+
script_path = cmd.split("'")[1]
32+
script = Path(script_path).read_text(encoding="utf-8")
33+
assert f"export CL_ANCHOR='{anchor.resolve()}'" in script
34+
35+
36+
def test_group_tab_anchors_at_platform_manifest(tmp_path):
37+
console_dir = _console_dir(tmp_path)
38+
profiles = [
39+
{"name": "a", "repo_root": str(tmp_path / "A")},
40+
{"name": "b", "repo_root": str(tmp_path / "B")},
41+
]
42+
block = _multi_pane_block(profiles, console_dir=console_dir, tab_name="platform")
43+
# Panes cd into PlatformManifest, not the bare ~/Documents/GitHub root.
44+
assert "PlatformManifest" in block
45+
assert "cd '" in block and "/PlatformManifest'" in block

0 commit comments

Comments
 (0)