From 4cafe8eb22d1363a73f7cd9d5921e88c693cd78b Mon Sep 17 00:00:00 2001 From: astandrik Date: Tue, 18 Aug 2026 12:44:25 +0300 Subject: [PATCH 1/2] fix(codex): stop installing global AGENTS guidance Signed-off-by: astandrik --- README.md | 6 +- scripts/smoke-test.sh | 121 ++++++++++++++++++++++++++-- src/cli/cli.c | 36 +++++++-- tests/test_cli.c | 182 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 326 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index dfd969044..932f92231 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,7 @@ overwrite user-modified agents. | Agent | Activation | MCP config | Durable context / augmentation | |-------|------------|------------|--------------------------------| | Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`/`Bash`, and post-`Read` coverage | -| Codex CLI | Detected | `$CODEX_HOME/config.toml` | `AGENTS.md`, skill, three read-only agents; `SessionStart` + `SubagentStart` | +| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Skill, three read-only agents; `SessionStart` + `SubagentStart` | | Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` | | Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill | | OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection | @@ -530,6 +530,10 @@ overwrite user-modified agents. | Oh My Pi (omp) | Detected | Effective agent directory (`OMP_PROFILE` / `PI_CODING_AGENT_DIR`; default `~/.omp/agent/mcp.json`) | Skill and three direct-MCP graph-tool subagents (Scout/Verify/Auditor); preserves user `AGENTS.md` | | Sourcegraph Cody | Explicit opt-in | Existing `$CBM_CODY_CONFIG_PATH` | MCP only | +For Codex, install no longer creates global `$CODEX_HOME/AGENTS.md` guidance. When that file +already exists, install removes only the legacy `codebase-memory-mcp` managed block and preserves +all user-owned bytes; the file itself remains even when the cleanup leaves it empty. + ### Sessions, compaction, and subagents Hooks installed by this project are fail-open and context-only. Claude Code's diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index fb7fe3dd3..e5c27f082 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -144,11 +144,12 @@ run_no_crash() { TMPDIR=$(smoke_mktemp_dir) DRYRUN_HOME="" +CODEX_LIFECYCLE_HOME="" # On MSYS2/Windows, convert POSIX path to native Windows path for the binary if command -v cygpath &>/dev/null; then TMPDIR=$(cygpath -m "$TMPDIR") fi -trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}"' EXIT +trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT CLI_STDERR=$(smoke_mktemp_file) # 10 of the cli call sites assign directly (VAR=$(cli ...)). Under @@ -1430,6 +1431,7 @@ echo '# Personal Rovo guidance' > "$ROVO_INSTRUCTIONS" PHASE8_INSTALL_RC=0 PHASE8_INSTALL_LOG=$(smoke_mktemp_file) HOME="$FAKE_HOME" \ + CODEX_HOME="$FAKE_HOME/.codex" \ XDG_CONFIG_HOME="$FAKE_HOME/.config" \ APPDATA="$FAKE_HOME/AppData/Roaming" \ LOCALAPPDATA="$FAKE_HOME/AppData/Local" \ @@ -1702,12 +1704,116 @@ if ! grep -q 'existing_section' "$FAKE_HOME/.codex/config.toml"; then fi echo "OK 8f-h: Codex TOML (MCP + preserved existing)" -# 8i: Codex instructions -if [ ! -f "$FAKE_HOME/.codex/AGENTS.md" ] || ! grep -q 'codebase-memory-mcp' "$FAKE_HOME/.codex/AGENTS.md"; then - echo "FAIL 8i: Codex AGENTS.md missing" - exit 1 -fi -echo "OK 8i: Codex instructions" +# 8i: Fresh Codex installs do not create global instructions. Exercise the +# complete legacy lifecycle in a second HOME so Codex-only reinstalls cannot +# perturb the all-agent fixture that the remaining Phase 8 checks inspect. +if [ -e "$FAKE_HOME/.codex/AGENTS.md" ]; then + echo "FAIL 8i: fresh Codex install created global AGENTS.md" + exit 1 +fi + +CODEX_LIFECYCLE_HOME=$(smoke_mktemp_dir) +CODEX_LIFECYCLE_ROOT="$CODEX_LIFECYCLE_HOME/.codex" +CODEX_INSTRUCTIONS="$CODEX_LIFECYCLE_ROOT/AGENTS.md" +mkdir -p "$CODEX_LIFECYCLE_ROOT" + +CODEX_FRESH_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --skip-binary --clients=codex -y > "$CODEX_FRESH_LOG" 2>&1 +if [ -e "$CODEX_INSTRUCTIONS" ] || + ! grep -q '\[mcp_servers.codebase-memory-mcp\]' "$CODEX_LIFECYCLE_ROOT/config.toml" || + [ ! -s "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" ] || + [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-scout.toml" ] || + [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory.toml" ] || + [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-auditor.toml" ] || + ! grep -q 'SessionStart' "$CODEX_LIFECYCLE_ROOT/config.toml" || + ! grep -q 'SubagentStart' "$CODEX_LIFECYCLE_ROOT/config.toml"; then + echo "FAIL 8i: isolated fresh Codex install changed instructions or lost another surface" + exit 1 +fi + +printf '%s\n' \ + '# Personal Codex guidance' \ + '' \ + 'legacy managed guidance' \ + '' \ + '# Keep this line' > "$CODEX_INSTRUCTIONS" +CODEX_EXPECTED=$(smoke_mktemp_file) +printf '%s\n' '# Personal Codex guidance' '# Keep this line' > "$CODEX_EXPECTED" +CODEX_LEGACY_SHA=$(smoke_file_sha256 "$CODEX_INSTRUCTIONS") + +CODEX_PLAN=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --plan --skip-binary --clients=codex > "$CODEX_PLAN" +CODEX_CLEANUP_PATH=$(json_get "$CODEX_PLAN" \ + "next((x.get('path','') for x in d.get('cleanup_actions_planned', []) if x.get('agent') == 'Codex CLI' and x.get('kind') == 'instructions' and x.get('operation') == 'remove_managed_block_if_present'), '')") +CODEX_PLANS_INSTRUCTIONS=$(json_get "$CODEX_PLAN" \ + "any(str(x).replace('\\\\','/').endswith('/.codex/AGENTS.md') for x in d.get('instruction_files_planned', []))") +if ! exact_path_match "$CODEX_CLEANUP_PATH" "$CODEX_INSTRUCTIONS" || + [ "$CODEX_PLANS_INSTRUCTIONS" != "False" ] || + [ "$CODEX_LEGACY_SHA" != "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" ]; then + echo "FAIL 8i: Codex plan receipt or no-mutation contract is wrong" + exit 1 +fi + +CODEX_DRY_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --dry-run --skip-binary --clients=codex -y > "$CODEX_DRY_LOG" 2>&1 +if ! grep -q 'managed block if present' "$CODEX_DRY_LOG" || + [ "$CODEX_LEGACY_SHA" != "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" ]; then + echo "FAIL 8i: Codex dry-run did not preview byte-identical legacy cleanup" + exit 1 +fi + +CODEX_MIGRATE_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --skip-binary --clients=codex -y > "$CODEX_MIGRATE_LOG" 2>&1 +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then + echo "FAIL 8i: Codex migration changed user-owned AGENTS.md bytes" + exit 1 +fi + +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" install --skip-binary --clients=codex -y > /dev/null 2>&1 +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then + echo "FAIL 8i: Codex reinstall restored global managed instructions" + exit 1 +fi +CODEX_UNINSTALL_LOG=$(smoke_mktemp_file) +HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" uninstall -y -n > "$CODEX_UNINSTALL_LOG" 2>&1 || true +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then + echo "FAIL 8i: Codex uninstall changed user-owned AGENTS.md bytes" + exit 1 +fi +echo "OK 8i: isolated Codex fresh/plan/dry-run/migration/reinstall/uninstall lifecycle" # 8j-l: Gemini MCP + hooks + merge CMD=$(json_get "$FAKE_HOME/.gemini/settings.json" "d['mcpServers']['codebase-memory-mcp']['command']") @@ -2685,6 +2791,7 @@ if [[ "$BINARY" == *.exe ]]; then UNINSTALL_BINARY="$SELF_PATH" fi HOME="$FAKE_HOME" \ + CODEX_HOME="$FAKE_HOME/.codex" \ XDG_CONFIG_HOME="$FAKE_HOME/.config" \ APPDATA="$FAKE_HOME/AppData/Roaming" \ LOCALAPPDATA="$FAKE_HOME/AppData/Local" \ diff --git a/src/cli/cli.c b/src/cli/cli.c index 56af212d1..e0382a96e 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -7522,7 +7522,8 @@ static void print_detected_agents(const cbm_detected_agents_t *a, const char *ho * behavior (it is the same code path with mutations disabled). */ typedef struct { char agent[CLI_BUF_32]; - char kind[CLI_BUF_32]; /* mcp_config | instructions | skills | hook */ + /* mcp_config | instructions | skills | hook | cleanup_instructions */ + char kind[CLI_BUF_32]; char path[CLI_BUF_1K]; } cbm_plan_entry_t; @@ -7860,6 +7861,20 @@ static bool install_generic_agent_config(const char *label, const char *binary_p return mcp_installed; } +static void cleanup_codex_legacy_instructions(const char *path, bool dry_run) { + if (!path || !cbm_file_exists(path)) { + return; + } + if (g_install_plan) { + plan_record("Codex CLI", "cleanup_instructions", path); + return; + } + printf(" instructions cleanup: %s (managed block if present)\n", path); + if (!dry_run && cbm_remove_instructions(path) != CLI_OK) { + record_agent_config_error(false, "Codex CLI", "legacy_instructions_cleanup", path); + } +} + static void install_windsurf_config(const char *binary_path, const char *config_path, const char *rules_path, bool dry_run) { if (g_install_plan) { @@ -8826,8 +8841,9 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const reason); goto codex_install_done; } - install_generic_agent_config("Codex CLI", binary_path, cp, ip, dry_run, + install_generic_agent_config("Codex CLI", binary_path, cp, NULL, dry_run, cbm_upsert_codex_mcp); + cleanup_codex_legacy_instructions(ip, dry_run); install_agent_skill("Codex CLI", skills_dir, force, dry_run); install_tiered_agent_profiles( (cbm_tiered_profile_set_t){ @@ -9915,9 +9931,10 @@ static bool cli_binary_is_externally_managed(const char *self_path, bool self_pa } /* Build the agent.install.plan.v1 receipt (#388): a machine-readable list of - * the config / instruction / skill / agent / hook files `install` WOULD write, produced by - * running the real install dispatch in record-only mode (no mutation, no - * network). Returns a heap JSON string (caller frees) or NULL. */ + * the config / instruction / skill / agent / hook files `install` WOULD write + * plus conditional cleanup actions, produced by running the real install + * dispatch in record-only mode (no mutation, no network). Returns a heap JSON + * string (caller frees) or NULL. */ static char *cbm_build_install_plan_json_options(const char *home, const char *binary_path, bool skip_config) { if (!home || !binary_path) { @@ -9993,10 +10010,18 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b yyjson_mut_val *agent_files = yyjson_mut_arr(doc); yyjson_mut_val *prompt_files = yyjson_mut_arr(doc); yyjson_mut_val *hooks = yyjson_mut_arr(doc); + yyjson_mut_val *cleanups = yyjson_mut_arr(doc); for (int i = 0; i < plan.count; i++) { cbm_plan_entry_t *e = &plan.items[i]; if (strcmp(e->kind, "mcp_config") == 0) { yyjson_mut_arr_add_strcpy(doc, configs, e->path); + } else if (strcmp(e->kind, "cleanup_instructions") == 0) { + yyjson_mut_val *cleanup = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, cleanup, "agent", e->agent); + yyjson_mut_obj_add_str(doc, cleanup, "kind", "instructions"); + yyjson_mut_obj_add_str(doc, cleanup, "operation", "remove_managed_block_if_present"); + yyjson_mut_obj_add_strcpy(doc, cleanup, "path", e->path); + yyjson_mut_arr_add_val(cleanups, cleanup); } else if (strcmp(e->kind, "hook") == 0) { yyjson_mut_val *h = yyjson_mut_obj(doc); yyjson_mut_obj_add_strcpy(doc, h, "agent", e->agent); @@ -10021,6 +10046,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b yyjson_mut_obj_add_val(doc, root, "agent_files_planned", agent_files); yyjson_mut_obj_add_val(doc, root, "prompt_files_planned", prompt_files); yyjson_mut_obj_add_val(doc, root, "hooks_planned", hooks); + yyjson_mut_obj_add_val(doc, root, "cleanup_actions_planned", cleanups); yyjson_mut_obj_add_bool(doc, root, "writes_started", false); yyjson_mut_obj_add_bool(doc, root, "network_after_install", false); yyjson_mut_obj_add_str(doc, root, "next_safe_command", "codebase-memory-mcp install -y"); diff --git a/tests/test_cli.c b/tests/test_cli.c index a72ed6474..8ff5a1c46 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -294,6 +294,32 @@ static bool test_plan_has_hook_for_agent(yyjson_val *root, const char *agent) { return false; } +static bool test_plan_cleanup_contains(yyjson_val *root, const char *agent, const char *kind, + const char *operation, const char *path) { + yyjson_val *items = root ? yyjson_obj_get(root, "cleanup_actions_planned") : NULL; + if (!items || !yyjson_is_arr(items)) { + return false; + } + size_t index; + size_t count; + yyjson_val *item; + yyjson_arr_foreach(items, index, count, item) { + yyjson_val *agent_value = yyjson_obj_get(item, "agent"); + yyjson_val *kind_value = yyjson_obj_get(item, "kind"); + yyjson_val *operation_value = yyjson_obj_get(item, "operation"); + yyjson_val *path_value = yyjson_obj_get(item, "path"); + if (agent_value && yyjson_is_str(agent_value) && kind_value && yyjson_is_str(kind_value) && + operation_value && yyjson_is_str(operation_value) && path_value && + yyjson_is_str(path_value) && strcmp(yyjson_get_str(agent_value), agent) == 0 && + strcmp(yyjson_get_str(kind_value), kind) == 0 && + strcmp(yyjson_get_str(operation_value), operation) == 0 && + strcmp(yyjson_get_str(path_value), path) == 0) { + return true; + } + } + return false; +} + static size_t test_count_substring(const char *text, const char *needle) { size_t count = 0U; size_t needle_len = strlen(needle); @@ -4496,6 +4522,23 @@ TEST(cli_install_plan_receipt_no_mutation_issue388) { } } } + + yyjson_doc *doc = NULL; + if (!missing) { + doc = yyjson_read(json, strlen(json), 0); + if (!doc) { + missing = "a valid JSON document"; + } else { + yyjson_val *cleanups = + yyjson_obj_get(yyjson_doc_get_root(doc), "cleanup_actions_planned"); + if (!cleanups || !yyjson_is_arr(cleanups) || yyjson_arr_size(cleanups) != 0U) { + missing = "an empty cleanup_actions_planned array"; + } + } + } + if (doc) { + yyjson_doc_free(doc); + } free(json); /* Critical: building the plan must NOT have created any config file. */ @@ -7865,23 +7908,149 @@ TEST(cli_codex_respects_codex_home) { char *saved = save_test_env("CODEX_HOME"); cbm_setenv("CODEX_HOME", codex_home, 1); + char expected_instructions[640]; + snprintf(expected_instructions, sizeof(expected_instructions), "%s/AGENTS.md", codex_home); + const char *user_instructions = "# Personal Codex guidance\n"; + ASSERT_EQ(write_test_file(expected_instructions, user_instructions), 0); + cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); char *json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp"); char expected_config[640]; - char expected_instructions[640]; snprintf(expected_config, sizeof(expected_config), "%s/config.toml", codex_home); - snprintf(expected_instructions, sizeof(expected_instructions), "%s/AGENTS.md", codex_home); - bool plans_config = json && strstr(json, expected_config) != NULL; - bool plans_instructions = json && strstr(json, expected_instructions) != NULL; + yyjson_doc *plan_doc = json ? yyjson_read(json, strlen(json), 0) : NULL; + yyjson_val *plan_root = plan_doc ? yyjson_doc_get_root(plan_doc) : NULL; + bool plans_config = test_json_string_array_contains(plan_root, "config_files_planned", + expected_config); + bool plans_instructions = test_json_string_array_contains( + plan_root, "instruction_files_planned", expected_instructions); + bool plans_cleanup = test_plan_cleanup_contains( + plan_root, "Codex CLI", "instructions", "remove_managed_block_if_present", + expected_instructions); + char *instructions_after = read_test_file_alloc(expected_instructions); + bool plan_preserved_user_file = + instructions_after && strcmp(instructions_after, user_instructions) == 0; + free(instructions_after); + yyjson_doc_free(plan_doc); free(json); restore_test_env("CODEX_HOME", saved); test_rmdir_r(tmpdir); if (!agents.codex) FAIL("Codex detection must honor CODEX_HOME"); - if (!plans_config || !plans_instructions) - FAIL("Codex install plan must place config and AGENTS.md under CODEX_HOME"); + if (!plans_config || plans_instructions || !plans_cleanup || !plan_preserved_user_file) + FAIL("Codex plan must keep config under CODEX_HOME and report legacy AGENTS cleanup " + "without planning a new instruction file or mutating user content"); + PASS(); +} + +TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-agents-cleanup-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char codex_home[512]; + char agents_path[640]; + char config_path[640]; + char skill_path[768]; + char profile_path[768]; + snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir); + snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_home); + snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_home); + snprintf(skill_path, sizeof(skill_path), "%s/skills/codebase-memory/SKILL.md", codex_home); + snprintf(profile_path, sizeof(profile_path), "%s/agents/codebase-memory.toml", codex_home); + ASSERT_EQ(test_mkdirp(codex_home), 0); + + char *saved_home = save_test_env("HOME"); + char *saved_path = save_test_env("PATH"); + char *saved_codex = save_test_env("CODEX_HOME"); + cbm_setenv("HOME", tmpdir, 1); + cbm_setenv("PATH", tmpdir, 1); + cbm_setenv("CODEX_HOME", codex_home, 1); + + struct stat state; + int fresh_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + bool fresh_did_not_create_agents = stat(agents_path, &state) != 0; + char *config = read_test_file_alloc(config_path); + bool other_surfaces_installed = + fresh_rc == 0 && config && strstr(config, "[mcp_servers.codebase-memory-mcp]") && + strstr(config, "SessionStart") && stat(skill_path, &state) == 0 && + stat(profile_path, &state) == 0; + free(config); + + const char *user_only = "# Personal Codex guidance\nKeep this byte-for-byte.\n"; + ASSERT_EQ(write_test_file(agents_path, user_only), 0); + int dry_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true); + char *after_dry = read_test_file_alloc(agents_path); + int unowned_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_unowned = read_test_file_alloc(agents_path); + bool unowned_preserved = dry_rc == 0 && unowned_rc == 0 && after_dry && after_unowned && + strcmp(after_dry, user_only) == 0 && + strcmp(after_unowned, user_only) == 0; + free(after_dry); + free(after_unowned); + + const char *legacy = "# Before\n\nlegacy\n" + "\n# After\n"; + const char *legacy_removed = "# Before\n# After\n"; + ASSERT_EQ(write_test_file(agents_path, legacy), 0); + int managed_dry_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true); + char *after_managed_dry = read_test_file_alloc(agents_path); + int managed_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_managed = read_test_file_alloc(agents_path); + int repeat_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_repeat = read_test_file_alloc(agents_path); + bool managed_removed_once = managed_dry_rc == 0 && after_managed_dry && + strcmp(after_managed_dry, legacy) == 0 && managed_rc == 0 && + repeat_rc == 0 && after_managed && after_repeat && + strcmp(after_managed, legacy_removed) == 0 && + strcmp(after_repeat, legacy_removed) == 0; + free(after_managed_dry); + free(after_managed); + free(after_repeat); + + const char *managed_only = "\nlegacy\n" + "\n"; + ASSERT_EQ(write_test_file(agents_path, managed_only), 0); + int managed_only_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_managed_only = read_test_file_alloc(agents_path); + bool empty_file_preserved = managed_only_rc == 0 && after_managed_only && + after_managed_only[0] == '\0' && stat(agents_path, &state) == 0; + free(after_managed_only); + + const char *malformed = "# Keep\n\nunterminated\n"; + ASSERT_EQ(write_test_file(agents_path, malformed), 0); + ASSERT_EQ(remove(config_path), 0); + ASSERT_EQ(remove(skill_path), 0); + ASSERT_EQ(remove(profile_path), 0); + int malformed_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *after_malformed = read_test_file_alloc(agents_path); + config = read_test_file_alloc(config_path); + bool malformed_preserved = malformed_rc != 0 && after_malformed && + strcmp(after_malformed, malformed) == 0 && config && + strstr(config, "[mcp_servers.codebase-memory-mcp]") && + strstr(config, "SessionStart") && stat(skill_path, &state) == 0 && + stat(profile_path, &state) == 0; + free(config); + free(after_malformed); + + restore_test_env("HOME", saved_home); + restore_test_env("PATH", saved_path); + restore_test_env("CODEX_HOME", saved_codex); + test_rmdir_r(tmpdir); + + if (!fresh_did_not_create_agents || !other_surfaces_installed || !unowned_preserved || + !managed_removed_once || !empty_file_preserved || !malformed_preserved) + FAIL("Codex install must stop creating global AGENTS guidance, remove only its owned " + "legacy block, preserve malformed or user-owned bytes, and keep other surfaces"); PASS(); } @@ -13853,6 +14022,7 @@ SUITE(cli) { RUN_TEST(cli_claude_user_scope_avoids_nested_mcp_json); RUN_TEST(cli_codex_respects_codex_home); RUN_TEST(cli_grok_respects_grok_home); + RUN_TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689); RUN_TEST(cli_gemini_session_hook_uses_json_for_all_sources); RUN_TEST(cli_gemini_installs_dedicated_graph_subagent); RUN_TEST(cli_antigravity_does_not_imply_gemini); From 4c1b23477b8e0de27feee74eac11c21a1fd64151 Mon Sep 17 00:00:00 2001 From: astandrik Date: Tue, 25 Aug 2026 13:29:48 +0300 Subject: [PATCH 2/2] fix(codex): keep global activation pointer Signed-off-by: astandrik --- README.md | 9 +- scripts/smoke-test.sh | 87 +++++++++----- src/cli/cli.c | 77 ++++++------ tests/test_cli.c | 272 +++++++++++++++++++++++++++--------------- 4 files changed, 279 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 932f92231..0ef3e7d26 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,7 @@ overwrite user-modified agents. | Agent | Activation | MCP config | Durable context / augmentation | |-------|------------|------------|--------------------------------| | Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`/`Bash`, and post-`Read` coverage | -| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Skill, three read-only agents; `SessionStart` + `SubagentStart` | +| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart` | | Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` | | Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill | | OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection | @@ -530,9 +530,10 @@ overwrite user-modified agents. | Oh My Pi (omp) | Detected | Effective agent directory (`OMP_PROFILE` / `PI_CODING_AGENT_DIR`; default `~/.omp/agent/mcp.json`) | Skill and three direct-MCP graph-tool subagents (Scout/Verify/Auditor); preserves user `AGENTS.md` | | Sourcegraph Cody | Explicit opt-in | Existing `$CBM_CODY_CONFIG_PATH` | MCP only | -For Codex, install no longer creates global `$CODEX_HOME/AGENTS.md` guidance. When that file -already exists, install removes only the legacy `codebase-memory-mcp` managed block and preserves -all user-owned bytes; the file itself remains even when the cleanup leaves it empty. +For Codex, install keeps only a tiny managed activation pointer in global +`$CODEX_HOME/AGENTS.md`; all detailed behavior lives in the installed `codebase-memory` skill. +Fresh installs create the pointer, upgrades replace the legacy full managed block while preserving +all user-owned bytes, and uninstall removes only the managed pointer. ### Sessions, compaction, and subagents diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index e5c27f082..87a47d2b2 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -1704,11 +1704,18 @@ if ! grep -q 'existing_section' "$FAKE_HOME/.codex/config.toml"; then fi echo "OK 8f-h: Codex TOML (MCP + preserved existing)" -# 8i: Fresh Codex installs do not create global instructions. Exercise the -# complete legacy lifecycle in a second HOME so Codex-only reinstalls cannot -# perturb the all-agent fixture that the remaining Phase 8 checks inspect. -if [ -e "$FAKE_HOME/.codex/AGENTS.md" ]; then - echo "FAIL 8i: fresh Codex install created global AGENTS.md" +# 8i: Codex keeps only a tiny global activation pointer; the installed skill +# owns all detailed behavior. Exercise the complete lifecycle in a second HOME +# so Codex-only reinstalls cannot perturb the all-agent fixture below. +CODEX_POINTER_EXPECTED=$(smoke_mktemp_file) +printf '%s\n' \ + '' \ + 'For structural codebase exploration, use the installed `codebase-memory` skill.' \ + '' > "$CODEX_POINTER_EXPECTED" +if [ ! -f "$FAKE_HOME/.codex/AGENTS.md" ] || + [ "$(smoke_file_sha256 "$FAKE_HOME/.codex/AGENTS.md")" != \ + "$(smoke_file_sha256 "$CODEX_POINTER_EXPECTED")" ]; then + echo "FAIL 8i: fresh Codex install did not create the exact activation pointer" exit 1 fi @@ -1724,15 +1731,17 @@ HOME="$CODEX_LIFECYCLE_HOME" \ APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ "$BINARY" install --skip-binary --clients=codex -y > "$CODEX_FRESH_LOG" 2>&1 -if [ -e "$CODEX_INSTRUCTIONS" ] || +if [ ! -f "$CODEX_INSTRUCTIONS" ] || + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \ + "$(smoke_file_sha256 "$CODEX_POINTER_EXPECTED")" ] || ! grep -q '\[mcp_servers.codebase-memory-mcp\]' "$CODEX_LIFECYCLE_ROOT/config.toml" || - [ ! -s "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" ] || + ! grep -q 'search_graph' "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" || [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-scout.toml" ] || [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory.toml" ] || [ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-auditor.toml" ] || ! grep -q 'SessionStart' "$CODEX_LIFECYCLE_ROOT/config.toml" || ! grep -q 'SubagentStart' "$CODEX_LIFECYCLE_ROOT/config.toml"; then - echo "FAIL 8i: isolated fresh Codex install changed instructions or lost another surface" + echo "FAIL 8i: isolated fresh Codex install lost the pointer or another surface" exit 1 fi @@ -1742,8 +1751,15 @@ printf '%s\n' \ 'legacy managed guidance' \ '' \ '# Keep this line' > "$CODEX_INSTRUCTIONS" -CODEX_EXPECTED=$(smoke_mktemp_file) -printf '%s\n' '# Personal Codex guidance' '# Keep this line' > "$CODEX_EXPECTED" +CODEX_EXPECTED_MIGRATED=$(smoke_mktemp_file) +printf '%s\n' \ + '# Personal Codex guidance' \ + '' \ + 'For structural codebase exploration, use the installed `codebase-memory` skill.' \ + '' \ + '# Keep this line' > "$CODEX_EXPECTED_MIGRATED" +CODEX_EXPECTED_USER=$(smoke_mktemp_file) +printf '%s\n' '# Personal Codex guidance' '# Keep this line' > "$CODEX_EXPECTED_USER" CODEX_LEGACY_SHA=$(smoke_file_sha256 "$CODEX_INSTRUCTIONS") CODEX_PLAN=$(smoke_mktemp_file) @@ -1753,14 +1769,13 @@ HOME="$CODEX_LIFECYCLE_HOME" \ APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ "$BINARY" install --plan --skip-binary --clients=codex > "$CODEX_PLAN" -CODEX_CLEANUP_PATH=$(json_get "$CODEX_PLAN" \ - "next((x.get('path','') for x in d.get('cleanup_actions_planned', []) if x.get('agent') == 'Codex CLI' and x.get('kind') == 'instructions' and x.get('operation') == 'remove_managed_block_if_present'), '')") -CODEX_PLANS_INSTRUCTIONS=$(json_get "$CODEX_PLAN" \ - "any(str(x).replace('\\\\','/').endswith('/.codex/AGENTS.md') for x in d.get('instruction_files_planned', []))") -if ! exact_path_match "$CODEX_CLEANUP_PATH" "$CODEX_INSTRUCTIONS" || - [ "$CODEX_PLANS_INSTRUCTIONS" != "False" ] || +CODEX_INSTRUCTION_PATH=$(json_get "$CODEX_PLAN" \ + "next((str(x) for x in d.get('instruction_files_planned', []) if str(x).replace('\\\\','/').endswith('/.codex/AGENTS.md')), '')") +CODEX_CLEANUP_COUNT=$(json_get "$CODEX_PLAN" "len(d.get('cleanup_actions_planned', []))") +if ! exact_path_match "$CODEX_INSTRUCTION_PATH" "$CODEX_INSTRUCTIONS" || + [ "$CODEX_CLEANUP_COUNT" != "0" ] || [ "$CODEX_LEGACY_SHA" != "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" ]; then - echo "FAIL 8i: Codex plan receipt or no-mutation contract is wrong" + echo "FAIL 8i: Codex plan did not describe a non-mutating pointer upsert" exit 1 fi @@ -1771,9 +1786,9 @@ HOME="$CODEX_LIFECYCLE_HOME" \ APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ "$BINARY" install --dry-run --skip-binary --clients=codex -y > "$CODEX_DRY_LOG" 2>&1 -if ! grep -q 'managed block if present' "$CODEX_DRY_LOG" || +if ! grep -q 'managed activation pointer' "$CODEX_DRY_LOG" || [ "$CODEX_LEGACY_SHA" != "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" ]; then - echo "FAIL 8i: Codex dry-run did not preview byte-identical legacy cleanup" + echo "FAIL 8i: Codex dry-run did not preview a byte-identical pointer migration" exit 1 fi @@ -1785,8 +1800,9 @@ HOME="$CODEX_LIFECYCLE_HOME" \ LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ "$BINARY" install --skip-binary --clients=codex -y > "$CODEX_MIGRATE_LOG" 2>&1 if [ ! -f "$CODEX_INSTRUCTIONS" ] || - [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then - echo "FAIL 8i: Codex migration changed user-owned AGENTS.md bytes" + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \ + "$(smoke_file_sha256 "$CODEX_EXPECTED_MIGRATED")" ]; then + echo "FAIL 8i: Codex migration did not replace only the legacy managed block" exit 1 fi @@ -1797,23 +1813,30 @@ HOME="$CODEX_LIFECYCLE_HOME" \ LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ "$BINARY" install --skip-binary --clients=codex -y > /dev/null 2>&1 if [ ! -f "$CODEX_INSTRUCTIONS" ] || - [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then - echo "FAIL 8i: Codex reinstall restored global managed instructions" + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \ + "$(smoke_file_sha256 "$CODEX_EXPECTED_MIGRATED")" ] || + [ "$(grep -c '' "$CODEX_INSTRUCTIONS")" -ne 1 ]; then + echo "FAIL 8i: Codex reinstall changed or duplicated the activation pointer" exit 1 fi CODEX_UNINSTALL_LOG=$(smoke_mktemp_file) -HOME="$CODEX_LIFECYCLE_HOME" \ - CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ - XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ - APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ - LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ - "$BINARY" uninstall -y -n > "$CODEX_UNINSTALL_LOG" 2>&1 || true +if ! HOME="$CODEX_LIFECYCLE_HOME" \ + CODEX_HOME="$CODEX_LIFECYCLE_ROOT" \ + XDG_CONFIG_HOME="$CODEX_LIFECYCLE_HOME/.config" \ + APPDATA="$CODEX_LIFECYCLE_HOME/AppData/Roaming" \ + LOCALAPPDATA="$CODEX_LIFECYCLE_HOME/AppData/Local" \ + "$BINARY" uninstall -y -n > "$CODEX_UNINSTALL_LOG" 2>&1; then + echo "FAIL 8i: Codex uninstall returned nonzero" + cat "$CODEX_UNINSTALL_LOG" + exit 1 +fi if [ ! -f "$CODEX_INSTRUCTIONS" ] || - [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != "$(smoke_file_sha256 "$CODEX_EXPECTED")" ]; then - echo "FAIL 8i: Codex uninstall changed user-owned AGENTS.md bytes" + [ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \ + "$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ]; then + echo "FAIL 8i: Codex uninstall did not remove only the activation pointer" exit 1 fi -echo "OK 8i: isolated Codex fresh/plan/dry-run/migration/reinstall/uninstall lifecycle" +echo "OK 8i: isolated Codex activation-pointer lifecycle" # 8j-l: Gemini MCP + hooks + merge CMD=$(json_get "$FAKE_HOME/.gemini/settings.json" "d['mcpServers']['codebase-memory-mcp']['command']") diff --git a/src/cli/cli.c b/src/cli/cli.c index e0382a96e..c9da2e43c 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -1389,18 +1389,7 @@ static const char skill_content[] = "5. `search_graph` results default to 50 per page — check `has_more` and use `offset`.\n"; static const char codex_instructions_content[] = - "# Codebase Knowledge Graph\n" - "\n" - "This project uses codebase-memory-mcp to maintain a knowledge graph of the codebase.\n" - "Use the MCP tools to explore and understand the code:\n" - "\n" - "- `search_graph` — find functions, classes, routes by pattern\n" - "- `trace_path` — trace who calls a function or what it calls\n" - "- `get_code_snippet` — read function source code\n" - "- `query_graph` — run Cypher queries for complex patterns\n" - "- `get_architecture` — high-level project summary\n" - "\n" - "Always prefer graph tools over grep for code discovery.\n"; + "For structural codebase exploration, use the installed `codebase-memory` skill.\n"; /* Old skill names — cleaned up during install to remove stale directories. */ static const char *old_skill_names[] = { @@ -7522,8 +7511,7 @@ static void print_detected_agents(const cbm_detected_agents_t *a, const char *ho * behavior (it is the same code path with mutations disabled). */ typedef struct { char agent[CLI_BUF_32]; - /* mcp_config | instructions | skills | hook | cleanup_instructions */ - char kind[CLI_BUF_32]; + char kind[CLI_BUF_32]; /* mcp_config | instructions | skills | hook */ char path[CLI_BUF_1K]; } cbm_plan_entry_t; @@ -7861,17 +7849,21 @@ static bool install_generic_agent_config(const char *label, const char *binary_p return mcp_installed; } -static void cleanup_codex_legacy_instructions(const char *path, bool dry_run) { - if (!path || !cbm_file_exists(path)) { - return; +static bool install_codex_activation_pointer(const char *path, bool dry_run) { + if (!path) { + return false; } if (g_install_plan) { - plan_record("Codex CLI", "cleanup_instructions", path); - return; + plan_record("Codex CLI", "instructions", path); + return true; } - printf(" instructions cleanup: %s (managed block if present)\n", path); - if (!dry_run && cbm_remove_instructions(path) != CLI_OK) { - record_agent_config_error(false, "Codex CLI", "legacy_instructions_cleanup", path); + return dry_run || cbm_upsert_instructions(path, codex_instructions_content) == CLI_OK; +} + +static void report_codex_activation_pointer_install(const char *path, bool installed) { + printf(" instructions: %s (managed activation pointer)\n", path); + if (!installed) { + record_agent_config_error(false, "Codex CLI", "instructions_install", path); } } @@ -8812,6 +8804,8 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir); snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir); snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir); + /* A broken hook config must not keep legacy full guidance active. */ + bool pointer_installed = install_codex_activation_pointer(ip, dry_run); char command[CLI_BUF_8K]; char command_windows[CLI_BUF_8K]; char hooks_json[CLI_BUF_1K]; @@ -8839,11 +8833,16 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const record_agent_config_error_with_reason( false, "Codex CLI", commands_ok ? "hook_preflight" : "hook_command_build", cp, reason); + if (!g_install_plan) { + report_codex_activation_pointer_install(ip, pointer_installed); + } goto codex_install_done; } install_generic_agent_config("Codex CLI", binary_path, cp, NULL, dry_run, cbm_upsert_codex_mcp); - cleanup_codex_legacy_instructions(ip, dry_run); + if (!g_install_plan) { + report_codex_activation_pointer_install(ip, pointer_installed); + } install_agent_skill("Codex CLI", skills_dir, force, dry_run); install_tiered_agent_profiles( (cbm_tiered_profile_set_t){ @@ -9931,10 +9930,9 @@ static bool cli_binary_is_externally_managed(const char *self_path, bool self_pa } /* Build the agent.install.plan.v1 receipt (#388): a machine-readable list of - * the config / instruction / skill / agent / hook files `install` WOULD write - * plus conditional cleanup actions, produced by running the real install - * dispatch in record-only mode (no mutation, no network). Returns a heap JSON - * string (caller frees) or NULL. */ + * the config / instruction / skill / agent / hook files `install` WOULD write, produced by + * running the real install dispatch in record-only mode (no mutation, no + * network). Returns a heap JSON string (caller frees) or NULL. */ static char *cbm_build_install_plan_json_options(const char *home, const char *binary_path, bool skip_config) { if (!home || !binary_path) { @@ -10010,18 +10008,10 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b yyjson_mut_val *agent_files = yyjson_mut_arr(doc); yyjson_mut_val *prompt_files = yyjson_mut_arr(doc); yyjson_mut_val *hooks = yyjson_mut_arr(doc); - yyjson_mut_val *cleanups = yyjson_mut_arr(doc); for (int i = 0; i < plan.count; i++) { cbm_plan_entry_t *e = &plan.items[i]; if (strcmp(e->kind, "mcp_config") == 0) { yyjson_mut_arr_add_strcpy(doc, configs, e->path); - } else if (strcmp(e->kind, "cleanup_instructions") == 0) { - yyjson_mut_val *cleanup = yyjson_mut_obj(doc); - yyjson_mut_obj_add_strcpy(doc, cleanup, "agent", e->agent); - yyjson_mut_obj_add_str(doc, cleanup, "kind", "instructions"); - yyjson_mut_obj_add_str(doc, cleanup, "operation", "remove_managed_block_if_present"); - yyjson_mut_obj_add_strcpy(doc, cleanup, "path", e->path); - yyjson_mut_arr_add_val(cleanups, cleanup); } else if (strcmp(e->kind, "hook") == 0) { yyjson_mut_val *h = yyjson_mut_obj(doc); yyjson_mut_obj_add_strcpy(doc, h, "agent", e->agent); @@ -10046,7 +10036,6 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b yyjson_mut_obj_add_val(doc, root, "agent_files_planned", agent_files); yyjson_mut_obj_add_val(doc, root, "prompt_files_planned", prompt_files); yyjson_mut_obj_add_val(doc, root, "hooks_planned", hooks); - yyjson_mut_obj_add_val(doc, root, "cleanup_actions_planned", cleanups); yyjson_mut_obj_add_bool(doc, root, "writes_started", false); yyjson_mut_obj_add_bool(doc, root, "network_after_install", false); yyjson_mut_obj_add_str(doc, root, "next_safe_command", "codebase-memory-mcp install -y"); @@ -10730,6 +10719,17 @@ static void uninstall_agent_mcp_instr(mcp_uninstall_args_t paths, bool dry_run, } } +static bool uninstall_codex_activation_pointer(const char *path, bool dry_run) { + return path && (dry_run || cbm_remove_instructions(path) == CLI_OK); +} + +static void report_codex_activation_pointer_uninstall(const char *path, bool removed) { + printf(" instructions: removed managed activation pointer\n"); + if (!removed) { + record_agent_config_error(true, "Codex CLI", "instructions_uninstall", path); + } +} + static void uninstall_agent_skill(const char *label, const char *skills_dir, bool dry_run) { int removed = cbm_remove_skills(skills_dir, dry_run); printf(" %s skill: %d removed\n", label, removed); @@ -11164,6 +11164,7 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir); snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir); snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir); + bool pointer_removed = uninstall_codex_activation_pointer(ip, dry_run); cbm_agent_installed_binary_path(home, installed_binary, sizeof(installed_binary)); char hook_command[CLI_BUF_8K]; char hook_command_windows[CLI_BUF_8K]; @@ -11184,10 +11185,12 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char ? NULL : cbm_toml_codex_hook_failure_name(preflight_failure); record_agent_config_error_with_reason(true, "Codex CLI", "hook_preflight", cp, reason); + report_codex_activation_pointer_uninstall(ip, pointer_removed); goto codex_toml_done; } - uninstall_agent_mcp_instr((mcp_uninstall_args_t){"Codex CLI", cp, ip}, dry_run, + uninstall_agent_mcp_instr((mcp_uninstall_args_t){"Codex CLI", cp, NULL}, dry_run, cbm_remove_codex_mcp_owned); + report_codex_activation_pointer_uninstall(ip, pointer_removed); if (!dry_run && cbm_reconcile_codex_hooks_command(cp, hook_command, hook_command_windows, CBM_TOML_CODEX_HOOK_REMOVE, false) != CLI_OK) { diff --git a/tests/test_cli.c b/tests/test_cli.c index 8ff5a1c46..4d918a417 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -294,31 +294,13 @@ static bool test_plan_has_hook_for_agent(yyjson_val *root, const char *agent) { return false; } -static bool test_plan_cleanup_contains(yyjson_val *root, const char *agent, const char *kind, - const char *operation, const char *path) { - yyjson_val *items = root ? yyjson_obj_get(root, "cleanup_actions_planned") : NULL; - if (!items || !yyjson_is_arr(items)) { - return false; - } - size_t index; - size_t count; - yyjson_val *item; - yyjson_arr_foreach(items, index, count, item) { - yyjson_val *agent_value = yyjson_obj_get(item, "agent"); - yyjson_val *kind_value = yyjson_obj_get(item, "kind"); - yyjson_val *operation_value = yyjson_obj_get(item, "operation"); - yyjson_val *path_value = yyjson_obj_get(item, "path"); - if (agent_value && yyjson_is_str(agent_value) && kind_value && yyjson_is_str(kind_value) && - operation_value && yyjson_is_str(operation_value) && path_value && - yyjson_is_str(path_value) && strcmp(yyjson_get_str(agent_value), agent) == 0 && - strcmp(yyjson_get_str(kind_value), kind) == 0 && - strcmp(yyjson_get_str(operation_value), operation) == 0 && - strcmp(yyjson_get_str(path_value), path) == 0) { - return true; - } - } - return false; -} +static const char test_codex_activation_pointer[] = + "For structural codebase exploration, use the installed `codebase-memory` skill.\n"; + +static const char test_codex_activation_block[] = + "\n" + "For structural codebase exploration, use the installed `codebase-memory` skill.\n" + "\n"; static size_t test_count_substring(const char *text, const char *needle) { size_t count = 0U; @@ -2428,11 +2410,11 @@ TEST(cli_skill_files_content) { } TEST(cli_codex_instructions) { - /* Port of TestCodexInstructionsCreation */ const char *instr = cbm_get_codex_instructions(); ASSERT_NOT_NULL(instr); - ASSERT(strstr(instr, "Codebase Knowledge Graph") != NULL); - ASSERT(strstr(instr, "trace_path") != NULL); + ASSERT_STR_EQ(instr, test_codex_activation_pointer); + ASSERT_NULL(strstr(instr, "search_graph")); + ASSERT_NULL(strstr(instr, "trace_path")); PASS(); } @@ -4523,22 +4505,6 @@ TEST(cli_install_plan_receipt_no_mutation_issue388) { } } - yyjson_doc *doc = NULL; - if (!missing) { - doc = yyjson_read(json, strlen(json), 0); - if (!doc) { - missing = "a valid JSON document"; - } else { - yyjson_val *cleanups = - yyjson_obj_get(yyjson_doc_get_root(doc), "cleanup_actions_planned"); - if (!cleanups || !yyjson_is_arr(cleanups) || yyjson_arr_size(cleanups) != 0U) { - missing = "an empty cleanup_actions_planned array"; - } - } - } - if (doc) { - yyjson_doc_free(doc); - } free(json); /* Critical: building the plan must NOT have created any config file. */ @@ -7923,9 +7889,7 @@ TEST(cli_codex_respects_codex_home) { expected_config); bool plans_instructions = test_json_string_array_contains( plan_root, "instruction_files_planned", expected_instructions); - bool plans_cleanup = test_plan_cleanup_contains( - plan_root, "Codex CLI", "instructions", "remove_managed_block_if_present", - expected_instructions); + bool plans_cleanup = json && strstr(json, "remove_managed_block_if_present") != NULL; char *instructions_after = read_test_file_alloc(expected_instructions); bool plan_preserved_user_file = instructions_after && strcmp(instructions_after, user_instructions) == 0; @@ -7938,15 +7902,15 @@ TEST(cli_codex_respects_codex_home) { if (!agents.codex) FAIL("Codex detection must honor CODEX_HOME"); - if (!plans_config || plans_instructions || !plans_cleanup || !plan_preserved_user_file) - FAIL("Codex plan must keep config under CODEX_HOME and report legacy AGENTS cleanup " - "without planning a new instruction file or mutating user content"); + if (!plans_config || !plans_instructions || plans_cleanup || !plan_preserved_user_file) + FAIL("Codex plan must include the managed activation pointer under CODEX_HOME without " + "mutating existing user content"); PASS(); } -TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { +TEST(cli_codex_install_uses_global_activation_pointer_issue1689) { char tmpdir[256]; - snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-agents-cleanup-XXXXXX"); + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-agents-pointer-XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("cbm_mkdtemp failed"); @@ -7955,11 +7919,17 @@ TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { char config_path[640]; char skill_path[768]; char profile_path[768]; + char binary_path[768]; snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir); snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_home); snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_home); snprintf(skill_path, sizeof(skill_path), "%s/skills/codebase-memory/SKILL.md", codex_home); snprintf(profile_path, sizeof(profile_path), "%s/agents/codebase-memory.toml", codex_home); +#ifdef _WIN32 + snprintf(binary_path, sizeof(binary_path), "%s/.local/bin/codebase-memory-mcp.exe", tmpdir); +#else + snprintf(binary_path, sizeof(binary_path), "%s/.local/bin/codebase-memory-mcp", tmpdir); +#endif ASSERT_EQ(test_mkdirp(codex_home), 0); char *saved_home = save_test_env("HOME"); @@ -7970,47 +7940,54 @@ TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { cbm_setenv("CODEX_HOME", codex_home, 1); struct stat state; - int fresh_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); - bool fresh_did_not_create_agents = stat(agents_path, &state) != 0; + int fresh_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); + char *fresh_agents = read_test_file_alloc(agents_path); + bool fresh_pointer_installed = fresh_rc == 0 && fresh_agents && + strcmp(fresh_agents, test_codex_activation_block) == 0; char *config = read_test_file_alloc(config_path); bool other_surfaces_installed = fresh_rc == 0 && config && strstr(config, "[mcp_servers.codebase-memory-mcp]") && strstr(config, "SessionStart") && stat(skill_path, &state) == 0 && stat(profile_path, &state) == 0; + free(fresh_agents); free(config); const char *user_only = "# Personal Codex guidance\nKeep this byte-for-byte.\n"; ASSERT_EQ(write_test_file(agents_path, user_only), 0); - int dry_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true); + int dry_rc = cbm_install_agent_configs(tmpdir, binary_path, false, true); char *after_dry = read_test_file_alloc(agents_path); - int unowned_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + int unowned_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); char *after_unowned = read_test_file_alloc(agents_path); - bool unowned_preserved = dry_rc == 0 && unowned_rc == 0 && after_dry && after_unowned && - strcmp(after_dry, user_only) == 0 && - strcmp(after_unowned, user_only) == 0; + char expected_user_pointer[1024]; + int user_pointer_written = snprintf(expected_user_pointer, sizeof(expected_user_pointer), + "%s%s", user_only, test_codex_activation_block); + bool unowned_preserved = + user_pointer_written > 0 && (size_t)user_pointer_written < sizeof(expected_user_pointer) && + dry_rc == 0 && unowned_rc == 0 && after_dry && after_unowned && + strcmp(after_dry, user_only) == 0 && + strcmp(after_unowned, expected_user_pointer) == 0; free(after_dry); free(after_unowned); const char *legacy = "# Before\n\nlegacy\n" "\n# After\n"; - const char *legacy_removed = "# Before\n# After\n"; + char expected_migration[1024]; + int migration_written = snprintf(expected_migration, sizeof(expected_migration), + "# Before\n%s# After\n", test_codex_activation_block); ASSERT_EQ(write_test_file(agents_path, legacy), 0); - int managed_dry_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true); + int managed_dry_rc = cbm_install_agent_configs(tmpdir, binary_path, false, true); char *after_managed_dry = read_test_file_alloc(agents_path); - int managed_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + int managed_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); char *after_managed = read_test_file_alloc(agents_path); - int repeat_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + int repeat_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); char *after_repeat = read_test_file_alloc(agents_path); - bool managed_removed_once = managed_dry_rc == 0 && after_managed_dry && - strcmp(after_managed_dry, legacy) == 0 && managed_rc == 0 && - repeat_rc == 0 && after_managed && after_repeat && - strcmp(after_managed, legacy_removed) == 0 && - strcmp(after_repeat, legacy_removed) == 0; + bool managed_migrated_once = + migration_written > 0 && (size_t)migration_written < sizeof(expected_migration) && + managed_dry_rc == 0 && after_managed_dry && strcmp(after_managed_dry, legacy) == 0 && + managed_rc == 0 && repeat_rc == 0 && after_managed && after_repeat && + strcmp(after_managed, expected_migration) == 0 && + strcmp(after_repeat, expected_migration) == 0 && + test_count_substring(after_repeat, "") == 1U; free(after_managed_dry); free(after_managed); free(after_repeat); @@ -8018,11 +7995,11 @@ TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { const char *managed_only = "\nlegacy\n" "\n"; ASSERT_EQ(write_test_file(agents_path, managed_only), 0); - int managed_only_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + int managed_only_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); char *after_managed_only = read_test_file_alloc(agents_path); - bool empty_file_preserved = managed_only_rc == 0 && after_managed_only && - after_managed_only[0] == '\0' && stat(agents_path, &state) == 0; + bool marker_only_migrated = managed_only_rc == 0 && after_managed_only && + strcmp(after_managed_only, test_codex_activation_block) == 0 && + stat(agents_path, &state) == 0; free(after_managed_only); const char *malformed = "# Keep\n\nunterminated\n"; @@ -8030,8 +8007,7 @@ TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { ASSERT_EQ(remove(config_path), 0); ASSERT_EQ(remove(skill_path), 0); ASSERT_EQ(remove(profile_path), 0); - int malformed_rc = - cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + int malformed_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); char *after_malformed = read_test_file_alloc(agents_path); config = read_test_file_alloc(config_path); bool malformed_preserved = malformed_rc != 0 && after_malformed && @@ -8042,15 +8018,25 @@ TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689) { free(config); free(after_malformed); + ASSERT_EQ(write_test_file(agents_path, expected_user_pointer), 0); + char *uninstall_argv[] = {"uninstall", "--yes"}; + int uninstall_rc = cli_test_cmd_uninstall(2, uninstall_argv); + char *after_uninstall = read_test_file_alloc(agents_path); + bool uninstall_preserved_foreign = uninstall_rc == 0 && after_uninstall && + strcmp(after_uninstall, user_only) == 0; + free(after_uninstall); + restore_test_env("HOME", saved_home); restore_test_env("PATH", saved_path); restore_test_env("CODEX_HOME", saved_codex); test_rmdir_r(tmpdir); - if (!fresh_did_not_create_agents || !other_surfaces_installed || !unowned_preserved || - !managed_removed_once || !empty_file_preserved || !malformed_preserved) - FAIL("Codex install must stop creating global AGENTS guidance, remove only its owned " - "legacy block, preserve malformed or user-owned bytes, and keep other surfaces"); + if (!fresh_pointer_installed || !other_surfaces_installed || !unowned_preserved || + !managed_migrated_once || !marker_only_migrated || !malformed_preserved || + !uninstall_preserved_foreign) + FAIL("Codex install must keep only a tiny managed activation pointer, preserve foreign " + "bytes, migrate legacy blocks, fail closed on malformed markers, and remove the " + "pointer on uninstall"); PASS(); } @@ -10119,8 +10105,10 @@ TEST(cli_codex_migrates_to_single_hook_representation) { char hooks_path[640]; char config_path[640]; + char agents_path[640]; snprintf(hooks_path, sizeof(hooks_path), "%s/hooks.json", codex_dir); snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_dir); + snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_dir); int first_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); char *first = read_test_file_alloc(config_path); int repeat_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false); @@ -10160,14 +10148,17 @@ TEST(cli_codex_migrates_to_single_hook_representation) { snprintf(agent_path, sizeof(agent_path), "%s/agents/codebase-memory.toml", codex_dir); struct stat state; hooks = read_test_file_alloc(hooks_path); + char *agents_after_uninstall = read_test_file_alloc(agents_path); bool independent_cleanup = uninstall_rc != 0 && stat(binary_path, &state) == 0 && stat(skill_path, &state) != 0 && stat(agent_path, &state) != 0 && - hooks && !strstr(hooks, "hook-augment"); + hooks && !strstr(hooks, "hook-augment") && agents_after_uninstall && + agents_after_uninstall[0] == '\0'; free(hooks); + free(agents_after_uninstall); char bad_home[256]; snprintf(bad_home, sizeof(bad_home), "/tmp/cli-codex-preflight-XXXXXX"); - bool no_partial = false; + bool pointer_only_on_preflight_failure = false; if (cbm_mkdtemp(bad_home)) { char bad_codex[512]; char bad_config[640]; @@ -10181,18 +10172,110 @@ TEST(cli_codex_migrates_to_single_hook_representation) { cbm_setenv("PATH", bad_home, 1); int bad_rc = cbm_install_agent_configs(bad_home, binary_path, false, false); char *bad_after = read_test_file_alloc(bad_config); - no_partial = bad_rc != 0 && bad_after && strcmp(bad_after, ambiguous) == 0 && - stat(bad_agents, &state) != 0; + char *bad_agents_after = read_test_file_alloc(bad_agents); + pointer_only_on_preflight_failure = + bad_rc != 0 && bad_after && strcmp(bad_after, ambiguous) == 0 && bad_agents_after && + strcmp(bad_agents_after, test_codex_activation_block) == 0; free(bad_after); + free(bad_agents_after); test_rmdir_r(bad_home); } restore_test_env("HOME", saved_home); restore_test_env("PATH", saved_path); restore_test_env("CODEX_HOME", saved_codex); test_rmdir_r(tmpdir); - if (!lifecycle_ok || !migrated || !independent_cleanup || !no_partial) - FAIL("Codex lifecycle preflight must be idempotent, transactional, and independently " - "clean owned side files"); + if (!lifecycle_ok || !migrated || !independent_cleanup || + !pointer_only_on_preflight_failure) + FAIL("Codex lifecycle preflight must be idempotent, preserve the activation pointer " + "contract, and independently clean owned side files"); + PASS(); +} + +TEST(cli_codex_pointer_migration_precedes_hook_preflight_issue1689) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-cleanup-preflight-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char codex_dir[512]; + char config_path[640]; + char agents_path[640]; + char skill_path[768]; + char profile_path[768]; + snprintf(codex_dir, sizeof(codex_dir), "%s/.codex", tmpdir); + snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_dir); + snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_dir); + snprintf(skill_path, sizeof(skill_path), "%s/skills/codebase-memory/SKILL.md", codex_dir); + snprintf(profile_path, sizeof(profile_path), "%s/agents/codebase-memory.toml", codex_dir); + if (test_mkdirp(codex_dir) != 0) { + test_rmdir_r(tmpdir); + FAIL("failed to create Codex cleanup preflight fixture directory"); + } + + const char *ambiguous = + "[hooks]\nSessionStart = [{ matcher = 'startup|resume|clear|compact', hooks = [" + "{ type = 'command', command = 'codebase-memory-mcp hook-augment' }, " + "{ type = 'command', command = 'foreign' }] }]\n"; + const char *legacy = "# Before\n\nlegacy\n" + "\n# After\n"; + char legacy_migrated[1024]; + int migrated_written = snprintf(legacy_migrated, sizeof(legacy_migrated), + "# Before\n%s# After\n", test_codex_activation_block); + if (migrated_written <= 0 || (size_t)migrated_written >= sizeof(legacy_migrated)) { + test_rmdir_r(tmpdir); + FAIL("failed to build expected Codex pointer migration"); + } + if (write_test_file(config_path, ambiguous) != 0 || + write_test_file(agents_path, legacy) != 0) { + test_rmdir_r(tmpdir); + FAIL("failed to write Codex cleanup preflight fixture"); + } + + char *saved_home = save_test_env("HOME"); + char *saved_path = save_test_env("PATH"); + char *saved_codex = save_test_env("CODEX_HOME"); + cbm_setenv("HOME", tmpdir, 1); + cbm_setenv("PATH", tmpdir, 1); + cbm_unsetenv("CODEX_HOME"); + + char *plan = cbm_build_install_plan_json(tmpdir, "/opt/codebase-memory-mcp"); + yyjson_doc *plan_doc = plan ? yyjson_read(plan, strlen(plan), 0) : NULL; + yyjson_val *plan_root = plan_doc ? yyjson_doc_get_root(plan_doc) : NULL; + bool plans_pointer = + test_json_string_array_contains(plan_root, "instruction_files_planned", agents_path); + char *config_after_plan = read_test_file_alloc(config_path); + char *agents_after_plan = read_test_file_alloc(agents_path); + bool plan_preserved_files = config_after_plan && agents_after_plan && + strcmp(config_after_plan, ambiguous) == 0 && + strcmp(agents_after_plan, legacy) == 0; + + int install_rc = + cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false); + char *config_after_install = read_test_file_alloc(config_path); + char *agents_after_install = read_test_file_alloc(agents_path); + struct stat state; + bool preflight_failed_closed = + install_rc != 0 && config_after_install && strcmp(config_after_install, ambiguous) == 0 && + stat(skill_path, &state) != 0 && stat(profile_path, &state) != 0; + bool pointer_migrated = + agents_after_install && strcmp(agents_after_install, legacy_migrated) == 0; + + free(config_after_plan); + free(agents_after_plan); + free(config_after_install); + free(agents_after_install); + if (plan_doc) { + yyjson_doc_free(plan_doc); + } + free(plan); + restore_test_env("HOME", saved_home); + restore_test_env("PATH", saved_path); + restore_test_env("CODEX_HOME", saved_codex); + test_rmdir_r(tmpdir); + + if (!plans_pointer || !plan_preserved_files || !preflight_failed_closed || !pointer_migrated) + FAIL("Codex legacy instructions must migrate to the activation pointer independently of " + "hook preflight while other config surfaces fail closed"); PASS(); } @@ -10261,13 +10344,15 @@ TEST(cli_codex_preflight_reports_heading_and_reason) { fclose(capture); } char *after = read_test_file_alloc(config_path); - struct stat state; - bool unchanged = after && strcmp(after, ambiguous) == 0 && stat(agents_path, &state) != 0; + char *agents_after = read_test_file_alloc(agents_path); + bool unchanged = after && strcmp(after, ambiguous) == 0 && agents_after && + strcmp(agents_after, test_codex_activation_block) == 0; bool diagnostic = strstr(output, "Codex CLI:\nerror: agent_config agent=Codex CLI op=hook_preflight path=") != NULL && strstr(output, "reason=ambiguous_hook_ownership") != NULL; free(after); + free(agents_after); restore_test_env("HOME", saved_home); restore_test_env("PATH", saved_path); @@ -14022,7 +14107,7 @@ SUITE(cli) { RUN_TEST(cli_claude_user_scope_avoids_nested_mcp_json); RUN_TEST(cli_codex_respects_codex_home); RUN_TEST(cli_grok_respects_grok_home); - RUN_TEST(cli_codex_install_removes_only_legacy_global_instructions_issue1689); + RUN_TEST(cli_codex_install_uses_global_activation_pointer_issue1689); RUN_TEST(cli_gemini_session_hook_uses_json_for_all_sources); RUN_TEST(cli_gemini_installs_dedicated_graph_subagent); RUN_TEST(cli_antigravity_does_not_imply_gemini); @@ -14067,6 +14152,7 @@ SUITE(cli) { RUN_TEST(cli_claude_hook_scripts_shell_quote_binary_path); RUN_TEST(cli_claude_hook_commands_shell_quote_custom_config_dir); RUN_TEST(cli_codex_migrates_to_single_hook_representation); + RUN_TEST(cli_codex_pointer_migration_precedes_hook_preflight_issue1689); #ifndef _WIN32 RUN_TEST(cli_codex_preflight_reports_heading_and_reason); #endif