diff --git a/adapters/codex/AGENTS.md.snippet b/adapters/codex/AGENTS.md.snippet new file mode 100644 index 00000000..593f97df --- /dev/null +++ b/adapters/codex/AGENTS.md.snippet @@ -0,0 +1,31 @@ +# Vouch — knowledge base + +This repo uses **vouch** for durable agent knowledge. The KB lives in +`.vouch/` and is reviewed in PRs like any other code. + +## How to remember things + +To preserve a fact, decision, or workflow across sessions: + +1. Register evidence: `kb_register_source` (or + `kb_register_source_from_path` for a file). +2. Propose a claim that cites it: `kb_propose_claim`. Every claim + MUST cite at least one source or evidence id. +3. For richer write-ups, propose pages: `kb_propose_page` with a + markdown body that references claims. + +You **cannot** write durable knowledge directly. Proposals land in +`.vouch/proposed/` and require human approval via `vouch approve`. +This is intentional. + +## How to read + +- `kb_search` for keyword search. +- `kb_context` to fill a working set for a task ("what does this KB know + about X?"). +- `kb_read_*` for specific ids. + +## Identity + +Set `VOUCH_AGENT=codex` in your env (or the MCP entry's `env:` block) so the +audit log can attribute writes to this host. diff --git a/adapters/codex/install.yaml b/adapters/codex/install.yaml index 7ba68b2f..40c6a5a2 100644 --- a/adapters/codex/install.yaml +++ b/adapters/codex/install.yaml @@ -1,10 +1,17 @@ # OpenAI Codex CLI adapter manifest. # +# T1 = project-local MCP config at `/.codex/config.toml`. # Codex reads `/.codex/config.toml` (also `~/.codex/config.toml` for # user-global). We ship the project-local form so `vouch install-mcp codex` # doesn't touch home-directory state -- see issue #179 scope decision. +# T2 = AGENTS.md fenced snippet (Codex reads AGENTS.md the way Cursor does). host: codex pretty: OpenAI Codex CLI +fence: + begin: "" + end: "" tiers: T1: - { src: config.toml, dst: .codex/config.toml } + T2: + - { src: AGENTS.md.snippet, dst: AGENTS.md, fenced_append: true } diff --git a/tests/test_install_adapter.py b/tests/test_install_adapter.py index 83036e8c..1714520e 100644 --- a/tests/test_install_adapter.py +++ b/tests/test_install_adapter.py @@ -480,3 +480,46 @@ def test_installed_wheel_resolves_adapters(tmp_path: Path) -> None: assert "claude-code" in result["hosts"], ( f"installed copy can't resolve adapters: {result}" ) + + +# --- codex: T2 AGENTS.md fenced snippet (vouchdev/vouch#385) --------------- + + +def test_install_codex_t1_writes_config_toml(tmp_path: Path) -> None: + """T1 produces the project-local .codex/config.toml.""" + result = install("codex", target=tmp_path, tier="T1") + assert (tmp_path / ".codex" / "config.toml").is_file() + assert not (tmp_path / "AGENTS.md").exists() + assert result.written and not result.appended + + +def test_install_codex_t2_creates_agents_md(tmp_path: Path) -> None: + """T2 appends the fenced AGENTS.md snippet; creates file when absent.""" + result = install("codex", target=tmp_path, tier="T2") + assert (tmp_path / ".codex" / "config.toml").is_file() + assert (tmp_path / "AGENTS.md").is_file() + assert "AGENTS.md" in result.written + content = (tmp_path / "AGENTS.md").read_text() + assert "" in content + assert "" in content + assert "VOUCH_AGENT=codex" in content + + +def test_install_codex_t2_idempotent(tmp_path: Path) -> None: + """Re-running T2 leaves AGENTS.md unchanged — fence dedup.""" + install("codex", target=tmp_path, tier="T2") + content_first = (tmp_path / "AGENTS.md").read_text() + again = install("codex", target=tmp_path, tier="T2") + content_again = (tmp_path / "AGENTS.md").read_text() + assert content_first == content_again + assert "AGENTS.md" not in again.appended + + +def test_install_codex_t2_appends_to_existing_agents_md(tmp_path: Path) -> None: + """When AGENTS.md already exists without a fence, the snippet is appended.""" + (tmp_path / "AGENTS.md").write_text("# Existing project notes\n") + result = install("codex", target=tmp_path, tier="T2") + assert "AGENTS.md" in result.appended + content = (tmp_path / "AGENTS.md").read_text() + assert content.startswith("# Existing project notes\n") + assert "" in content