fix(wrap): add Copilot unwrap command (#1251)

## Description

Adds the missing `headroom unwrap copilot` command so the durable setup
created by `headroom wrap copilot` can be removed without touching
user-authored Copilot instructions.

Closes #1172

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add `headroom unwrap copilot` with `--port` and `--no-stop-proxy`
options.
- Remove only Headroom's marker-fenced RTK block from
`.github/copilot-instructions.md`.
- Preserve user-authored content and leave malformed/unmatched markers
unchanged.
- Remove an instruction file that contains only Headroom's generated
block.
- Update the changelog.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
> .\.venv\Scripts\python.exe -X utf8 -m pytest tests/test_cli/test_wrap_copilot.py -q
30 passed in 1.11s

> .\.venv\Scripts\ruff.exe check .
All checks passed!

> uv run --extra dev ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_copilot.py
2 files already formatted

> uv run --extra dev mypy headroom/cli/wrap.py
Success: no issues found in 1 source file
```

The new command test failed before the implementation with:

```text
Error: No such command 'copilot'.
```

## Real Behavior Proof

- Environment: Windows, Python 3.12.12, local editable Headroom
checkout.
- Exact command / steps: created an isolated project containing user
guidance plus a Headroom marker-fenced RTK block, then ran
`.venv\Scripts\headroom.exe unwrap copilot --no-stop-proxy`.
- Observed result: command exited `0`, printed `Removed Headroom rtk
instructions from Copilot.`, and the resulting file contained only `Keep
user guidance.`.
- Not tested: a live Copilot CLI session or terminating a real proxy
process; proxy shutdown delegates to the existing tested unwrap helper
and is covered here with a command-level mock.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

Not applicable; this is a CLI-only change.

## Additional Notes

No dependencies were added. The unchecked comment item is not applicable
because the cleanup helper and command are straightforward and
documented with docstrings.

This pull request includes code written with the assistance of AI. The
changes have not yet been reviewed by a human.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
This commit is contained in:
Terminal Chai 2026-06-23 09:25:36 +05:30 committed by GitHub
parent 23d73ae070
commit b4fde0c3a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 159 additions and 0 deletions

View file

@ -1663,6 +1663,34 @@ def _inject_rtk_instructions(file_path: Path, verbose: bool = False) -> bool:
return True
def _remove_rtk_instructions(file_path: Path) -> bool:
"""Remove Headroom's marker-fenced rtk guidance from an instruction file."""
if not file_path.exists():
return False
content = file_path.read_text(encoding="utf-8")
end_marker = "<!-- /headroom:rtk-instructions -->"
start = content.find(_RTK_MARKER)
if start < 0:
return False
end = content.find(end_marker, start)
if end < 0:
return False
end += len(end_marker)
prefix = content[:start].rstrip()
suffix = content[end:].lstrip("\r\n")
cleaned = "\n\n".join(part for part in (prefix, suffix) if part)
if cleaned:
cleaned = cleaned.rstrip() + "\n"
if cleaned:
file_path.write_text(cleaned, encoding="utf-8")
else:
file_path.unlink()
return True
def _inject_memory_mcp_config(db_path: str, user_id: str) -> None:
"""Register headroom memory as an MCP server in Codex's config.toml.
@ -3632,6 +3660,26 @@ def copilot(
)
# =============================================================================
# GitHub Copilot CLI (unwrap)
# =============================================================================
@unwrap.command("copilot")
@click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)")
@click.option("--no-stop-proxy", is_flag=True, help="Do not stop the local Headroom proxy")
def unwrap_copilot(port: int, no_stop_proxy: bool) -> None:
"""Undo durable setup from ``headroom wrap copilot``."""
instructions = Path.cwd() / ".github" / "copilot-instructions.md"
if _remove_rtk_instructions(instructions):
click.echo(" Removed Headroom rtk instructions from Copilot.")
else:
click.echo(" No Headroom rtk instructions found for Copilot.")
if not no_stop_proxy:
_echo_unwrap_proxy_stop_status(_stop_local_proxy_for_unwrap(port), port)
# =============================================================================
# OpenAI Codex CLI
# =============================================================================

View file

@ -668,6 +668,117 @@ def test_wrap_copilot_fails_when_binary_missing(
assert "Install GitHub Copilot CLI" in result.output
def test_unwrap_copilot_removes_rtk_instructions_and_stops_proxy(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
wrap_cli, main = wrap_modules
monkeypatch.chdir(tmp_path)
instructions = tmp_path / ".github" / "copilot-instructions.md"
instructions.parent.mkdir()
instructions.write_text(
"Keep user guidance.\n\n" + wrap_cli.RTK_INSTRUCTIONS_BLOCK,
encoding="utf-8",
)
with patch(
"headroom.cli.wrap._stop_local_proxy_for_unwrap",
return_value="stopped",
) as stop_proxy:
result = runner.invoke(main, ["unwrap", "copilot", "--port", "9999"])
assert result.exit_code == 0, result.output
assert instructions.read_text(encoding="utf-8") == "Keep user guidance.\n"
stop_proxy.assert_called_once_with(9999)
assert "Removed Headroom rtk instructions from Copilot." in result.output
assert "Stopped local Headroom proxy on port 9999" in result.output
def test_unwrap_copilot_preserves_instructions_after_rtk_block(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
wrap_cli, main = wrap_modules
monkeypatch.chdir(tmp_path)
instructions = tmp_path / ".github" / "copilot-instructions.md"
instructions.parent.mkdir()
instructions.write_text(
wrap_cli.RTK_INSTRUCTIONS_BLOCK + "\nKeep trailing guidance.\n",
encoding="utf-8",
)
result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"])
assert result.exit_code == 0, result.output
assert instructions.read_text(encoding="utf-8") == "Keep trailing guidance.\n"
def test_unwrap_copilot_leaves_malformed_marker_content_unchanged(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
wrap_cli, main = wrap_modules
monkeypatch.chdir(tmp_path)
instructions = tmp_path / ".github" / "copilot-instructions.md"
instructions.parent.mkdir()
content = f"<!-- /headroom:rtk-instructions -->\nKeep user guidance.\n{wrap_cli._RTK_MARKER}\n"
instructions.write_text(content, encoding="utf-8")
result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"])
assert result.exit_code == 0, result.output
assert instructions.read_text(encoding="utf-8") == content
assert "No Headroom rtk instructions found for Copilot." in result.output
def test_unwrap_copilot_deletes_generated_only_instruction_file(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
wrap_cli, main = wrap_modules
monkeypatch.chdir(tmp_path)
instructions = tmp_path / ".github" / "copilot-instructions.md"
instructions.parent.mkdir()
instructions.write_text(wrap_cli.RTK_INSTRUCTIONS_BLOCK, encoding="utf-8")
result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"])
assert result.exit_code == 0, result.output
assert not instructions.exists()
@pytest.mark.parametrize("create_user_file", [False, True])
def test_unwrap_copilot_is_noop_without_managed_instructions(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
create_user_file: bool,
) -> None:
_wrap_cli, main = wrap_modules
monkeypatch.chdir(tmp_path)
instructions = tmp_path / ".github" / "copilot-instructions.md"
if create_user_file:
instructions.parent.mkdir()
instructions.write_text("Keep user guidance.\n", encoding="utf-8")
result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"])
assert result.exit_code == 0, result.output
assert instructions.exists() is create_user_file
if create_user_file:
assert instructions.read_text(encoding="utf-8") == "Keep user guidance.\n"
assert "No Headroom rtk instructions found for Copilot." in result.output
# ---------------------------------------------------------------------------
# Regression suite for #610 — GitHub Copilot endpoint routing per auth mode.
#