fix(wrap): make RTK opt-in (off by default) across wrap subcommands (#2344)

## Description

RTK CLI-command filtering was set up **by default** across ~16 `wrap`
subcommands (copilot, codex, aider, cursor, cline, continue, goose,
openhands, opencode, grok, omp, openclaude, vibe, …) via `if not
no_rtk:` — so users got rtk hooks / instruction injection without opting
in. `wrap claude` was the lone exception (already gated on
`--context-tool`).

This makes RTK **opt-in (off by default)** everywhere, so Headroom's own
savings are what's measured unless a user explicitly wants rtk.

Closes #

## Type of Change
- [x] Bug fix (behavior change: default flip)

## Changes Made
- **Central gate** `_rtk_opt_in()` — RTK runs only when explicitly
enabled via `--rtk` or `HEADROOM_RTK=1`. Guards the 3 RTK entry points
(`_setup_rtk`, `_ensure_rtk_binary`, `_inject_rtk_instructions`) so they
no-op by default — one small change instead of editing ~30 call sites.
- **`--rtk` opt-in flag** on all 18 tool subcommands via a shared
eager-callback option (`expose_value=False`, sets `HEADROOM_RTK=1`; no
subcommand signature changes).
- `wrap claude`'s legacy `--context-tool` still opts in (mirrored into
the gate).
- **`--no-rtk` kept** as an accepted, now-redundant no-op (back-compat).
- lean-ctx and all non-RTK behavior untouched.

## Testing
```text
pytest tests/test_wrap_rtk_opt_in.py   -> 4 passed
ruff check / format                    -> clean
mypy headroom                          -> Success: no issues found in 504 source files
```
Verified `--rtk` appears in `wrap {claude,codex,copilot} --help`;
`_rtk_opt_in()` is False by default, True with `HEADROOM_RTK=1`; entry
points no-op + write nothing when off.

## Real Behavior Proof
- Env: local `.venv`, click CliRunner.
- Steps: import wrap; assert gate default-off / env-on; assert
`_setup_rtk`/`_ensure_rtk_binary` return None and
`_inject_rtk_instructions` returns False + writes no file when not opted
in; assert `--rtk` in subcommand help.
- Observed: all pass. Not tested: a live end-to-end wrap launch (proxy
spawn).

## Notes
Part 2 of 3 (RTK opt-in). Separate PRs cover the code-graph MCP engine
and the proxy-option cleanup. No `CHANGELOG.md` edit — release-please
generates it from the PR title (per the changelog guard).

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
This commit is contained in:
Tejas Chopra 2026-07-17 16:47:19 -07:00 committed by GitHub
parent 1b8c11ebfb
commit 44136ed042
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 165 additions and 5 deletions

View file

@ -953,6 +953,9 @@ def main() -> None:
"PATH": f"{shim_dir}{os.pathsep}{base_env['PATH']}", "PATH": f"{shim_dir}{os.pathsep}{base_env['PATH']}",
"HEADROOM_E2E_LOG_DIR": str(log_dir), "HEADROOM_E2E_LOG_DIR": str(log_dir),
"OPENAI_TARGET_API_URL": "http://127.0.0.1:19001/v1", "OPENAI_TARGET_API_URL": "http://127.0.0.1:19001/v1",
# RTK is opt-in (off by default). These wrap smoke tests assert
# RTK-instruction injection, so exercise the RTK-on path.
"HEADROOM_RTK": "1",
} }
) )

View file

@ -647,8 +647,42 @@ def _start_proxy(
stdio_log_file.close() stdio_log_file.close()
def _rtk_opt_in() -> bool:
"""Whether RTK CLI-command filtering was explicitly enabled.
RTK is opt-in (off by default): turn it on with ``--rtk`` (which sets
``HEADROOM_RTK=1``) or by exporting ``HEADROOM_RTK=1``. ``--no-rtk`` remains
accepted as a deprecated no-op.
"""
return os.environ.get("HEADROOM_RTK", "").strip().lower() in ("1", "true", "yes", "on")
def _rtk_flag_callback(ctx: Any, param: Any, value: bool) -> bool:
"""Click eager callback: ``--rtk`` sets HEADROOM_RTK so the central RTK gate
(:func:`_rtk_opt_in`) sees the opt-in without threading a param through every
wrap subcommand."""
if value:
os.environ["HEADROOM_RTK"] = "1"
return value
# Shared opt-in flag applied to every ``wrap`` subcommand. ``expose_value=False``
# so no subcommand signature changes; it works purely through HEADROOM_RTK.
_rtk_option = click.option(
"--rtk",
is_flag=True,
default=False,
expose_value=False,
is_eager=True,
callback=_rtk_flag_callback,
help="Enable RTK CLI-command filtering (opt-in; off by default). Also enabled by HEADROOM_RTK=1.",
)
def _setup_rtk(verbose: bool = False) -> Path | None: def _setup_rtk(verbose: bool = False) -> Path | None:
"""Ensure rtk is installed and hooks are registered.""" """Ensure rtk is installed and hooks are registered."""
if not _rtk_opt_in():
return None
from headroom.rtk import get_rtk_path from headroom.rtk import get_rtk_path
from headroom.rtk.installer import ensure_rtk, register_claude_hooks from headroom.rtk.installer import ensure_rtk, register_claude_hooks
@ -2123,6 +2157,8 @@ def _snapshot_codex_config_if_unwrapped(config_file: Path, backup_file: Path) ->
def _ensure_rtk_binary(verbose: bool = False) -> Path | None: def _ensure_rtk_binary(verbose: bool = False) -> Path | None:
"""Ensure rtk binary is installed (download if needed). No hook registration.""" """Ensure rtk binary is installed (download if needed). No hook registration."""
if not _rtk_opt_in():
return None
from headroom.rtk import get_rtk_path from headroom.rtk import get_rtk_path
from headroom.rtk.installer import ensure_rtk from headroom.rtk.installer import ensure_rtk
@ -2693,6 +2729,8 @@ def _inject_rtk_instructions(file_path: Path, verbose: bool = False) -> bool:
Idempotent skips if marker already present. Appends to existing content. Idempotent skips if marker already present. Appends to existing content.
Returns True if instructions were written. Returns True if instructions were written.
""" """
if not _rtk_opt_in():
return False
if file_path.exists(): if file_path.exists():
existing = _read_text(file_path) existing = _read_text(file_path)
if _RTK_MARKER in existing: if _RTK_MARKER in existing:
@ -4396,6 +4434,7 @@ def wrap_selfheal(marker: str | None) -> None:
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
# no "-p" short alias here: claude's own -p/--print must fall through to CLAUDE_ARGS # no "-p" short alias here: claude's own -p/--print must fall through to CLAUDE_ARGS
"--port", "--port",
@ -4523,7 +4562,12 @@ def claude(
headroom wrap claude --no-serena # Never register the Serena backup headroom wrap claude --no-serena # Never register the Serena backup
headroom wrap claude --1m # Preserve the 1M context window headroom wrap claude --1m # Preserve the 1M context window
""" """
setup_context_tool = context_tool and not no_rtk # RTK/context-tool is opt-in (off by default): --context-tool (legacy) and
# --rtk both enable it. Mirror --context-tool into HEADROOM_RTK so the central
# RTK gate (_rtk_opt_in) fires for the legacy flag too.
if context_tool:
os.environ["HEADROOM_RTK"] = "1"
setup_context_tool = (context_tool or _rtk_opt_in()) and not no_rtk
if prepare_only: if prepare_only:
if setup_context_tool: if setup_context_tool:
if _selected_context_tool() == _CONTEXT_TOOL_LEAN_CTX: if _selected_context_tool() == _CONTEXT_TOOL_LEAN_CTX:
@ -4936,6 +4980,7 @@ def unwrap_claude(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -5459,6 +5504,7 @@ def _run_codex_wrap(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -5577,6 +5623,7 @@ def codex(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -5681,6 +5728,7 @@ def aider(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)") @click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)")
@click.option( @click.option(
"--no-context-tool", "--no-context-tool",
@ -5783,6 +5831,7 @@ def openclaude(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -5862,6 +5911,7 @@ def vibe(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -5949,6 +5999,7 @@ def kimi(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6092,6 +6143,7 @@ def grok(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6198,6 +6250,7 @@ def cursor(
@wrap.command("grok-build", context_settings={"ignore_unknown_options": True}) @wrap.command("grok-build", context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6294,6 +6347,7 @@ def grok_build(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6396,6 +6450,7 @@ def cline(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6491,6 +6546,7 @@ def zcode(
@wrap.command("continue", context_settings={"ignore_unknown_options": True}) @wrap.command("continue", context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6615,6 +6671,7 @@ def continue_dev(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6740,6 +6797,7 @@ def goose(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -6883,6 +6941,7 @@ def openhands(
@wrap.command("openclaw") @wrap.command("openclaw")
@_rtk_option
@click.option( @click.option(
"--plugin-path", "--plugin-path",
type=click.Path(path_type=Path, file_okay=False, dir_okay=True), type=click.Path(path_type=Path, file_okay=False, dir_okay=True),
@ -7124,6 +7183,7 @@ def openclaw(
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )
@ -7648,6 +7708,7 @@ def unwrap_codex(port: int, no_stop_proxy: bool) -> None:
@wrap.command(context_settings={"ignore_unknown_options": True}) @wrap.command(context_settings={"ignore_unknown_options": True})
@_rtk_option
@click.option( @click.option(
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)" "--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
) )

View file

@ -164,6 +164,7 @@ def test_wrap_codex_prepare_only_accepts_no_context_tool_alias(monkeypatch, tmp_
def test_wrap_aider_prepare_only_injects_conventions(monkeypatch, tmp_path: Path) -> None: def test_wrap_aider_prepare_only_injects_conventions(monkeypatch, tmp_path: Path) -> None:
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1") # RTK is opt-in; exercise the RTK-on path
runner = CliRunner() runner = CliRunner()
with runner.isolated_filesystem(temp_dir=str(tmp_path)): with runner.isolated_filesystem(temp_dir=str(tmp_path)):
@ -180,6 +181,7 @@ def test_wrap_cursor_prepare_only_registers_native_hook(monkeypatch, tmp_path: P
# GH #756: when rtk's own `--agent cursor` hook registers successfully, # GH #756: when rtk's own `--agent cursor` hook registers successfully,
# headroom must not also inject RTK_INSTRUCTIONS_BLOCK into .cursorrules. # headroom must not also inject RTK_INSTRUCTIONS_BLOCK into .cursorrules.
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1") # RTK is opt-in; exercise the RTK-on path
runner = CliRunner() runner = CliRunner()
# headroom trusts the on-disk hook, not rtk's exit code, so simulate rtk # headroom trusts the on-disk hook, not rtk's exit code, so simulate rtk
@ -206,6 +208,7 @@ def test_wrap_cursor_prepare_only_falls_back_to_cursorrules_when_hook_fails(
monkeypatch, tmp_path: Path monkeypatch, tmp_path: Path
) -> None: ) -> None:
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1") # RTK is opt-in; exercise the RTK-on path
runner = CliRunner() runner = CliRunner()
with runner.isolated_filesystem(temp_dir=str(tmp_path)): with runner.isolated_filesystem(temp_dir=str(tmp_path)):

View file

@ -470,6 +470,7 @@ class TestInjectAndRestoreRoundTrip:
"""`wrap codex` injects the rtk block into the Codex global AGENTS.md; """`wrap codex` injects the rtk block into the Codex global AGENTS.md;
`unwrap codex` must take it back out (regression for #1421).""" `unwrap codex` must take it back out (regression for #1421)."""
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1")
codex_home = tmp_path / ".codex" codex_home = tmp_path / ".codex"
codex_home.mkdir() codex_home.mkdir()
agents = codex_home / "AGENTS.md" agents = codex_home / "AGENTS.md"
@ -487,6 +488,7 @@ class TestInjectAndRestoreRoundTrip:
"""Only the marker-fenced rtk block is removed; the user's own AGENTS.md """Only the marker-fenced rtk block is removed; the user's own AGENTS.md
prose survives the unwrap.""" prose survives the unwrap."""
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1")
codex_home = tmp_path / ".codex" codex_home = tmp_path / ".codex"
codex_home.mkdir() codex_home.mkdir()
agents = codex_home / "AGENTS.md" agents = codex_home / "AGENTS.md"
@ -1275,6 +1277,7 @@ def test_wrap_codex_injects_rtk_globally_without_changing_project_agents(
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: ) -> None:
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1")
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
project_dir.mkdir() project_dir.mkdir()
project_agents = project_dir / "AGENTS.md" project_agents = project_dir / "AGENTS.md"
@ -1308,6 +1311,7 @@ def test_wrap_codex_launch_injects_rtk_globally_without_changing_project_agents(
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: ) -> None:
_set_test_home(monkeypatch, tmp_path) _set_test_home(monkeypatch, tmp_path)
monkeypatch.setenv("HEADROOM_RTK", "1")
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
project_dir.mkdir() project_dir.mkdir()
project_agents = project_dir / "AGENTS.md" project_agents = project_dir / "AGENTS.md"

View file

@ -21,6 +21,12 @@ def _expected_project_prefix() -> str:
return f"/p/{quote(Path.cwd().name, safe='')}" return f"/p/{quote(Path.cwd().name, safe='')}"
@pytest.fixture(autouse=True)
def _enable_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
# RTK is opt-in (off by default); these tests exercise the RTK-on injection path.
monkeypatch.setenv("HEADROOM_RTK", "1")
@pytest.fixture @pytest.fixture
def runner() -> CliRunner: def runner() -> CliRunner:
return CliRunner() return CliRunner()

View file

@ -31,7 +31,11 @@ _EXISTING = "Be in “happy places” — really.\n".encode() + b"legacy \x9d by
@pytest.mark.parametrize("inject, marker", INJECTORS) @pytest.mark.parametrize("inject, marker", INJECTORS)
def test_inject_appends_into_file_with_non_ascii_and_stray_byte(inject, marker, tmp_path: Path): def test_inject_appends_into_file_with_non_ascii_and_stray_byte(
inject, marker, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
if marker == _RTK_MARKER:
monkeypatch.setenv("HEADROOM_RTK", "1")
target = tmp_path / "AGENTS.md" target = tmp_path / "AGENTS.md"
target.write_bytes(_EXISTING) target.write_bytes(_EXISTING)
@ -45,7 +49,11 @@ def test_inject_appends_into_file_with_non_ascii_and_stray_byte(inject, marker,
@pytest.mark.parametrize("inject, marker", INJECTORS) @pytest.mark.parametrize("inject, marker", INJECTORS)
def test_inject_creates_file_when_absent(inject, marker, tmp_path: Path): def test_inject_creates_file_when_absent(
inject, marker, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
if marker == _RTK_MARKER:
monkeypatch.setenv("HEADROOM_RTK", "1")
target = tmp_path / "nested" / "AGENTS.md" target = tmp_path / "nested" / "AGENTS.md"
assert inject(target) is True assert inject(target) is True
@ -53,7 +61,9 @@ def test_inject_creates_file_when_absent(inject, marker, tmp_path: Path):
@pytest.mark.parametrize("inject, marker", INJECTORS) @pytest.mark.parametrize("inject, marker", INJECTORS)
def test_inject_is_idempotent(inject, marker, tmp_path: Path): def test_inject_is_idempotent(inject, marker, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
if marker == _RTK_MARKER:
monkeypatch.setenv("HEADROOM_RTK", "1")
target = tmp_path / "AGENTS.md" target = tmp_path / "AGENTS.md"
target.write_bytes(_EXISTING) target.write_bytes(_EXISTING)

View file

@ -31,6 +31,12 @@ HINTFILE_AGENTS = [
] ]
@pytest.fixture(autouse=True)
def _enable_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
# RTK is opt-in (off by default); these tests exercise the RTK-on injection path.
monkeypatch.setenv("HEADROOM_RTK", "1")
@pytest.fixture @pytest.fixture
def runner() -> CliRunner: def runner() -> CliRunner:
return CliRunner() return CliRunner()

View file

@ -241,6 +241,7 @@ def test_wrap_omp_rtk_injects_into_cwd_agents_md(
runner: CliRunner, omp_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch runner: CliRunner, omp_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "rtk") monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "rtk")
monkeypatch.setenv("HEADROOM_RTK", "1")
with ( with (
patch("headroom.cli.wrap.shutil.which", return_value="omp"), patch("headroom.cli.wrap.shutil.which", return_value="omp"),
patch("headroom.cli.wrap._launch_tool"), patch("headroom.cli.wrap._launch_tool"),
@ -260,8 +261,9 @@ def test_wrap_omp_rtk_injects_into_cwd_agents_md(
def test_unwrap_omp_restored_and_cleans_agents_md( def test_unwrap_omp_restored_and_cleans_agents_md(
runner: CliRunner, omp_home: Path, tmp_path: Path runner: CliRunner, omp_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.setenv("HEADROOM_RTK", "1")
original = "providers:\n anthropic:\n apiKey: sk-user-secret\n" original = "providers:\n anthropic:\n apiKey: sk-user-secret\n"
omp_home.write_bytes(original.encode("utf-8")) omp_home.write_bytes(original.encode("utf-8"))
inject_models_override(8787, "proj") inject_models_override(8787, "proj")

View file

@ -24,6 +24,12 @@ def _expected_project_prefix() -> str:
return f"/p/{quote(Path.cwd().name, safe='')}" return f"/p/{quote(Path.cwd().name, safe='')}"
@pytest.fixture(autouse=True)
def _enable_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
# RTK is opt-in (off by default); these tests exercise the RTK-on injection path.
monkeypatch.setenv("HEADROOM_RTK", "1")
@pytest.fixture @pytest.fixture
def runner() -> CliRunner: def runner() -> CliRunner:
return CliRunner() return CliRunner()

View file

@ -13,6 +13,12 @@ from headroom.cli import wrap as wrap_mod
from headroom.cli.main import main from headroom.cli.main import main
@pytest.fixture(autouse=True)
def _enable_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
# RTK is opt-in (off by default); these tests exercise the RTK-on injection path.
monkeypatch.setenv("HEADROOM_RTK", "1")
@pytest.fixture @pytest.fixture
def runner() -> CliRunner: def runner() -> CliRunner:
return CliRunner() return CliRunner()

View file

@ -18,6 +18,12 @@ from headroom.cli import wrap as wrap_mod
from headroom.cli.main import main from headroom.cli.main import main
@pytest.fixture(autouse=True)
def _enable_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
# RTK is opt-in (off by default); these tests exercise the RTK-on injection path.
monkeypatch.setenv("HEADROOM_RTK", "1")
@pytest.fixture @pytest.fixture
def runner() -> CliRunner: def runner() -> CliRunner:
return CliRunner() return CliRunner()

View file

@ -0,0 +1,47 @@
"""RTK is opt-in (off by default): enabled only via --rtk / HEADROOM_RTK=1.
Regression for the RTK-default flip: the three RTK entry points must no-op
unless explicitly opted in, and every wrap subcommand must expose --rtk.
"""
from __future__ import annotations
import os
from unittest.mock import patch
from click.testing import CliRunner
from headroom.cli import wrap
def _no_rtk_env() -> dict[str, str]:
env = dict(os.environ)
env.pop("HEADROOM_RTK", None)
return env
def test_rtk_opt_in_off_by_default() -> None:
with patch.dict(os.environ, _no_rtk_env(), clear=True):
assert wrap._rtk_opt_in() is False
def test_rtk_opt_in_on_via_env() -> None:
for val in ("1", "true", "yes", "on"):
with patch.dict(os.environ, {"HEADROOM_RTK": val}):
assert wrap._rtk_opt_in() is True
def test_rtk_entry_points_noop_when_not_opted_in(tmp_path) -> None:
agents = tmp_path / "AGENTS.md"
with patch.dict(os.environ, _no_rtk_env(), clear=True):
assert wrap._setup_rtk() is None
assert wrap._ensure_rtk_binary() is None
assert wrap._inject_rtk_instructions(agents) is False
assert not agents.exists() # nothing written when RTK is off
def test_rtk_flag_present_on_subcommands() -> None:
runner = CliRunner()
for tool in ("claude", "codex", "copilot", "aider", "continue"):
out = runner.invoke(wrap.wrap, [tool, "--help"]).output
assert "--rtk" in out, f"--rtk missing from `wrap {tool} --help`"